diff --git a/.CI/CreateAppImage.sh b/.CI/CreateAppImage.sh index 486c14ff6..12995b33a 100755 --- a/.CI/CreateAppImage.sh +++ b/.CI/CreateAppImage.sh @@ -2,13 +2,28 @@ set -e +# Print all commands as they are run +set -x + if [ ! -f ./bin/chatterino ] || [ ! -x ./bin/chatterino ]; then echo "ERROR: No chatterino binary file found. This script must be run in the build folder, and chatterino must be built first." exit 1 fi -export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/opt/qt512/lib/" -export PATH="/opt/qt512/bin:$PATH" +if [ -n "$Qt5_DIR" ]; then + echo "Using Qt DIR from Qt5_DIR: $Qt5_DIR" + _QT_DIR="$Qt5_DIR" +elif [ -n "$Qt6_DIR" ]; then + echo "Using Qt DIR from Qt6_DIR: $Qt6_DIR" + _QT_DIR="$Qt6_DIR" +fi + +if [ -n "$_QT_DIR" ]; then + export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:${_QT_DIR}/lib" + export PATH="${_QT_DIR}/bin:$PATH" +else + echo "No Qt environment variable set, assuming system-installed Qt" +fi script_path=$(readlink -f "$0") script_dir=$(dirname "$script_path") @@ -25,20 +40,32 @@ echo "" cp "$chatterino_dir"/resources/icon.png ./appdir/chatterino.png -linuxdeployqt_path="linuxdeployqt-6-x86_64.AppImage" -linuxdeployqt_url="https://github.com/probonopd/linuxdeployqt/releases/download/6/linuxdeployqt-6-x86_64.AppImage" +linuxdeployqt_path="linuxdeployqt-x86_64.AppImage" +linuxdeployqt_url="https://github.com/probonopd/linuxdeployqt/releases/download/continuous/linuxdeployqt-continuous-x86_64.AppImage" if [ ! -f "$linuxdeployqt_path" ]; then - wget -nv "$linuxdeployqt_url" + echo "Downloading LinuxDeployQT from $linuxdeployqt_url to $linuxdeployqt_path" + curl --location --fail --silent "$linuxdeployqt_url" -o "$linuxdeployqt_path" chmod a+x "$linuxdeployqt_path" fi -if [ ! -f appimagetool-x86_64.AppImage ]; then - wget -nv "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" - chmod a+x appimagetool-x86_64.AppImage + +appimagetool_path="appimagetool-x86_64.AppImage" +appimagetool_url="https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" + +if [ ! -f "$appimagetool_path" ]; then + echo "Downloading AppImageTool from $appimagetool_url to $appimagetool_path" + curl --location --fail --silent "$appimagetool_url" -o "$appimagetool_path" + chmod a+x "$appimagetool_path" fi + +# For some reason, the copyright file for libc was not found. We need to manually copy it from the host system +mkdir -p appdir/usr/share/doc/libc6/ +cp /usr/share/doc/libc6/copyright appdir/usr/share/doc/libc6/ + echo "Run LinuxDeployQT" ./"$linuxdeployqt_path" \ - appdir/usr/share/applications/*.desktop \ + --appimage-extract-and-run \ + appdir/usr/share/applications/com.chatterino.chatterino.desktop \ -no-translations \ -bundle-non-qt-libs \ -unsupported-allow-new-glibc @@ -56,4 +83,9 @@ cd "$here/usr" exec "$here/usr/bin/chatterino" "$@"' > appdir/AppRun chmod a+x appdir/AppRun -./appimagetool-x86_64.AppImage appdir +./"$appimagetool_path" \ + --appimage-extract-and-run \ + appdir + +# TODO: Create appimage in a unique directory instead maybe idk? +rm -rf appdir diff --git a/.CI/CreateDMG.sh b/.CI/CreateDMG.sh index 3eb2202c4..7174eb605 100755 --- a/.CI/CreateDMG.sh +++ b/.CI/CreateDMG.sh @@ -1,18 +1,38 @@ -#!/bin/sh +#!/usr/bin/env bash -if [ -d bin/chatterino.app ] && [ ! -d chatterino.app ]; then - >&2 echo "Moving bin/chatterino.app down one directory" - mv bin/chatterino.app chatterino.app +set -eo pipefail + +if [ ! -d chatterino.app ]; then + echo "ERROR: No 'chatterino.app' dir found in the build directory. Make sure you've run ./CI/MacDeploy.sh" + exit 1 +fi + +if [ -z "$OUTPUT_DMG_PATH" ]; then + echo "ERROR: Must specify the path for where to save the final .dmg. Make sure you've set the OUTPUT_DMG_PATH environment variable." + exit 1 +fi + +if [ -z "$SKIP_VENV" ]; then + echo "Creating python3 virtual environment" + python3 -m venv venv + echo "Entering python3 virtual environment" + . venv/bin/activate + echo "Installing dmgbuild" + python3 -m pip install dmgbuild +fi + +if [ -n "$MACOS_CODESIGN_CERTIFICATE" ]; then + echo "Codesigning force deep inside the app" + codesign -s "$MACOS_CODESIGN_CERTIFICATE" --deep --force chatterino.app + echo "Done!" fi -echo "Running MACDEPLOYQT" -$Qt5_DIR/bin/macdeployqt chatterino.app -echo "Creating python3 virtual environment" -python3 -m venv venv -echo "Entering python3 virtual environment" -. venv/bin/activate -echo "Installing dmgbuild" -python3 -m pip install dmgbuild echo "Running dmgbuild.." -dmgbuild --settings ./../.CI/dmg-settings.py -D app=./chatterino.app Chatterino2 chatterino-osx.dmg +dmgbuild --settings ./../.CI/dmg-settings.py -D app=./chatterino.app Chatterino2 "$OUTPUT_DMG_PATH" echo "Done!" + +if [ -n "$MACOS_CODESIGN_CERTIFICATE" ]; then + echo "Codesigning the dmg" + codesign -s "$MACOS_CODESIGN_CERTIFICATE" --deep --force "$OUTPUT_DMG_PATH" + echo "Done!" +fi diff --git a/.CI/CreateUbuntuDeb.sh b/.CI/CreateUbuntuDeb.sh index 9a90ca32f..5c31b625b 100755 --- a/.CI/CreateUbuntuDeb.sh +++ b/.CI/CreateUbuntuDeb.sh @@ -1,35 +1,101 @@ #!/bin/sh + set -e +breakline() { + printf "================================================================================\n\n" +} + +# Configured in the CI step +install_prefix="appdir/usr" + +# The directory we finally pack into our .deb package +packaging_dir="package" + +# Get the Ubuntu Release (e.g. 20.04, 22.04 or 24.04) +ubuntu_release="$(lsb_release -rs)" + +# The final path where we'll save the .deb package +deb_path="Chatterino-ubuntu-${ubuntu_release}-x86_64.deb" + +# Refactor opportunity: +case "$ubuntu_release" in + 20.04) + # Qt6 static-linked deb, see https://github.com/Chatterino/docker + dependencies="libc6, libstdc++6, libblkid1, libbsd0, libexpat1, libffi7, libfontconfig1, libfreetype6, libglib2.0-0, libglvnd0, libglx0, libgraphite2-3, libharfbuzz0b, libicu66, libjpeg-turbo8, libmount1, libopengl0, libpcre2-16-0, libpcre3, libpng16-16, libselinux1, libssl1.1, libuuid1, libx11-xcb1, libxau6, libxcb1, libxcb-cursor0, libxcb-glx0, libxcb-icccm4, libxcb-image0, libxcb-keysyms1, libxcb-randr0, libxcb-render0, libxcb-render-util0, libxcb-shape0, libxcb-shm0, libxcb-sync1, libxcb-util1, libxcb-xfixes0, libxcb-xkb1, libxdmcp6, libxkbcommon0, libxkbcommon-x11-0, zlib1g" + ;; + 22.04) + # Qt6 static-linked deb, see https://github.com/Chatterino/docker + dependencies="libc6, libstdc++6, libglx0, libopengl0, libpng16-16, libharfbuzz0b, libfreetype6, libfontconfig1, libjpeg-turbo8, libxcb-glx0, libegl1, libx11-6, libxkbcommon0, libx11-xcb1, libxkbcommon-x11-0, libxcb-cursor0, libxcb-icccm4, libxcb-image0, libxcb-keysyms1, libxcb-randr0, libxcb-render-util0, libxcb-shm0, libxcb-sync1, libxcb-xfixes0, libxcb-render0, libxcb-shape0, libxcb-xkb1, libxcb1, libbrotli1, libglib2.0-0, zlib1g, libicu70, libpcre2-16-0, libssl3, libgraphite2-3, libexpat1, libuuid1, libxcb-util1, libxau6, libxdmcp6, libffi8, libmount1, libselinux1, libpcre3, libbsd0, libblkid1, libpcre2-8-0, libmd0" + ;; + 24.04) + # Qt6 static-linked deb, see https://github.com/Chatterino/docker + dependencies="libc6, libstdc++6, libglx0, libopengl0, libpng16-16, libharfbuzz0b, libfreetype6, libfontconfig1, libjpeg-turbo8, libxcb-glx0, libegl1, libx11-6, libxkbcommon0, libx11-xcb1, libxkbcommon-x11-0, libxcb-cursor0, libxcb-icccm4, libxcb-image0, libxcb-keysyms1, libxcb-randr0, libxcb-render-util0, libxcb-shm0, libxcb-sync1, libxcb-xfixes0, libxcb-render0, libxcb-shape0, libxcb-xkb1, libxcb1, libbrotli1, libglib2.0-0, zlib1g, libicu74, libpcre2-16-0, libssl3, libgraphite2-3, libexpat1, libuuid1, libxcb-util1, libxau6, libxdmcp6, libffi8, libmount1, libselinux1, libpcre3, libbsd0, libblkid1, libpcre2-8-0, libmd0" + ;; + *) + echo "Unsupported Ubuntu release $ubuntu_release" + exit 1 + ;; +esac + +echo "Building Ubuntu .deb file on '$ubuntu_release'" +echo "Dependencies: $dependencies" + if [ ! -f ./bin/chatterino ] || [ ! -x ./bin/chatterino ]; then echo "ERROR: No chatterino binary file found. This script must be run in the build folder, and chatterino must be built first." exit 1 fi -chatterino_version=$(git describe | cut -c 2-) -echo "Found Chatterino version $chatterino_version via git" +chatterino_version=$(git describe 2>/dev/null) || true +if [ "$(echo "$chatterino_version" | cut -c1-1)" = 'v' ]; then + chatterino_version="$(echo "$chatterino_version" | cut -c2-)" +else + chatterino_version="0.0.0-dev" +fi -rm -vrf "./package" || true # delete any old packaging dir +# Make sure no old remnants of a previous packaging remains +rm -vrf "$packaging_dir" -# create ./package/ from scratch -mkdir package/DEBIAN -p -packaging_dir="$(realpath ./package)" +mkdir -p "$packaging_dir/DEBIAN" echo "Making control file" cat >> "$packaging_dir/DEBIAN/control" << EOF Package: chatterino -Section: net -Priority: optional +Version: $chatterino_version Architecture: amd64 Maintainer: Mm2PL -Description: Testing out chatterino as a Ubuntu package -Depends: libc6, libqt5concurrent5, libqt5core5a, libqt5dbus5, libqt5gui5, libqt5multimedia5, libqt5network5, libqt5svg5, libqt5widgets5, libssl1.1, libstdc++6 +Depends: $dependencies +Section: net +Priority: optional +Homepage: https://github.com/Chatterino/chatterino2 +Description: Ubuntu package built for $ubuntu_release EOF -echo "Version: $chatterino_version" >> "$packaging_dir/DEBIAN/control" +cat "$packaging_dir/DEBIAN/control" +breakline -echo "Running make install in package dir" -DESTDIR="$packaging_dir" make INSTALL_ROOT="$packaging_dir" -j"$(nproc)" install; find "$packaging_dir/" -echo "" -echo "Building package..." -dpkg-deb --build "$packaging_dir" "Chatterino.deb" +echo "Running make install" +make install +find "$install_prefix" +breakline + + +echo "Merge install into packaging dir" +cp -rv "$install_prefix/" "$packaging_dir/" +find "$packaging_dir" +breakline + + +echo "Building package" +dpkg-deb --build "$packaging_dir" "$deb_path" +breakline + + +echo "Package info" +dpkg --info "$deb_path" +breakline + + +echo "Package contents" +dpkg --contents "$deb_path" +breakline diff --git a/.CI/MacDeploy.sh b/.CI/MacDeploy.sh new file mode 100755 index 000000000..c798bfe16 --- /dev/null +++ b/.CI/MacDeploy.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash + +# Bundle relevant qt & system dependencies into the ./chatterino.app folder + +set -eo pipefail + +if [ -d bin/chatterino.app ] && [ ! -d chatterino.app ]; then + >&2 echo "Moving bin/chatterino.app down one directory" + mv bin/chatterino.app chatterino.app +fi + +if [ -n "$Qt5_DIR" ]; then + echo "Using Qt DIR from Qt5_DIR: $Qt5_DIR" + _QT_DIR="$Qt5_DIR" +elif [ -n "$Qt6_DIR" ]; then + echo "Using Qt DIR from Qt6_DIR: $Qt6_DIR" + _QT_DIR="$Qt6_DIR" +fi + +if [ -n "$_QT_DIR" ]; then + export PATH="${_QT_DIR}/bin:$PATH" +else + echo "No Qt environment variable set, assuming system-installed Qt" +fi + +echo "Running MACDEPLOYQT" + +_macdeployqt_args=() + +if [ -n "$MACOS_CODESIGN_CERTIFICATE" ]; then + _macdeployqt_args+=("-codesign=$MACOS_CODESIGN_CERTIFICATE") +fi + +macdeployqt chatterino.app "${_macdeployqt_args[@]}" + +if [ -n "$MACOS_CODESIGN_CERTIFICATE" ]; then + # Validate that chatterino.app was codesigned correctly + codesign -v chatterino.app +fi diff --git a/.CI/build-installer.ps1 b/.CI/build-installer.ps1 new file mode 100644 index 000000000..b60145d82 --- /dev/null +++ b/.CI/build-installer.ps1 @@ -0,0 +1,51 @@ +if (-not (Test-Path -PathType Container Chatterino2)) { + Write-Error "Couldn't find a folder called 'Chatterino2' in the current directory."; + exit 1 +} + +# Check if we're on a tag +$OldErrorActionPref = $ErrorActionPreference; +$ErrorActionPreference = 'Continue'; +git describe --exact-match --match 'v*' *> $null; +$isTagged = $?; +$ErrorActionPreference = $OldErrorActionPref; + +$defines = $null; +if ($isTagged) { + # This is a release. + # Make sure, any existing `modes` file is overwritten for the user, + # for example when updating from nightly to stable. + Write-Output "" | Out-File Chatterino2/modes -Encoding ASCII; + $installerBaseName = "Chatterino.Installer"; +} +else { + Write-Output nightly | Out-File Chatterino2/modes -Encoding ASCII; + $defines = "/DIS_NIGHTLY=1"; + $installerBaseName = "Chatterino.Nightly.Installer"; +} + +if ($Env:GITHUB_OUTPUT) { + # This is used in CI when creating the artifact + "C2_INSTALLER_BASE_NAME=$installerBaseName" >> "$Env:GITHUB_OUTPUT" +} + +# Copy vc_redist.x64.exe +if ($null -eq $Env:VCToolsRedistDir) { + Write-Error "VCToolsRedistDir is not set. Forgot to set Visual Studio environment variables?"; + exit 1 +} +Copy-Item "$Env:VCToolsRedistDir\vc_redist.x64.exe" .; + +$VCRTVersion = (Get-Item "$Env:VCToolsRedistDir\vc_redist.x64.exe").VersionInfo; + +# Build the installer +ISCC ` + /DWORKING_DIR="$($pwd.Path)\" ` + /DINSTALLER_BASE_NAME="$installerBaseName" ` + /DSHIPPED_VCRT_MINOR="$($VCRTVersion.FileMinorPart)" ` + /DSHIPPED_VCRT_VERSION="$($VCRTVersion.FileDescription)" ` + $defines ` + /O. ` + "$PSScriptRoot\chatterino-installer.iss"; + +Move-Item "$installerBaseName.exe" "$installerBaseName$($Env:VARIANT_SUFFIX).exe" diff --git a/.CI/chatterino-installer.iss b/.CI/chatterino-installer.iss new file mode 100644 index 000000000..5068a3c29 --- /dev/null +++ b/.CI/chatterino-installer.iss @@ -0,0 +1,134 @@ +; Script generated by the Inno Setup Script Wizard. +; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! + +#define MyAppName "Chatterino" +#define MyAppVersion "2.5.1" +#define MyAppPublisher "Chatterino Team" +#define MyAppURL "https://www.chatterino.com" +#define MyAppExeName "chatterino.exe" + +; used in build-installer.ps1 +; if set, must end in a backslash +#ifndef WORKING_DIR +#define WORKING_DIR "" +#endif + +; Set to the build part of the VCRT version +#ifndef SHIPPED_VCRT_BUILD +#define SHIPPED_VCRT_BUILD 0 +#endif +; Set to the string representation of the VCRT version +#ifndef SHIPPED_VCRT_VERSION +#define SHIPPED_VCRT_VERSION ? +#endif + +[Setup] +; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. +; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) +AppId={{F5FE6614-04D4-4D32-8600-0ABA0AC113A4} +AppName={#MyAppName} +AppVersion={#MyAppVersion} +VersionInfoVersion={#MyAppVersion} +AppVerName={#MyAppName} {#MyAppVersion} +AppPublisher={#MyAppPublisher} +AppPublisherURL={#MyAppURL} +AppSupportURL={#MyAppURL} +AppUpdatesURL={#MyAppURL} +DefaultDirName={autopf}\{#MyAppName} +DisableProgramGroupPage=yes +ArchitecturesInstallIn64BitMode=x64 +;Uncomment the following line to run in non administrative install mode (install for current user only.) +;PrivilegesRequired=lowest +PrivilegesRequiredOverridesAllowed=dialog +OutputDir=out +; This is defined by the build-installer.ps1 script, +; but kept optional for regular use. +#ifdef INSTALLER_BASE_NAME +OutputBaseFilename={#INSTALLER_BASE_NAME} +#else +OutputBaseFilename=Chatterino.Installer +#endif +Compression=lzma +SolidCompression=yes +WizardStyle=modern +UsePreviousTasks=no +UninstallDisplayIcon={app}\{#MyAppExeName} +RestartIfNeededByRun=no + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" + +#ifdef IS_NIGHTLY +[Messages] +SetupAppTitle=Setup (Nightly) +SetupWindowTitle=Setup - %1 (Nightly) +#endif + +[Tasks] +; Only show this option if the VCRT can be updated. +Name: "vcredist"; Description: "Install the required {#SHIPPED_VCRT_VERSION} ({code:VCRTDescription})"; Check: NeedsNewVCRT(); +; GroupDescription: "{cm:AdditionalIcons}"; +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; Flags: unchecked +Name: "freshinstall"; Description: "Fresh install (delete old settings/logs)"; Flags: unchecked + +[Files] +Source: "{#WORKING_DIR}Chatterino2\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs +Source: "{#WORKING_DIR}vc_redist.x64.exe"; DestDir: "{tmp}"; Tasks: vcredist; +; NOTE: Don't use "Flags: ignoreversion" on any shared system files + +[Icons] +Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" +Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon + +[Run] +; VC++ redistributable +Filename: {tmp}\vc_redist.x64.exe; Parameters: "/install /passive /norestart"; StatusMsg: "Installing 64-bit Windows Universal Runtime..."; Flags: waituntilterminated; Tasks: vcredist +; Run chatterino +Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent + +[InstallDelete] +; Delete cache on install +Type: filesandordirs; Name: "{userappdata}\Chatterino2\Cache" +; Delete %appdata%\Chatterino2 on freshinstall +Type: filesandordirs; Name: "{userappdata}\Chatterino2"; Tasks: freshinstall + +[UninstallDelete] +; Delete cache on uninstall +Type: filesandordirs; Name: "{userappdata}\Chatterino2\Cache" + +[Code] +// Get the VCRT version as a string. Null if the version could not be found. +function GetVCRT(): Variant; +var + VCRTVersion: String; +begin + Result := Null; + if RegQueryStringValue(HKEY_LOCAL_MACHINE, 'SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64', 'Version', VCRTVersion) then + Result := VCRTVersion; +end; + +// Gets a description about the VCRT installed vs shipped. +// This doesn't compare the versions. +function VCRTDescription(Param: String): String; +var + VCRTVersion: Variant; +begin + VCRTVersion := GetVCRT; + if VarIsNull(VCRTVersion) then + Result := 'none is installed' + else + Result := VCRTVersion + ' is installed'; +end; + +// Checks if a new VCRT is needed by comparing the minor version (the major one is locked at 14). +function NeedsNewVCRT(): Boolean; +var + VCRTBuild: Cardinal; +begin + Result := True; + if RegQueryDWordValue(HKEY_LOCAL_MACHINE, 'SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64', 'Minor', VCRTBuild) then + begin + if VCRTBuild >= {#SHIPPED_VCRT_MINOR} then + Result := False; + end; +end; diff --git a/.CI/chatterino-nightly.flatpakref b/.CI/chatterino-nightly.flatpakref new file mode 100644 index 000000000..61cf409e3 --- /dev/null +++ b/.CI/chatterino-nightly.flatpakref @@ -0,0 +1,9 @@ +[Flatpak Ref] +Name=com.chatterino.chatterino +Branch=beta +Title=com.chatterino.chatterino from flathub +IsRuntime=false +Url=https://dl.flathub.org/beta-repo/ +SuggestRemoteName=flathub-beta +GPGKey=mQINBFlD2sABEADsiUZUOYBg1UdDaWkEdJYkTSZD68214m8Q1fbrP5AptaUfCl8KYKFMNoAJRBXn9FbE6q6VBzghHXj/rSnA8WPnkbaEWR7xltOqzB1yHpCQ1l8xSfH5N02DMUBSRtD/rOYsBKbaJcOgW0K21sX+BecMY/AI2yADvCJEjhVKrjR9yfRX+NQEhDcbXUFRGt9ZT+TI5yT4xcwbvvTu7aFUR/dH7+wjrQ7lzoGlZGFFrQXSs2WI0WaYHWDeCwymtohXryF8lcWQkhH8UhfNJVBJFgCY8Q6UHkZG0FxMu8xnIDBMjBmSZKwKQn0nwzwM2afskZEnmNPYDI8nuNsSZBZSAw+ThhkdCZHZZRwzmjzyRuLLVFpOj3XryXwZcSefNMPDkZAuWWzPYjxS80cm2hG1WfqrG0Gl8+iX69cbQchb7gbEb0RtqNskTo9DDmO0bNKNnMbzmIJ3/rTbSahKSwtewklqSP/01o0WKZiy+n/RAkUKOFBprjJtWOZkc8SPXV/rnoS2dWsJWQZhuPPtv3tefdDiEyp7ePrfgfKxuHpZES0IZRiFI4J/nAUP5bix+srcIxOVqAam68CbAlPvWTivRUMRVbKjJiGXIOJ78wAMjqPg3QIC0GQ0EPAWwAOzzpdgbnG7TCQetaVV8rSYCuirlPYN+bJIwBtkOC9SWLoPMVZTwQARAQABtC5GbGF0aHViIFJlcG8gU2lnbmluZyBLZXkgPGZsYXRodWJAZmxhdGh1Yi5vcmc+iQJUBBMBCAA+FiEEblwF2XnHba+TwIE1QYTdTZB6fK4FAllD2sACGwMFCRLMAwAFCwkIBwIGFQgJCgsCBBYCAwECHgECF4AACgkQQYTdTZB6fK5RJQ/+Ptd4sWxaiAW91FFk7+wmYOkEe1NY2UDNJjEEz34PNP/1RoxveHDt43kYJQ23OWaPJuZAbu+fWtjRYcMBzOsMCaFcRSHFiDIC9aTp4ux/mo+IEeyarYt/oyKb5t5lta6xaAqg7rwt65jW5/aQjnS4h7eFZ+dAKta7Y/fljNrOznUp81/SMcx4QA5G2Pw0hs4Xrxg59oONOTFGBgA6FF8WQghrpR7SnEe0FSEOVsAjwQ13Cfkfa7b70omXSWp7GWfUzgBKyoWxKTqzMN3RQHjjhPJcsQnrqH5enUu4Pcb2LcMFpzimHnUgb9ft72DP5wxfzHGAWOUiUXHbAekfq5iFks8cha/RST6wkxG3Rf44Zn09aOxh1btMcGL+5xb1G0BuCQnA0fP/kDYIPwh9z22EqwRQOspIcvGeLVkFeIfubxpcMdOfQqQnZtHMCabV5Q/Rk9K1ZGc8M2hlg8gHbXMFch2xJ0Wu72eXbA/UY5MskEeBgawTQnQOK/vNm7t0AJMpWK26Qg6178UmRghmeZDj9uNRc3EI1nSbgvmGlpDmCxaAGqaGL1zW4KPW5yN25/qeqXcgCvUjZLI9PNq3Kvizp1lUrbx7heRiSoazCucvHQ1VHUzcPVLUKKTkoTP8okThnRRRsBcZ1+jI4yMWIDLOCT7IW3FePr+3xyuy5eEo9a25Ag0EWUPa7AEQALT/CmSyZ8LWlRYQZKYw417p7Z2hxqd6TjwkwM3IQ1irumkWcTZBZIbBgrSOg6CcXD2oWydCQHWi9qaxhuhEl2bJL5LskmBcMxVdQeD0LLHd8QUnbnnIby8ocvWN1alPfvJFjCUTrmD22U1ycOzRw2lIe4kiQONbOZtdWrVImQQSndjFlisitbmlWHvHm2lOOYy8+GJB7YffVV193hmnBSJffCy4bvkuLxsI+n1DhOzc7MPV3z6HGk4HiEcF0yyt9tCYhpsxHFdBoq2h771HfAcS0s98EVAqYMFnf9em+4cnYpdI6mhIfS1FQiKl6DBAYA8tT3ggla00DurPo0JwX/zN+PaO5h/6O9aCZwV7G6rbkgMuqMergXaf8oP38gr0z+MqWnkfM63Bodq68GP4l4hd02BoFBbDf38TMuGQB14+twJMdfbAxo2MbgluvQgfwHfZ2ca6gyEY+9s/YD1gugLjV+S6CB51WkFNe1z4tAPgJZNxUcKCbeaHNbthl8Hks/pY9RCEseX/EdfzF18epbSjJMPh4DPQXbUoFwmyuYcoBOPmvZHNl9hK7B/1RP8w1ZrXk8qdupC0SNbafX7270B7lMMVImzZetGsM9ypXJ6llhp3FwW09iseNyGJGPsr/dvTMGDXqOPfU/9SAS1LSTY4K9PbRtdrBE318YX8mIk5ABEBAAGJBHIEGAEIACYWIQRuXAXZecdtr5PAgTVBhN1NkHp8rgUCWUPa7AIbAgUJEswDAAJACRBBhN1NkHp8rsF0IAQZAQgAHRYhBFSmzd2JGfsgQgDYrFYnAunj7X7oBQJZQ9rsAAoJEFYnAunj7X7oR6AP/0KYmiAFeqx14Z43/6s2gt3VhxlSd8bmcVV7oJFbMhdHBIeWBp2BvsUf00I0Zl14ZkwCKfLwbbORC2eIxvzJ+QWjGfPhDmS4XUSmhlXxWnYEveSek5Tde+fmu6lqKM8CHg5BNx4GWIX/vdLi1wWJZyhrUwwICAxkuhKxuP2Z1An48930eslTD2GGcjByc27+9cIZjHKa07I/aLffo04V+oMT9/tgzoquzgpVV4jwekADo2MJjhkkPveSNI420bgT+Q7Fi1l0X1aFUniBvQMsaBa27PngWm6xE2ZYvh7nWCdd5g0c0eLIHxWwzV1lZ4Ryx4ITO/VL25ItECcjhTRdYa64sA62MYSaB0x3eR+SihpgP3wSNPFu3MJo6FKTFdi4CBAEmpWHFW7FcRmd+cQXeFrHLN3iNVWryy0HK/CUEJmiZEmpNiXecl4vPIIuyF0zgSCztQtKoMr+injpmQGC/rF/ELBVZTUSLNB350S0Ztvw0FKWDAJSxFmoxt3xycqvvt47rxTrhi78nkk6jATKGyvP55sO+K7Q7Wh0DXA69hvPrYW2eu8jGCdVGxi6HX7L1qcfEd0378S71dZ3g9o6KKl1OsDWWQ6MJ6FGBZedl/ibRfs8p5+sbCX3lQSjEFy3rx6n0rUrXx8U2qb+RCLzJlmC5MNBOTDJwHPcX6gKsUcXZrEQALmRHoo3SrewO41RCr+5nUlqiqV3AohBMhnQbGzyHf2+drutIaoh7Rj80XRh2bkkuPLwlNPf+bTXwNVGse4bej7B3oV6Ae1N7lTNVF4Qh+1OowtGjmfJPWo0z1s6HFJVxoIof9z58Msvgao0zrKGqaMWaNQ6LUeC9g9Aj/9Uqjbo8X54aLiYs8Z1WNc06jKP+gv8AWLtv6CR+l2kLez1YMDucjm7v6iuCMVAmZdmxhg5I/X2+OM3vBsqPDdQpr2TPDLX3rCrSBiS0gOQ6DwN5N5QeTkxmY/7QO8bgLo/Wzu1iilH4vMKW6LBKCaRx5UEJxKpL4wkgITsYKneIt3NTHo5EOuaYk+y2+Dvt6EQFiuMsdbfUjs3seIHsghX/cbPJa4YUqZAL8C4OtVHaijwGo0ymt9MWvS9yNKMyT0JhN2/BdeOVWrHk7wXXJn/ZjpXilicXKPx4udCF76meE+6N2u/T+RYZ7fP1QMEtNZNmYDOfA6sViuPDfQSHLNbauJBo/n1sRYAsL5mcG22UDchJrlKvmK3EOADCQg+myrm8006LltubNB4wWNzHDJ0Ls2JGzQZCd/xGyVmUiidCBUrD537WdknOYE4FD7P0cHaM9brKJ/M8LkEH0zUlo73bY4XagbnCqve6PvQb5G2Z55qhWphd6f4B6DGed86zJEa/RhS +RuntimeRepo=https://dl.flathub.org/repo/flathub.flatpakrepo diff --git a/.CI/deploy-crt.ps1 b/.CI/deploy-crt.ps1 new file mode 100644 index 000000000..cab12693e --- /dev/null +++ b/.CI/deploy-crt.ps1 @@ -0,0 +1,30 @@ +param ( + [string] $InstallDir = "Chatterino2" +) + +if ($null -eq $Env:VCToolsRedistDir) { + Write-Error "VCToolsRedistDir is not set. Forgot to set Visual Studio environment variables?"; + exit 1 +} + +# A path to the runtime libraries (e.g. "$Env:VCToolsRedistDir\onecore\x64\Microsoft.VC143.CRT") +$vclibs = (Get-ChildItem "$Env:VCToolsRedistDir\onecore\x64" -Filter '*.CRT')[0].FullName; + +# All executables and libraries in the installation directory +$targets = Get-ChildItem -Recurse -Include '*.dll', '*.exe' $InstallDir; +# All dependencies of the targets (with duplicates) +$all_deps = $targets | ForEach-Object { (dumpbin /DEPENDENTS $_.FullName) -match '^(?!Dump of).+\.dll$' } | ForEach-Object { $_.Trim() }; +# All dependencies without duplicates +$dependencies = $all_deps | Sort-Object -Unique; + +$n_deployed = 0; +foreach ($dll in $dependencies) { + Write-Output "Checking for $dll"; + if (Test-Path -PathType Leaf "$vclibs\$dll") { + Write-Output "Deploying $dll"; + Copy-Item "$vclibs\$dll" "$InstallDir\$dll" -Force; + $n_deployed++; + } +} + +Write-Output "Deployed $n_deployed libraries"; diff --git a/.CI/format-recent-changes.py b/.CI/format-recent-changes.py new file mode 100644 index 000000000..630cfb85c --- /dev/null +++ b/.CI/format-recent-changes.py @@ -0,0 +1,61 @@ +from datetime import datetime, timezone +import os +import subprocess +import re + +LINE_REGEX = re.compile( + r"""(?x) +^(?P[A-Fa-f0-9]+)\s+ +\( + <(?P[^>]+)>\s+ + (?P[^\s]+\s[^\s]+\s[^\s]+)\s+ + (?P\d+) +\)\s +(?P.*)$ +""" +) +VERSION_REGEX = re.compile(r"^#+\s*v?\d") + +# contains lines in the form of +# {commit-sha} (<{email}>\s+{date}\s+{line-no}) {line} +p = subprocess.run( + ["git", "blame", "-e", "--date=iso", "../CHANGELOG.md"], + cwd=os.path.dirname(os.path.realpath(__file__)), + text=True, + check=True, + capture_output=True, +) + +unreleased_lines: list[tuple[datetime, str]] = [] +for line in p.stdout.splitlines(): + if not line: + continue + m = LINE_REGEX.match(line) + assert m, f"Failed to match '{line}'" + content = m.group("content") + + if not content: + continue + if content.startswith("#"): + if VERSION_REGEX.match(content): + break + continue # ignore lines with '#' + + d = datetime.fromisoformat(m.group("date")) + d = d.astimezone(tz=timezone.utc) + content = content.replace("- ", f"- [{d.strftime('%Y-%m-%d')}] ", 1) + unreleased_lines.append((d, content)) + +unreleased_lines.sort(key=lambda it: it[0], reverse=True) + +if len(unreleased_lines) == 0: + print("No changes since last release.") + +for _, line in unreleased_lines[:5]: + print(line) + +if len(unreleased_lines) > 5: + print("
More Changes\n") + for _, line in unreleased_lines[5:]: + print(line) + print("
") diff --git a/.CI/full-ubuntu-build.sh b/.CI/full-ubuntu-build.sh new file mode 100755 index 000000000..5c2e9f801 --- /dev/null +++ b/.CI/full-ubuntu-build.sh @@ -0,0 +1,39 @@ +#!/bin/sh + +# TODO: Investigate if the -fno-sized-deallocation flag is still necessary +# TODO: Test appimage/deb creation + +set -e + +env + +BUILD_TESTS="On" +BUILD_BENCHMARKS="ON" + +ubuntu_version="$(lsb_release -sr)" +if [ "$ubuntu_version" = "20.04" ]; then + BUILD_TESTS="Off" + BUILD_BENCHMARKS="Off" +fi + +rm -rf build +mkdir build +cmake \ + -B build \ + -DCMAKE_INSTALL_PREFIX=appdir/usr/ \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_APP=On \ + -DBUILD_TESTS="$BUILD_TESTS" \ + -DBUILD_BENCHMARKS="$BUILD_BENCHMARKS" \ + -DUSE_PRECOMPILED_HEADERS=OFF \ + -DCMAKE_EXPORT_COMPILE_COMMANDS=On \ + -DCHATTERINO_PLUGINS="$C2_PLUGINS" \ + -DCMAKE_PREFIX_PATH="$Qt6_DIR/lib/cmake" \ + -DBUILD_WITH_QT6="$C2_BUILD_WITH_QT6" \ + -DCHATTERINO_STATIC_QT_BUILD=On \ + -DCMAKE_CXX_FLAGS="-fno-sized-deallocation" \ + . +cmake --build build + +# sh ./../.CI/CreateAppImage.sh +# sh ./../.CI/CreateUbuntuDeb.sh diff --git a/.CI/setup-clang-tidy.sh b/.CI/setup-clang-tidy.sh new file mode 100755 index 000000000..4884285eb --- /dev/null +++ b/.CI/setup-clang-tidy.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +set -ev; + +# aqt installs into .qtinstall/Qt//gcc_64 +# This is doing the same as jurplel/install-qt-action +# See https://github.com/jurplel/install-qt-action/blob/74ca8cd6681420fc8894aed264644c7a76d7c8cb/action/src/main.ts#L52-L74 +qtpath=$(echo .qtinstall/Qt/[0-9]*/*/bin/qmake | sed -e s:/bin/qmake$::) +export LD_LIBRARY_PATH="$qtpath/lib" +export QT_ROOT_DIR=$qtpath +export QT_PLUGIN_PATH="$qtpath/plugins" +export PATH="$PATH:$(realpath "$qtpath/bin")" +export Qt6_DIR="$(realpath "$qtpath")" + +cmake -S. -Bbuild-clang-tidy \ + -DCMAKE_BUILD_TYPE=Debug \ + -DPAJLADA_SETTINGS_USE_BOOST_FILESYSTEM=On \ + -DUSE_PRECOMPILED_HEADERS=OFF \ + -DCMAKE_EXPORT_COMPILE_COMMANDS=On \ + -DCHATTERINO_LTO=Off \ + -DCHATTERINO_PLUGINS=On \ + -DBUILD_WITH_QT6=On \ + -DBUILD_TESTS=On \ + -DBUILD_BENCHMARKS=On + +# Run MOC and UIC +# This will compile the dependencies +# Get the targets using `ninja -t targets | grep autogen` +cmake --build build-clang-tidy --parallel -t \ + Core_autogen \ + LibCommuni_autogen \ + Model_autogen \ + Util_autogen \ + chatterino-lib_autogen diff --git a/.cirrus.yml b/.cirrus.yml index 94c6d2059..d4ac409cd 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -1,13 +1,26 @@ freebsd_instance: - image: freebsd-12-1-release-amd64 + image_family: freebsd-14-0 task: install_script: - - pkg install -y boost-libs git qt5-buildtools qt5-concurrent qt5-core qt5-multimedia qt5-svg qtkeychain qt5-qmake cmake qt5-linguist + - pkg install -y boost-libs git qt6-base qt6-svg qt6-5compat qt6-imageformats qtkeychain-qt6 cmake script: | git submodule init git submodule update mkdir build cd build - cmake CMAKE_C_COMPILER="cc" -DCMAKE_CXX_COMPILER="c++" -DCMAKE_C_FLAGS="-O2 -pipe -fstack-protector-strong -fno-strict-aliasing " -DCMAKE_CXX_FLAGS="-O2 -pipe -fstack-protector-strong -fno-strict-aliasing " -DLINK_OPTIONS="-fstack-protector-strong" -DCMAKE_INSTALL_PREFIX="/usr/local" -DCMAKE_BUILD_TYPE="release" .. + c++ --version + cmake \ + -DCMAKE_C_COMPILER="cc" \ + -DCMAKE_CXX_COMPILER="c++" \ + -DCMAKE_C_FLAGS="-O2 -pipe -fstack-protector-strong -fno-strict-aliasing " \ + -DCMAKE_CXX_FLAGS="-O2 -pipe -fstack-protector-strong -fno-strict-aliasing " \ + -DLINK_OPTIONS="-fstack-protector-strong" \ + -DCMAKE_INSTALL_PREFIX="/usr/local" \ + -DUSE_SYSTEM_QTKEYCHAIN="ON" \ + -DCMAKE_BUILD_TYPE="release" \ + -DCMAKE_EXPORT_COMPILE_COMMANDS="ON" \ + -DBUILD_WITH_QT6="ON" \ + .. + cat compile_commands.json make -j $(getconf _NPROCESSORS_ONLN) diff --git a/tests/.clang-format b/.clang-format similarity index 52% rename from tests/.clang-format rename to .clang-format index f34c1465b..cfbe49d31 100644 --- a/tests/.clang-format +++ b/.clang-format @@ -9,14 +9,13 @@ AllowShortLoopsOnASingleLine: false AlwaysBreakAfterDefinitionReturnType: false AlwaysBreakBeforeMultilineStrings: false BasedOnStyle: Google -BraceWrapping: { - AfterClass: 'true' - AfterControlStatement: 'true' - AfterFunction: 'true' - AfterNamespace: 'false' - BeforeCatch: 'true' - BeforeElse: 'true' -} +BraceWrapping: + AfterClass: "true" + AfterControlStatement: "true" + AfterFunction: "true" + AfterNamespace: "false" + BeforeCatch: "true" + BeforeElse: "true" BreakBeforeBraces: Custom BreakConstructorInitializersBeforeComma: true ColumnLimit: 80 @@ -27,9 +26,28 @@ IndentCaseLabels: true IndentWidth: 4 IndentWrappedFunctionNames: true IndentPPDirectives: AfterHash -IncludeBlocks: Preserve +SortIncludes: CaseInsensitive +IncludeBlocks: Regroup +IncludeCategories: + # Project includes + - Regex: '^"[a-zA-Z\._-]+(/[a-zA-Z0-9\._-]+)*"$' + Priority: 1 + # Qt includes + - Regex: '^$' + Priority: 3 + CaseSensitive: true + # LibCommuni includes + - Regex: "^$" + Priority: 3 + # Standard library includes + - Regex: "^<[a-zA-Z_]+>$" + Priority: 4 + # Third party library includes + - Regex: "^<([a-zA-Z_0-9-]+/)*[a-zA-Z_0-9-]+.h(pp)?>$" + Priority: 3 NamespaceIndentation: Inner PointerBindsToType: false SpacesBeforeTrailingComments: 2 Standard: Auto ReflowComments: false +InsertNewlineAtEOF: true diff --git a/.clang-tidy b/.clang-tidy index b279868d2..b5e8bef8c 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,47 +1,78 @@ -Checks: '-*, - clang-diagnostic-*, - llvm-*, - misc-*, - -misc-unused-parameters, - readability-identifier-naming, - -llvm-header-guard, - modernize-*, - readability-*, - performance-*, - misc-*, - bugprone-*, - cert-*, - cppcoreguidelines-*, - -cppcoreguidelines-pro-type-cstyle-cast, - -cppcoreguidelines-pro-bounds-pointer-arithmetic, - -cppcoreguidelines-pro-bounds-array-to-pointer-decay, - -cppcoreguidelines-pro-type-member-init, - -cppcoreguidelines-owning-memory, - -cppcoreguidelines-avoid-magic-numbers, - -readability-magic-numbers, - -performance-noexcept-move-constructor, - -misc-non-private-member-variables-in-classes, - -cppcoreguidelines-non-private-member-variables-in-classes, - -modernize-use-nodiscard, - -modernize-use-trailing-return-type, - -readability-identifier-length, - -readability-function-cognitive-complexity, - -bugprone-easily-swappable-parameters, - ' +Checks: "-*, + clang-diagnostic-*, + llvm-*, + misc-*, + -misc-unused-parameters, + readability-identifier-naming, + -llvm-header-guard, + -llvm-include-order, + modernize-*, + readability-*, + performance-*, + misc-*, + bugprone-*, + cert-*, + cppcoreguidelines-*, + -cppcoreguidelines-pro-type-cstyle-cast, + -cppcoreguidelines-pro-bounds-pointer-arithmetic, + -cppcoreguidelines-pro-bounds-array-to-pointer-decay, + -cppcoreguidelines-owning-memory, + -cppcoreguidelines-avoid-magic-numbers, + -cppcoreguidelines-avoid-const-or-ref-data-members, + -readability-magic-numbers, + -performance-noexcept-move-constructor, + -misc-non-private-member-variables-in-classes, + -misc-no-recursion, + -cppcoreguidelines-non-private-member-variables-in-classes, + -modernize-use-nodiscard, + -modernize-use-trailing-return-type, + -readability-identifier-length, + -readability-function-cognitive-complexity, + -bugprone-easily-swappable-parameters, + -cert-err58-cpp, + -modernize-avoid-c-arrays, + -misc-include-cleaner + " CheckOptions: - - key: readability-identifier-naming.ClassCase - value: CamelCase - - key: readability-identifier-naming.EnumCase - value: CamelCase - - key: readability-identifier-naming.FunctionCase - value: camelBack - - key: readability-identifier-naming.MemberCase - value: camelBack - - key: readability-identifier-naming.PrivateMemberSuffix - value: _ - - key: readability-identifier-naming.UnionCase - value: CamelCase - - key: readability-identifier-naming.GlobalVariableCase - value: UPPER_CASE - - key: readability-identifier-naming.VariableCase - value: camelBack + - key: readability-identifier-naming.ClassCase + value: CamelCase + - key: readability-identifier-naming.EnumCase + value: CamelCase + + - key: readability-identifier-naming.FunctionCase + value: camelBack + - key: readability-identifier-naming.FunctionIgnoredRegexp + value: ^TEST$ + + - key: readability-identifier-naming.MemberCase + value: camelBack + - key: readability-identifier-naming.PrivateMemberIgnoredRegexp + value: ^.*_$ + - key: readability-identifier-naming.ProtectedMemberIgnoredRegexp + value: ^.*_$ + - key: readability-identifier-naming.UnionCase + value: CamelCase + - key: readability-identifier-naming.GlobalConstantCase + value: UPPER_CASE + - key: readability-identifier-naming.GlobalVariableCase + value: UPPER_CASE + - key: readability-identifier-naming.VariableCase + value: camelBack + - key: readability-implicit-bool-conversion.AllowPointerConditions + value: true + + # Lua state + - key: readability-identifier-naming.LocalPointerIgnoredRegexp + value: ^L$ + + # Benchmarks + - key: readability-identifier-naming.FunctionIgnoredRegexp + value: ^BM_[^_]+$ + - key: readability-identifier-naming.ClassIgnoredRegexp + value: ^BM_[^_]+$ + + - key: misc-const-correctness.AnalyzeValues + value: false + + - key: cppcoreguidelines-special-member-functions.AllowSoleDefaultDtor + value: true diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 000000000..db7699448 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,7 @@ +comment: false +ignore: + - "/usr/local*/**/*" + - "/usr/include/**/*" + - "lib/" + - "**/ui_*.h" + - "**/moc_*.cpp" diff --git a/.docker/Dockerfile-ubuntu-20.04-base b/.docker/Dockerfile-ubuntu-20.04-base new file mode 100644 index 000000000..d193850f0 --- /dev/null +++ b/.docker/Dockerfile-ubuntu-20.04-base @@ -0,0 +1,38 @@ +FROM ubuntu:20.04 + +ENV TZ=UTC +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +RUN apt-get update && apt-get -y install --no-install-recommends \ + cmake \ + virtualenv \ + rapidjson-dev \ + libfuse2 \ + libssl-dev \ + libboost-dev \ + libxcb-randr0-dev \ + libboost-system-dev \ + libboost-filesystem-dev \ + libpulse-dev \ + libxkbcommon-x11-0 \ + build-essential \ + libgl1-mesa-dev \ + libxcb-icccm4 \ + libxcb-image0 \ + libxcb-keysyms1 \ + libxcb-render-util0 \ + libxcb-xinerama0 + +RUN apt-get -y install \ + git \ + lsb-release \ + python3-pip && \ + apt-get clean all + +# Install Qt as we do in CI + +RUN pip3 install -U pip && \ + pip3 install aqtinstall && \ + aqt install-qt linux desktop 5.12.12 && \ + mkdir -p /opt/qt512 && \ + mv /5.12.12/gcc_64/* /opt/qt512 diff --git a/.docker/Dockerfile-ubuntu-20.04-build b/.docker/Dockerfile-ubuntu-20.04-build new file mode 100644 index 000000000..4e566bb0d --- /dev/null +++ b/.docker/Dockerfile-ubuntu-20.04-build @@ -0,0 +1,17 @@ +FROM chatterino-ubuntu-20.04-base + +ADD . /src + +RUN mkdir /src/build + +# cmake +RUN cd /src/build && \ + CXXFLAGS=-fno-sized-deallocation cmake \ + -DCMAKE_INSTALL_PREFIX=appdir/usr/ \ + -DCMAKE_PREFIX_PATH=/opt/qt512/lib/cmake \ + -DBUILD_WITH_QTKEYCHAIN=OFF \ + .. + +# build +RUN cd /src/build && \ + make -j8 diff --git a/.docker/Dockerfile-ubuntu-20.04-package b/.docker/Dockerfile-ubuntu-20.04-package new file mode 100644 index 000000000..4d0a7b189 --- /dev/null +++ b/.docker/Dockerfile-ubuntu-20.04-package @@ -0,0 +1,21 @@ +FROM chatterino-ubuntu-20.04-build + +# In CI, this is set from the aqtinstall action +ENV Qt5_DIR=/opt/qt512 + +WORKDIR /src/build + +ADD .CI /src/.CI + +# Install dependencies necessary for AppImage packaging +RUN apt-get update && apt-get -y install --no-install-recommends \ + curl \ + libfontconfig \ + libxrender1 \ + file + +# package deb +RUN ./../.CI/CreateUbuntuDeb.sh + +# package appimage +RUN ./../.CI/CreateAppImage.sh diff --git a/.docker/Dockerfile-ubuntu-22.04-base b/.docker/Dockerfile-ubuntu-22.04-base new file mode 100644 index 000000000..5ce5b583d --- /dev/null +++ b/.docker/Dockerfile-ubuntu-22.04-base @@ -0,0 +1,44 @@ +FROM ubuntu:22.04 + +ENV TZ=UTC +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +RUN apt-get update && apt-get -y install --no-install-recommends \ + cmake \ + virtualenv \ + rapidjson-dev \ + libfuse2 \ + libssl-dev \ + libboost-dev \ + libxcb-randr0-dev \ + libboost-system-dev \ + libboost-filesystem-dev \ + libpulse-dev \ + libxkbcommon-x11-0 \ + build-essential \ + libgl1-mesa-dev \ + libxcb-icccm4 \ + libxcb-image0 \ + libxcb-keysyms1 \ + libxcb-render-util0 \ + libxcb-xinerama0 \ + libfontconfig + +RUN apt-get -y install \ + git \ + lsb-release \ + python3-pip && \ + apt-get clean all + +# Install Qt as we do in CI + +RUN pip3 install -U pip && \ + pip3 install aqtinstall && \ + aqt install-qt linux desktop 5.15.2 && \ + mkdir -p /opt/qt515 && \ + mv /5.15.2/gcc_64/* /opt/qt515 + +ADD ./.patches /tmp/.patches + +# Apply Qt patches +RUN patch "/opt/qt515/include/QtConcurrent/qtconcurrentthreadengine.h" /tmp/.patches/qt5-on-newer-gcc.patch diff --git a/.docker/Dockerfile-ubuntu-22.04-build b/.docker/Dockerfile-ubuntu-22.04-build new file mode 100644 index 000000000..5b16f6842 --- /dev/null +++ b/.docker/Dockerfile-ubuntu-22.04-build @@ -0,0 +1,17 @@ +FROM chatterino-ubuntu-22.04-base + +ADD . /src + +RUN mkdir /src/build + +# cmake +RUN cd /src/build && \ + CXXFLAGS=-fno-sized-deallocation cmake \ + -DCMAKE_INSTALL_PREFIX=appdir/usr/ \ + -DCMAKE_PREFIX_PATH=/opt/qt515/lib/cmake \ + -DBUILD_WITH_QTKEYCHAIN=OFF \ + .. + +# build +RUN cd /src/build && \ + make -j8 diff --git a/.docker/Dockerfile-ubuntu-22.04-package b/.docker/Dockerfile-ubuntu-22.04-package new file mode 100644 index 000000000..e3b546918 --- /dev/null +++ b/.docker/Dockerfile-ubuntu-22.04-package @@ -0,0 +1,21 @@ +FROM chatterino-ubuntu-22.04-build + +# In CI, this is set from the aqtinstall action +ENV Qt5_DIR=/opt/qt515 + +WORKDIR /src/build + +ADD .CI /src/.CI + +# Install dependencies necessary for AppImage packaging +RUN apt-get update && apt-get -y install --no-install-recommends \ + curl \ + libxcb-shape0 \ + libfontconfig1 \ + file + +# package deb +RUN ./../.CI/CreateUbuntuDeb.sh + +# package appimage +RUN ./../.CI/CreateAppImage.sh diff --git a/.docker/Dockerfile-ubuntu-22.04-qt6-build b/.docker/Dockerfile-ubuntu-22.04-qt6-build new file mode 100644 index 000000000..3fd5b8a20 --- /dev/null +++ b/.docker/Dockerfile-ubuntu-22.04-qt6-build @@ -0,0 +1,59 @@ +ARG UBUNTU_VERSION=22.04 + +FROM ubuntu:$UBUNTU_VERSION + +ARG QT_VERSION=6.2.4 +ARG BUILD_WITH_QT6=ON + +ENV TZ=UTC +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +RUN apt-get update && apt-get -y install --no-install-recommends \ + cmake \ + virtualenv \ + rapidjson-dev \ + libfuse2 \ + libssl-dev \ + libboost-dev \ + libxcb-randr0-dev \ + libboost-system-dev \ + libboost-filesystem-dev \ + libpulse-dev \ + libxkbcommon-x11-0 \ + build-essential \ + libgl1-mesa-dev \ + libxcb-icccm4 \ + libxcb-image0 \ + libxcb-keysyms1 \ + libxcb-render-util0 \ + libxcb-xinerama0 \ + libfontconfig1-dev + +RUN apt-get -y install \ + git \ + lsb-release \ + python3-pip && \ + apt-get clean all + +# Install Qt as we do in CI + +RUN pip3 install -U pip && \ + pip3 install aqtinstall && \ + aqt install-qt linux desktop $QT_VERSION -O /opt/qt --modules qt5compat + +ADD . /src + +RUN mkdir /src/build + +# cmake +RUN cd /src/build && \ + CXXFLAGS=-fno-sized-deallocation cmake \ + -DBUILD_WITH_QT6=$BUILD_WITH_QT6 \ + -DCMAKE_INSTALL_PREFIX=appdir/usr/ \ + -DCMAKE_PREFIX_PATH=/opt/qt/$QT_VERSION/gcc_64/lib/cmake \ + -DBUILD_WITH_QTKEYCHAIN=OFF \ + .. + +# build +RUN cd /src/build && \ + make -j8 diff --git a/.docker/Dockerfile-ubuntu-22.04-qt6-package b/.docker/Dockerfile-ubuntu-22.04-qt6-package new file mode 100644 index 000000000..265754915 --- /dev/null +++ b/.docker/Dockerfile-ubuntu-22.04-qt6-package @@ -0,0 +1,23 @@ +ARG UBUNTU_VERSION=22.04 + +FROM chatterino-ubuntu-$UBUNTU_VERSION-qt6-build + +# In CI, this is set from the aqtinstall action +ENV Qt6_DIR=/opt/qt/6.2.4/gcc_64 + +WORKDIR /src/build + +ADD .CI /src/.CI + +# Install dependencies necessary for AppImage packaging +RUN apt-get update && apt-get -y install --no-install-recommends \ + curl \ + libxcb-shape0 \ + libfontconfig1 \ + file + +# package deb +RUN ./../.CI/CreateUbuntuDeb.sh + +# package appimage +RUN ./../.CI/CreateAppImage.sh diff --git a/.docker/Dockerfile-ubuntu-22.04-test b/.docker/Dockerfile-ubuntu-22.04-test new file mode 100644 index 000000000..d3d2b50f4 --- /dev/null +++ b/.docker/Dockerfile-ubuntu-22.04-test @@ -0,0 +1,24 @@ +FROM chatterino-ubuntu-22.04-base + +ADD . /src + +RUN mkdir /src/build + +# cmake +RUN cd /src/build && \ + CXXFLAGS=-fno-sized-deallocation cmake \ + -DCMAKE_PREFIX_PATH=/opt/qt515/lib/cmake \ + -DUSE_PRECOMPILED_HEADERS=OFF \ + -DBUILD_WITH_QTKEYCHAIN=OFF \ + -DBUILD_TESTS=ON \ + .. + +# build +RUN cd /src/build && \ + make -j8 + +ENV QT_QPA_PLATFORM=minimal +ENV QT_PLUGIN_PATH=/opt/qt515/plugins + +# test +CMD /src/build/bin/chatterino-test diff --git a/.docker/README.md b/.docker/README.md new file mode 100644 index 000000000..08fc9261b --- /dev/null +++ b/.docker/README.md @@ -0,0 +1,42 @@ +## Groups + +### Ubuntu 20.04 package + +`Dockerfile-ubuntu-20.04-package` relies on `Dockerfile-ubuntu-20.04-build` + +To build, from the repo root + +1. Build a docker image that contains all the dependencies necessary to build Chatterino on Ubuntu 20.04 + `docker buildx build -t chatterino-ubuntu-20.04-base -f .docker/Dockerfile-ubuntu-20.04-base .` +1. Build a docker image that contains all the build artifacts and source from building Chatterino on Ubuntu 20.04 + `docker buildx build -t chatterino-ubuntu-20.04-build -f .docker/Dockerfile-ubuntu-20.04-build .` +1. Build a docker image that uses the above-built image & packages it into a .deb file + `docker buildx build -t chatterino-ubuntu-20.04-package -f .docker/Dockerfile-ubuntu-20.04-package .` + +To extract the final package, you can run the following command: +`docker run -v $PWD:/opt/mount --rm -it chatterino-ubuntu-20.04-package bash -c "cp /src/build/Chatterino-x86_64.deb /opt/mount/"` + +### Ubuntu 22.04 package + +`Dockerfile-ubuntu-22.04-package` relies on `Dockerfile-ubuntu-22.04-build` + +To build, from the repo root + +1. Build a docker image that contains all the dependencies necessary to build Chatterino on Ubuntu 22.04 + `docker buildx build -t chatterino-ubuntu-22.04-base -f .docker/Dockerfile-ubuntu-22.04-base .` +1. Build a docker image that contains all the build artifacts and source from building Chatterino on Ubuntu 22.04 + `docker buildx build -t chatterino-ubuntu-22.04-build -f .docker/Dockerfile-ubuntu-22.04-build .` +1. Build a docker image that uses the above-built image & packages it into a .deb file + `docker buildx build -t chatterino-ubuntu-22.04-package -f .docker/Dockerfile-ubuntu-22.04-package .` + +To extract the final package, you can run the following command: +`docker run -v $PWD:/opt/mount --rm -it chatterino-ubuntu-22.04-package bash -c "cp /src/build/Chatterino-x86_64.deb /opt/mount/"` + +NOTE: The AppImage from Ubuntu 22.04 is broken. Approach with caution + +#### Testing + +1. Build a docker image builds the Chatterino tests + `docker buildx build -t chatterino-ubuntu-22.04-test -f .docker/Dockerfile-ubuntu-22.04-test .` +1. Run the tests + `docker run --rm --network=host chatterino-ubuntu-22.04-test` diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..1d2b1be66 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +build* +.mypy_cache +.cache +.docker diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 000000000..e5ae6a097 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,14 @@ +# If a commit modifies a ton of files and doesn't really contribute to the +# output of git-blame, please add it here +# +# Don't add commits from the same PR you are creating. We squash PRs into a +# single commit, so references to those commits will be lost +# +# 2018 - changed to 80 max column +f71ff08e686ae76c3dd4084d0f05f27ba9b3fdcb +# +# 2018 - added brace wrapping after if and for +e259b9e39f46f3cb0e4838c988d4f320a03dfaa4 +# +# 2019 - Normalize line endings in already existing files +b06eb9df835c25154899fbcf43e9b37addcea1b1 diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 996f0a9d9..20628285f 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -custom: "https://streamelements.com/fourtf/tip" \ No newline at end of file +custom: "https://streamelements.com/fourtf/tip" diff --git a/.github/ISSUE_TEMPLATE/a_make_a_report.yml b/.github/ISSUE_TEMPLATE/a_make_a_report.yml index 07a36f670..73314f8aa 100644 --- a/.github/ISSUE_TEMPLATE/a_make_a_report.yml +++ b/.github/ISSUE_TEMPLATE/a_make_a_report.yml @@ -6,48 +6,47 @@ body: - type: checkboxes id: acknowledgments attributes: - label: Checklist - description: - options: - - label: I'm reporting a problem with Chatterino - required: true - - label: I've verified that I'm running **the most** recent nightly build or stable release - required: true - - label: I've looked for my problem on the [wiki](https://wiki.chatterino.com/Help/) - required: true - - label: I've searched the [issues and pull requests](https://github.com/Chatterino/chatterino2/issues?q=) for similar looking reports - required: true + label: Checklist + description: + options: + - label: I'm reporting a problem with Chatterino + required: true + - label: I've verified that I'm running **the most** recent nightly build or stable release + required: true + - label: I've looked for my problem on the [wiki](https://wiki.chatterino.com/Help/) + required: true + - label: I've searched the [issues and pull requests](https://github.com/Chatterino/chatterino2/issues?q=) for similar looking reports + required: true - type: textarea id: description validations: - required: true + required: true attributes: - label: Describe your issue - description: | - Write a brief description of your issue. - Important: - Focus on the problem instead of a concrete solution. This ensures that the focus of the thread is to resolve your issue. - If you want to voice a concrete idea you can add a comment below after posting the issue. - placeholder: | - Examples: - - I cannot do X. - - I have trouble doing X. - - Feature X has stopped working for me. + label: Describe your issue + description: | + Write a brief description of your issue. + Important: + Focus on the problem instead of a concrete solution. This ensures that the focus of the thread is to resolve your issue. + If you want to voice a concrete idea you can add a comment below after posting the issue. + placeholder: | + Examples: + - I cannot do X. + - I have trouble doing X. + - Feature X has stopped working for me. - type: textarea id: screenshots attributes: - label: Screenshots - description: While optional, it's highly encouraged to include screenshots or videos to illustrate what you mean. - placeholder: You can upload them using the text editor's dedicated button. + label: Screenshots + description: While optional, it's highly encouraged to include screenshots or videos to illustrate what you mean. + placeholder: You can upload them using the text editor's dedicated button. - type: input id: versions validations: - required: true + required: true attributes: - label: OS and Chatterino Version - description: The name of your Operating System and the version shown in Chatterino's about settings page (⚙ -> about tab). - placeholder: Chatterino 2.3.5 (commit 81a62764, 2022-04-05) on Windows 10 Version 2009, kernel 10.0.19043 - + label: OS and Chatterino Version + description: The name of your Operating System and the version shown in Chatterino's about settings page (⚙ -> about tab). + placeholder: Chatterino 2.3.5 (commit 81a62764, 2022-04-05) on Windows 10 Version 2009, kernel 10.0.19043 diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 47dbbb2a4..2f0ec0fae 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,11 +1,11 @@ blank_issues_enabled: false contact_links: - - name: Issue about the Chatterino Browser Extension - url: https://github.com/Chatterino/chatterino-browser-ext/issues - about: Make a suggestion or report a bug about the Chatterino browser extension. - - name: Suggestions or feature request - url: https://github.com/chatterino/chatterino2/discussions/categories/ideas - about: Got something you think should change or be added? Search for or start a new discussion! - - name: Help - url: https://github.com/chatterino/chatterino2/discussions/categories/q-a - about: Chatterino2 not working as you'd expect? Not sure it's a bug? Check the Q&A section! + - name: Issue about the Chatterino Browser Extension + url: https://github.com/Chatterino/chatterino-browser-ext/issues + about: Make a suggestion or report a bug about the Chatterino browser extension. + - name: Suggestions or feature request + url: https://github.com/chatterino/chatterino2/discussions/categories/ideas + about: Got something you think should change or be added? Search for or start a new discussion! + - name: Help + url: https://github.com/chatterino/chatterino2/discussions/categories/q-a + about: Chatterino2 not working as you'd expect? Not sure it's a bug? Check the Q&A section! diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index d42cd3164..2c20ec100 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,7 +1,5 @@ -Pull request checklist: - -- [ ] `CHANGELOG.md` was updated, if applicable - -# Description - - + diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a5706046b..d01e87617 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,305 +5,435 @@ on: push: branches: - master + - "bugfix-release/*" + - "release/*" pull_request: workflow_dispatch: + merge_group: -concurrency: +concurrency: group: build-${{ github.ref }} cancel-in-progress: true +env: + C2_ENABLE_LTO: ${{ github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/bugfix-release/') || startsWith(github.ref, 'refs/heads/release/') }} + CHATTERINO_REQUIRE_CLEAN_GIT: On + C2_BUILD_WITH_QT6: Off + # Last known good conan version + # 2.0.3 has a bug on Windows (conan-io/conan#13606) + CONAN_VERSION: 2.0.2 + jobs: + build-ubuntu-docker: + name: "Build Ubuntu in Docker" + runs-on: ubuntu-latest + container: ${{ matrix.container }} + strategy: + matrix: + include: + - os: ubuntu-20.04 + container: ghcr.io/chatterino/chatterino2-build-ubuntu-20.04:latest + qt-version: 6.7.2 + force-lto: false + plugins: true + skip-artifact: false + skip-crashpad: false + build-appimage: false + build-deb: true + - os: ubuntu-22.04 + container: ghcr.io/chatterino/chatterino2-build-ubuntu-22.04:latest + qt-version: 6.7.2 + force-lto: false + plugins: true + skip-artifact: false + skip-crashpad: false + build-appimage: true + build-deb: true + - os: ubuntu-24.04 + container: ghcr.io/chatterino/chatterino2-build-ubuntu-24.04:latest + qt-version: 6.7.2 + force-lto: false + plugins: true + skip-artifact: false + skip-crashpad: false + build-appimage: false + build-deb: true + env: + C2_ENABLE_LTO: ${{ matrix.force-lto }} + C2_PLUGINS: ${{ matrix.plugins }} + C2_ENABLE_CRASHPAD: ${{ matrix.skip-crashpad == false }} + C2_BUILD_WITH_QT6: ${{ startsWith(matrix.qt-version, '6.') }} + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + fetch-depth: 0 # allows for tags access + + - name: Fix git permission error + run: | + git config --global --add safe.directory '*' + + - name: Build + run: | + mkdir build + cd build + CXXFLAGS=-fno-sized-deallocation cmake \ + -DCMAKE_INSTALL_PREFIX=appdir/usr/ \ + -DCMAKE_BUILD_TYPE=Release \ + -DPAJLADA_SETTINGS_USE_BOOST_FILESYSTEM=On \ + -DUSE_PRECOMPILED_HEADERS=OFF \ + -DCMAKE_EXPORT_COMPILE_COMMANDS=On \ + -DCHATTERINO_LTO="$C2_ENABLE_LTO" \ + -DCHATTERINO_PLUGINS="$C2_PLUGINS" \ + -DCMAKE_PREFIX_PATH="$Qt6_DIR/lib/cmake" \ + -DBUILD_WITH_QT6="$C2_BUILD_WITH_QT6" \ + -DCHATTERINO_STATIC_QT_BUILD=On \ + .. + make -j"$(nproc)" + + - name: Package - AppImage (Ubuntu) + if: matrix.build-appimage + run: | + cd build + sh ./../.CI/CreateAppImage.sh + + - name: Upload artifact - AppImage (Ubuntu) + if: matrix.build-appimage + uses: actions/upload-artifact@v4 + with: + name: Chatterino-x86_64-Qt-${{ matrix.qt-version }}.AppImage + path: build/Chatterino-x86_64.AppImage + + - name: Package - .deb (Ubuntu) + if: matrix.build-deb + run: | + cd build + sh ./../.CI/CreateUbuntuDeb.sh + + - name: Upload artifact - .deb (Ubuntu) + if: matrix.build-deb + uses: actions/upload-artifact@v4 + with: + name: Chatterino-${{ matrix.os }}-Qt-${{ matrix.qt-version }}.deb + path: build/Chatterino-${{ matrix.os }}-x86_64.deb + build: + name: "Build ${{ matrix.os }}, Qt ${{ matrix.qt-version }} (LTO:${{ matrix.force-lto }}, crashpad:${{ matrix.skip-crashpad && 'off' || 'on' }})" runs-on: ${{ matrix.os }} strategy: matrix: - os: [windows-latest, ubuntu-latest, macos-latest] - qt-version: [5.15.2, 5.12.12] - pch: [true] include: - - os: ubuntu-latest + # macOS + - os: macos-13 qt-version: 5.15.2 - pch: false + force-lto: false + plugins: true + skip-artifact: false + skip-crashpad: false + # Windows + - os: windows-latest + qt-version: 6.7.1 + force-lto: false + plugins: true + skip-artifact: false + skip-crashpad: false + # Windows 7/8 + - os: windows-latest + qt-version: 5.15.2 + force-lto: false + plugins: true + skip-artifact: false + skip-crashpad: true + fail-fast: false + env: + C2_ENABLE_LTO: ${{ matrix.force-lto }} + C2_PLUGINS: ${{ matrix.plugins }} + C2_ENABLE_CRASHPAD: ${{ matrix.skip-crashpad == false }} + C2_BUILD_WITH_QT6: ${{ startsWith(matrix.qt-version, '6.') }} + C2_USE_OPENSSL3: ${{ startsWith(matrix.qt-version, '6.') && 'True' || 'False' }} + C2_CONAN_CACHE_SUFFIX: ${{ startsWith(matrix.qt-version, '6.') && '-QT6' || '' }} steps: - - name: Set environment variables for windows-latest - if: matrix.os == 'windows-latest' - run: | - echo "vs_version=2022" >> $GITHUB_ENV - shell: bash - - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: - submodules: true - fetch-depth: 0 # allows for tags access + submodules: recursive + fetch-depth: 0 # allows for tags access - - name: Cache Qt - id: cache-qt - uses: actions/cache@v3 + - name: Install Qt5 + if: startsWith(matrix.qt-version, '5.') + uses: jurplel/install-qt-action@v4.0.0 with: - path: "${{ github.workspace }}/qt/" - key: ${{ runner.os }}-QtCache-${{ matrix.qt-version }} - - # LINUX - - name: Install Qt - uses: jurplel/install-qt-action@v2 - with: - cached: ${{ steps.cache-qt.outputs.cache-hit }} + cache: true + cache-key-prefix: ${{ runner.os }}-QtCache-${{ matrix.qt-version }}-v2 + version: ${{ matrix.qt-version }} + + - name: Install Qt6 + if: startsWith(matrix.qt-version, '6.') + uses: jurplel/install-qt-action@v4.0.0 + with: + cache: true + cache-key-prefix: ${{ runner.os }}-QtCache-${{ matrix.qt-version }}-v2 + modules: qt5compat qtimageformats version: ${{ matrix.qt-version }} - dir: "${{ github.workspace }}/qt/" # WINDOWS - - name: Cache conan packages part 1 + - name: Enable Developer Command Prompt (Windows) if: startsWith(matrix.os, 'windows') - uses: actions/cache@v3 - with: - key: ${{ runner.os }}-conan-user-${{ hashFiles('**/conanfile.txt') }} - path: ~/.conan/ + uses: ilammy/msvc-dev-cmd@v1.13.0 - - name: Cache conan packages part 2 + - name: Setup sccache (Windows) + # sccache v0.7.4 + uses: hendrikmuhs/ccache-action@v1.2.14 if: startsWith(matrix.os, 'windows') - uses: actions/cache@v3 with: - key: ${{ runner.os }}-conan-root-${{ hashFiles('**/conanfile.txt') }} - path: C:/.conan/ + variant: sccache + # only save on on the default (master) branch + save: ${{ github.event_name == 'push' }} + key: sccache-build-${{ matrix.os }}-${{ matrix.qt-version }}-${{ matrix.skip-crashpad }} + restore-keys: | + sccache-build-${{ matrix.os }}-${{ matrix.qt-version }} - - name: Add Conan to path + - name: Cache conan packages (Windows) if: startsWith(matrix.os, 'windows') - run: echo "C:\Program Files\Conan\conan\" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - + uses: actions/cache@v4 + with: + key: ${{ runner.os }}-conan-user-${{ hashFiles('**/conanfile.py') }}${{ env.C2_CONAN_CACHE_SUFFIX }} + path: ~/.conan2/ + + - name: Install Conan (Windows) + if: startsWith(matrix.os, 'windows') + run: | + python3 -c "import site; import sys; print(f'{site.USER_BASE}\\Python{sys.version_info.major}{sys.version_info.minor}\\Scripts')" >> "$GITHUB_PATH" + pip3 install --user "conan==${{ env.CONAN_VERSION }}" + shell: powershell + + - name: Setup Conan (Windows) + if: startsWith(matrix.os, 'windows') + run: | + conan --version + conan profile detect -f + shell: powershell + - name: Install dependencies (Windows) if: startsWith(matrix.os, 'windows') run: | - choco install conan -y - - - name: Enable Developer Command Prompt - if: startsWith(matrix.os, 'windows') - uses: ilammy/msvc-dev-cmd@v1.10.0 + mkdir build + cd build + conan install .. ` + -s build_type=RelWithDebInfo ` + -c tools.cmake.cmaketoolchain:generator="NMake Makefiles" ` + -b missing ` + --output-folder=. ` + -o with_openssl3="$Env:C2_USE_OPENSSL3" + shell: powershell - name: Build (Windows) if: startsWith(matrix.os, 'windows') + shell: pwsh run: | - mkdir build - cd build - conan install .. -b missing - cmake -G"NMake Makefiles" -DCMAKE_BUILD_TYPE=Release -DUSE_CONAN=ON .. - set cl=/MP - nmake /S /NOLOGO - windeployqt bin/chatterino.exe --release --no-compiler-runtime --no-translations --no-opengl-sw --dir Chatterino2/ - cp bin/chatterino.exe Chatterino2/ - echo nightly > Chatterino2/modes - 7z a chatterino-windows-x86-64.zip Chatterino2/ - - - name: Upload artifact (Windows) - if: startsWith(matrix.os, 'windows') - uses: actions/upload-artifact@v3 - with: - name: chatterino-windows-x86-64-${{ matrix.qt-version }}.zip - path: build/chatterino-windows-x86-64.zip - - - name: Clean Conan pkgs - if: startsWith(matrix.os, 'windows') - run: conan remove "*" -fsb - shell: bash - - # LINUX - - name: Install dependencies (Ubuntu) - if: startsWith(matrix.os, 'ubuntu') - run: | - sudo apt-get update - sudo apt-get -y install \ - cmake \ - virtualenv \ - rapidjson-dev \ - libssl-dev \ - libboost-dev \ - libxcb-randr0-dev \ - libboost-system-dev \ - libboost-filesystem-dev \ - libpulse-dev \ - libxkbcommon-x11-0 \ - libgstreamer-plugins-base1.0-0 \ - build-essential \ - libgl1-mesa-dev \ - libxcb-icccm4 \ - libxcb-image0 \ - libxcb-keysyms1 \ - libxcb-render-util0 \ - libxcb-xinerama0 - - - name: Build (Ubuntu) - if: startsWith(matrix.os, 'ubuntu') - run: | - mkdir build - cd build - cmake \ - -DCMAKE_INSTALL_PREFIX=appdir/usr/ \ - -DCMAKE_BUILD_TYPE=Release \ - -DPAJLADA_SETTINGS_USE_BOOST_FILESYSTEM=On \ - -DUSE_PRECOMPILED_HEADERS=${{ matrix.pch }} \ - -DCMAKE_EXPORT_COMPILE_COMMANDS=On \ + cd build + cmake ` + -G"NMake Makefiles" ` + -DCMAKE_BUILD_TYPE=RelWithDebInfo ` + -DCMAKE_TOOLCHAIN_FILE="conan_toolchain.cmake" ` + -DUSE_PRECOMPILED_HEADERS=ON ` + -DBUILD_WITH_CRASHPAD="$Env:C2_ENABLE_CRASHPAD" ` + -DCHATTERINO_LTO="$Env:C2_ENABLE_LTO" ` + -DCHATTERINO_PLUGINS="$Env:C2_PLUGINS" ` + -DBUILD_WITH_QT6="$Env:C2_BUILD_WITH_QT6" ` .. - make -j$(nproc) - shell: bash + set cl=/MP + nmake /S /NOLOGO - - name: clang-tidy review - if: (startsWith(matrix.os, 'ubuntu') && matrix.pch == false && matrix.qt-version == '5.15.2' && github.event_name == 'pull_request') - uses: ZedThree/clang-tidy-review@v0.9.0 - id: review - with: - build_dir: build - config_file: '.clang-tidy' - - - name: Package - AppImage (Ubuntu) - if: startsWith(matrix.os, 'ubuntu') + - name: Build crashpad (Windows) + if: startsWith(matrix.os, 'windows') && !matrix.skip-crashpad + shell: pwsh run: | - cd build - sh ./../.CI/CreateAppImage.sh - shell: bash + cd build + set cl=/MP + nmake /S /NOLOGO chatterino-crash-handler + mkdir Chatterino2/crashpad + cp bin/crashpad/crashpad-handler.exe Chatterino2/crashpad/crashpad-handler.exe + 7z a bin/chatterino-Qt-${{ matrix.qt-version }}.pdb.7z bin/chatterino.pdb - - name: Package - .deb (Ubuntu) - if: startsWith(matrix.os, 'ubuntu') + - name: Prepare build dir (windows) + if: startsWith(matrix.os, 'windows') run: | - cd build - sh ./../.CI/CreateUbuntuDeb.sh + cd build + windeployqt bin/chatterino.exe --release --no-compiler-runtime --no-translations --no-opengl-sw --dir Chatterino2/ + cp bin/chatterino.exe Chatterino2/ + ..\.CI\deploy-crt.ps1 Chatterino2 + echo nightly > Chatterino2/modes + + - name: Package (windows) + if: startsWith(matrix.os, 'windows') + working-directory: build + run: | + 7z a chatterino-windows-x86-64-Qt-${{ matrix.qt-version }}.zip Chatterino2/ + + - name: Upload artifact (Windows - binary) + if: startsWith(matrix.os, 'windows') && !matrix.skip-artifact + uses: actions/upload-artifact@v4 + with: + name: chatterino-windows-x86-64-Qt-${{ matrix.qt-version }}.zip + path: build/chatterino-windows-x86-64-Qt-${{ matrix.qt-version }}.zip + + - name: Upload artifact (Windows - symbols) + if: startsWith(matrix.os, 'windows') && !matrix.skip-artifact + uses: actions/upload-artifact@v4 + with: + name: chatterino-windows-x86-64-Qt-${{ matrix.qt-version }}-symbols.pdb.7z + path: build/bin/chatterino-Qt-${{ matrix.qt-version }}.pdb.7z + + - name: Clean Conan cache + if: startsWith(matrix.os, 'windows') + run: conan cache clean --source --build --download "*" shell: bash - - name: Upload artifact - AppImage (Ubuntu) - if: startsWith(matrix.os, 'ubuntu') - uses: actions/upload-artifact@v3 - with: - name: Chatterino-x86_64-${{ matrix.qt-version }}.AppImage - path: build/Chatterino-x86_64.AppImage - - - name: Upload artifact - .deb (Ubuntu) - if: startsWith(matrix.os, 'ubuntu') - uses: actions/upload-artifact@v3 - with: - name: Chatterino-${{ matrix.qt-version }}.deb - path: build/Chatterino.deb - # MACOS - name: Install dependencies (MacOS) if: startsWith(matrix.os, 'macos') run: | - brew install boost openssl rapidjson p7zip create-dmg cmake tree + brew install boost openssl rapidjson p7zip create-dmg cmake tree shell: bash - name: Build (MacOS) if: startsWith(matrix.os, 'macos') run: | - mkdir build - cd build - cmake \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 \ - -DOPENSSL_ROOT_DIR=/usr/local/opt/openssl \ - -DUSE_PRECOMPILED_HEADERS=${{ matrix.pch }} \ - .. - make -j$(sysctl -n hw.logicalcpu) + mkdir build + cd build + cmake \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 \ + -DOPENSSL_ROOT_DIR=/usr/local/opt/openssl \ + -DUSE_PRECOMPILED_HEADERS=OFF \ + -DCHATTERINO_LTO="$C2_ENABLE_LTO" \ + -DCHATTERINO_PLUGINS="$C2_PLUGINS" \ + -DBUILD_WITH_QT6="$C2_BUILD_WITH_QT6" \ + .. + make -j"$(sysctl -n hw.logicalcpu)" shell: bash - name: Package (MacOS) if: startsWith(matrix.os, 'macos') + env: + OUTPUT_DMG_PATH: chatterino-macos-Qt-${{ matrix.qt-version}}.dmg run: | - ls -la - pwd - ls -la build || true - cd build - sh ./../.CI/CreateDMG.sh + ls -la + pwd + ls -la build || true + cd build + ./../.CI/MacDeploy.sh + ./../.CI/CreateDMG.sh shell: bash - name: Upload artifact (MacOS) if: startsWith(matrix.os, 'macos') - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: chatterino-osx-${{ matrix.qt-version }}.dmg - path: build/chatterino-osx.dmg + name: chatterino-macos-Qt-${{ matrix.qt-version }}.dmg + path: build/chatterino-macos-Qt-${{ matrix.qt-version }}.dmg create-release: - needs: build + needs: [build-ubuntu-docker, build] runs-on: ubuntu-latest if: (github.event_name == 'push' && github.ref == 'refs/heads/master') steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # allows for tags access + + # Windows + - uses: actions/download-artifact@v4 + name: Windows Qt6.7.1 + with: + name: chatterino-windows-x86-64-Qt-6.7.1.zip + path: release-artifacts/ + + - uses: actions/download-artifact@v4 + name: Windows Qt6.7.1 symbols + with: + name: chatterino-windows-x86-64-Qt-6.7.1-symbols.pdb.7z + path: release-artifacts/ + + - uses: actions/download-artifact@v4 + name: Windows Qt5.15.2 + with: + name: chatterino-windows-x86-64-Qt-5.15.2.zip + path: release-artifacts/ + + # Linux + - uses: actions/download-artifact@v4 + name: Linux AppImage + with: + name: Chatterino-x86_64-Qt-6.7.2.AppImage + path: release-artifacts/ + + - uses: actions/download-artifact@v4 + name: Ubuntu 20.04 deb + with: + name: Chatterino-ubuntu-20.04-Qt-6.7.2.deb + path: release-artifacts/ + + - uses: actions/download-artifact@v4 + name: Ubuntu 22.04 deb + with: + name: Chatterino-ubuntu-22.04-Qt-6.7.2.deb + path: release-artifacts/ + + - uses: actions/download-artifact@v4 + name: Ubuntu 24.04 deb + with: + name: Chatterino-ubuntu-24.04-Qt-6.7.2.deb + path: release-artifacts/ + + - name: Copy flatpakref + run: | + cp .CI/chatterino-nightly.flatpakref release-artifacts/ + shell: bash + + - name: Rename artifacts + run: | + ls -l + + # Mark all Windows Qt5 builds as old + mv chatterino-windows-x86-64-Qt-5.15.2.zip chatterino-windows-old-x86-64-Qt-5.15.2.zip + working-directory: release-artifacts + shell: bash + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Format changes + id: format-changes + run: | + delimiter=$(openssl rand -hex 32) + { + echo "changelog<<$delimiter" + python3 ./.CI/format-recent-changes.py + echo $delimiter + } >> "$GITHUB_OUTPUT" + shell: bash + - name: Create release - id: create_release - uses: pajlada/create-release@v2.0.3 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: ncipollo/release-action@v1.14.0 with: - tag_name: nightly-build - backup_tag_name: backup-nightly-build - release_name: Nightly Release - body: | - Nightly Build + replacesArtifacts: true + allowUpdates: true + artifactErrorsFailBuild: true + artifacts: "release-artifacts/*" + body: ${{ steps.format-changes.outputs.changelog }} prerelease: true + name: Nightly Release + tag: nightly-build - - uses: actions/download-artifact@v3 - with: - name: chatterino-windows-x86-64-5.15.2.zip - path: windows/ - - - uses: actions/download-artifact@v3 - with: - name: Chatterino-x86_64-5.15.2.AppImage - path: linux/ - - - uses: actions/download-artifact@v3 - with: - name: Chatterino-5.15.2.deb - path: ubuntu/ - - - uses: actions/download-artifact@v3 - with: - name: chatterino-osx-5.15.2.dmg - path: macos/ - - # TODO: Extract dmg and appimage - - # - name: Read upload URL into output - # id: upload_url - # run: | - # echo "::set-output name=upload_url::$(cat release-upload-url.txt/release-upload-url.txt)" - - - name: Upload release asset (Windows) - uses: actions/upload-release-asset@v1.0.2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./windows/chatterino-windows-x86-64.zip - asset_name: chatterino-windows-x86-64.zip - asset_content_type: application/zip - - - name: Upload release asset (Ubuntu) - uses: actions/upload-release-asset@v1.0.2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./linux/Chatterino-x86_64.AppImage - asset_name: Chatterino-x86_64.AppImage - asset_content_type: application/x-executable - - - name: Upload release asset (Ubuntu .deb) - uses: actions/upload-release-asset@v1.0.2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./ubuntu/Chatterino.deb - asset_name: Chatterino-x86_64.deb - asset_content_type: application/vnd.debian.binary-package - - - name: Upload release asset (MacOS) - uses: actions/upload-release-asset@v1.0.2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./macos/chatterino-osx.dmg - asset_name: chatterino-osx.dmg - asset_content_type: application/x-bzip2 - + - name: Update nightly-build tag + run: | + git tag -f nightly-build + git push -f origin nightly-build + shell: bash diff --git a/.github/workflows/changelog-category-check.yml b/.github/workflows/changelog-category-check.yml new file mode 100644 index 000000000..fb4bd9b23 --- /dev/null +++ b/.github/workflows/changelog-category-check.yml @@ -0,0 +1,33 @@ +--- +name: Changelog Category Check + +on: + pull_request: + types: + - labeled + - unlabeled + - opened + - synchronize + - reopened + +jobs: + changelog-category-check: + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v7 + id: label-checker + with: + result-encoding: "string" + script: | + const response = await github.rest.issues.listLabelsOnIssue({ + issue_number: context.payload.pull_request.number, + owner: context.repo.owner, + repo: context.repo.repo + }); + if (new Set(response.data.map(label => label.name)).has("skip-changelog-checker")) { + return "skip"; + } + return ""; + + - uses: pajlads/changelog-checker@v1.0.1 + if: steps.label-checker.outputs.result != 'skip' diff --git a/.github/workflows/changelog-check.yml b/.github/workflows/changelog-check.yml index 70298141c..4681d687c 100644 --- a/.github/workflows/changelog-check.yml +++ b/.github/workflows/changelog-check.yml @@ -2,8 +2,8 @@ name: Changelog Check on: pull_request: - branches: [ master ] - types: [ opened, synchronize, reopened, ready_for_review, labeled, unlabeled ] + branches: [master] + types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled] jobs: check-changelog: @@ -13,5 +13,5 @@ jobs: - name: Changelog check uses: dangoslen/changelog-enforcer@v3 with: - changeLogPath: 'CHANGELOG.md' - skipLabels: 'no changelog entry needed, ci, submodules' + changeLogPath: "CHANGELOG.md" + skipLabels: "no changelog entry needed, ci, submodules" diff --git a/.github/workflows/check-formatting.yml b/.github/workflows/check-formatting.yml index 67cfa762d..14d3e81bd 100644 --- a/.github/workflows/check-formatting.yml +++ b/.github/workflows/check-formatting.yml @@ -6,26 +6,31 @@ on: branches: - master pull_request: + merge_group: -concurrency: +concurrency: group: check-formatting-${{ github.ref }} cancel-in-progress: true jobs: check: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: apt-get update run: sudo apt-get update - name: Install clang-format - run: sudo apt-get -y install clang-format dos2unix + run: sudo apt-get -y install dos2unix - name: Check formatting - run: ./tools/check-format.sh + uses: DoozyX/clang-format-lint-action@v0.18 + with: + source: "./src ./tests/src ./benchmarks/src ./mocks/include" + extensions: "hpp,cpp" + clangFormatVersion: 16 - name: Check line-endings - run: ./tools/check-line-endings.sh + run: ./scripts/check-line-endings.sh diff --git a/.github/workflows/clang-tidy.yml b/.github/workflows/clang-tidy.yml new file mode 100644 index 000000000..19052648b --- /dev/null +++ b/.github/workflows/clang-tidy.yml @@ -0,0 +1,65 @@ +--- +name: clang-tidy + +on: + pull_request: + +concurrency: + group: clang-tidy-${{ github.ref }} + cancel-in-progress: true + +jobs: + review: + name: "clang-tidy ${{ matrix.os }}, Qt ${{ matrix.qt-version }})" + runs-on: ${{ matrix.os }} + strategy: + matrix: + include: + # Ubuntu 22.04, Qt 6.6 + - os: ubuntu-22.04 + qt-version: 6.6.2 + + fail-fast: false + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + fetch-depth: 0 # allows for tags access + + - name: Install Qt6 + if: startsWith(matrix.qt-version, '6.') + uses: jurplel/install-qt-action@v4.0.0 + with: + cache: true + cache-key-prefix: ${{ runner.os }}-QtCache-${{ matrix.qt-version }}-v2 + modules: qt5compat qtimageformats + version: ${{ matrix.qt-version }} + dir: ${{ github.workspace }}/.qtinstall + set-env: false + + - name: clang-tidy review + timeout-minutes: 20 + uses: ZedThree/clang-tidy-review@v0.19.0 + with: + build_dir: build-clang-tidy + config_file: ".clang-tidy" + split_workflow: true + exclude: "lib/*,tools/crash-handler/*" + cmake_command: >- + ./.CI/setup-clang-tidy.sh + apt_packages: >- + libsecret-1-dev, + libboost-dev, libboost-system-dev, libboost-filesystem-dev, + libssl-dev, + rapidjson-dev, + libbenchmark-dev, + build-essential, + libgl1-mesa-dev, libgstreamer-gl1.0-0, libpulse-dev, + libxcb-glx0, libxcb-icccm4, libxcb-image0, libxcb-keysyms1, libxcb-randr0, + libxcb-render-util0, libxcb-render0, libxcb-shape0, libxcb-shm0, libxcb-sync1, + libxcb-util1, libxcb-xfixes0, libxcb-xinerama0, libxcb1, libxkbcommon-dev, + libxkbcommon-x11-0, libxcb-xkb-dev, libxcb-cursor0 + + - name: clang-tidy-review upload + uses: ZedThree/clang-tidy-review/upload@v0.19.0 diff --git a/.github/workflows/create-installer.yml b/.github/workflows/create-installer.yml new file mode 100644 index 000000000..5700e3f16 --- /dev/null +++ b/.github/workflows/create-installer.yml @@ -0,0 +1,58 @@ +name: Create installer + +on: + workflow_run: + workflows: ["Build"] + types: [completed] + # make sure this only runs on the default branch + branches: + - master + - "bugfix-release/*" + - "release/*" + workflow_dispatch: + +jobs: + create-installer: + runs-on: windows-latest + # Only run manually or when a build succeeds + if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }} + strategy: + matrix: + qt-version: ["6.7.1"] + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # allows for tags access + + - name: Download artifact + uses: dawidd6/action-download-artifact@v6 + with: + workflow: build.yml + name: chatterino-windows-x86-64-Qt-${{ matrix.qt-version }}.zip + commit: ${{ github.sha }} + path: build/ + + - name: Unzip + run: 7z e -spf chatterino-windows-x86-64-Qt-${{ matrix.qt-version }}.zip + working-directory: build + + - name: Install InnoSetup + run: choco install innosetup + + - name: Add InnoSetup to path + run: echo "C:\Program Files (x86)\Inno Setup 6\" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + + - name: Enable Developer Command Prompt + uses: ilammy/msvc-dev-cmd@v1.13.0 + + - name: Build installer + id: build-installer + working-directory: build + run: ..\.CI\build-installer.ps1 + shell: powershell + + - name: Upload installer + uses: actions/upload-artifact@v4 + with: + path: build/${{ steps.build-installer.outputs.C2_INSTALLER_BASE_NAME }}.exe + name: ${{ steps.build-installer.outputs.C2_INSTALLER_BASE_NAME }}.exe diff --git a/.github/workflows/homebrew.yml b/.github/workflows/homebrew.yml index c5498eedd..6da0e71d3 100644 --- a/.github/workflows/homebrew.yml +++ b/.github/workflows/homebrew.yml @@ -1,28 +1,30 @@ -name: 'Publish Homebrew Cask on Release' +name: "Publish Homebrew Cask on Release" on: push: tags: # Should match semver for mainline releases (not including -beta) - - 'v2.[0-9]+.[0-9]+' + - "v2.[0-9]+.[0-9]+" # TODO: handle beta and nightly releases # Need to make those casks manually first # - v2.[0-9]+.[0-9]+-beta(?:[0-9]+) env: # This gets updated later on in the run by a bash script to strip the prefix - C2_CASK_NAME: 'chatterino' - C2_TAGGED_VERSION: '${{ github.ref }}' + C2_CASK_NAME: chatterino + # The full version of Chatterino (e.g. v2.4.1) + C2_TAGGED_VERSION: ${{ github.ref_name }} + HOMEBREW_GITHUB_API_TOKEN: ${{ secrets.HOMEBREW_GITHUB_API_TOKEN }} jobs: update_stable_homebrew_cask: - name: 'Update the stable homebrew cask' - runs-on: 'macos-latest' + name: "Update the stable homebrew cask" + runs-on: "macos-latest" steps: # Pulls out the version from the ref (e.g. refs/tags/v2.3.1 -> 2.3.1) - - name: 'Extract version from tag' + - name: Execute brew bump-cask-pr with version run: | - 'STRIPPED_VERSION=$(echo "refs/tags/$C2_TAGGED_VERSION" | sed "s/refs\/tags\/v//gm")' - 'echo "C2_TAGGED_VERSION=$STRIPPED_VERSION" >> $GITHUB_ENV' - - name: 'Execute ''brew bump-cask-pr'' with version' - run: 'brew bump-cask-pr --version $C2_TAGGED_VERSION $C2_CASK_NAME' - + echo "Running bump-cask-pr for cask '$C2_CASK_NAME' and version '$C2_TAGGED_VERSION'" + C2_TAGGED_VERSION_STRIPPED="${C2_TAGGED_VERSION:1}" + echo "Stripped version: '$C2_TAGGED_VERSION_STRIPPED'" + brew developer on + brew bump-cask-pr --version "$C2_TAGGED_VERSION_STRIPPED" "$C2_CASK_NAME" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index af82fdc0c..c26015e4e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,8 +6,9 @@ on: branches: - master pull_request: + merge_group: -concurrency: +concurrency: group: lint-${{ github.ref }} cancel-in-progress: true @@ -16,10 +17,17 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - name: Lint Markdown files - uses: actionsx/prettier@v2 + - name: Check formatting with Prettier + uses: actionsx/prettier@3d9f7c3fa44c9cb819e68292a328d7f4384be206 with: # prettier CLI arguments. - args: --check '**/*.md' + args: --write . + - name: Show diff + run: git --no-pager diff --exit-code --color=never + shell: bash + - name: Check Theme files + run: | + npm i ajv-cli + npx -- ajv validate -s docs/ChatterinoTheme.schema.json -d "resources/themes/*.json" diff --git a/.github/workflows/post-clang-tidy-review.yml b/.github/workflows/post-clang-tidy-review.yml new file mode 100644 index 000000000..c55b3a3d9 --- /dev/null +++ b/.github/workflows/post-clang-tidy-review.yml @@ -0,0 +1,20 @@ +--- +name: Post clang-tidy review comments + +on: + workflow_run: + workflows: ["clang-tidy"] + types: + - completed + +jobs: + post: + runs-on: ubuntu-latest + # Only when a build succeeds + if: ${{ github.event.workflow_run.conclusion == 'success' }} + + steps: + - uses: ZedThree/clang-tidy-review/post@v0.19.0 + with: + lgtm_comment_body: "" + num_comments_as_exitcode: false diff --git a/.github/workflows/push-aur.yml b/.github/workflows/push-aur.yml deleted file mode 100644 index a16f3c91a..000000000 --- a/.github/workflows/push-aur.yml +++ /dev/null @@ -1,26 +0,0 @@ ---- -name: Build on Arch Linux - -on: - push: - branches: - - master - pull_request: - -concurrency: - group: build-archlinux-${{ github.ref }} - cancel-in-progress: true - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - name: Sync AUR package with current version - uses: pajlada/aur-sync-action@master - with: - package_name: chatterino2-git - commit_username: chatterino2-ci - commit_email: chatterino2-ci@pajlada.com - ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} - dry_run: true diff --git a/.github/workflows/test-macos.yml b/.github/workflows/test-macos.yml new file mode 100644 index 000000000..9e76429fe --- /dev/null +++ b/.github/workflows/test-macos.yml @@ -0,0 +1,98 @@ +--- +name: Test MacOS + +on: + pull_request: + workflow_dispatch: + merge_group: + +env: + TWITCH_PUBSUB_SERVER_TAG: v1.0.7 + QT_QPA_PLATFORM: minimal + HOMEBREW_NO_AUTO_UPDATE: 1 + HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1 + +concurrency: + group: test-macos-${{ github.ref }} + cancel-in-progress: true + +jobs: + test-macos: + name: "Test ${{ matrix.os }}, Qt ${{ matrix.qt-version }}" + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [macos-13] + qt-version: [5.15.2, 6.7.1] + plugins: [false] + fail-fast: false + env: + C2_BUILD_WITH_QT6: ${{ startsWith(matrix.qt-version, '6.') && 'ON' || 'OFF' }} + QT_MODULES: ${{ startsWith(matrix.qt-version, '6.') && 'qt5compat qtimageformats' || '' }} + + steps: + - name: Enable plugin support + if: matrix.plugins + run: | + echo "C2_PLUGINS=ON" >> "$GITHUB_ENV" + + - name: Set BUILD_WITH_QT6 + if: startsWith(matrix.qt-version, '6.') + run: | + echo "C2_BUILD_WITH_QT6=ON" >> "$GITHUB_ENV" + + - uses: actions/checkout@v4 + with: + submodules: recursive + fetch-depth: 0 # allows for tags access + + - name: Install Qt + uses: jurplel/install-qt-action@v4.0.0 + with: + cache: true + cache-key-prefix: ${{ runner.os }}-QtCache-${{ matrix.qt-version }}-v2 + modules: ${{ env.QT_MODULES }} + version: ${{ matrix.qt-version }} + + - name: Install dependencies + run: | + brew install boost openssl rapidjson p7zip create-dmg cmake + + - name: Build + run: | + mkdir build-test + cd build-test + cmake \ + -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DBUILD_TESTS=On \ + -DBUILD_APP=OFF \ + -DUSE_PRECOMPILED_HEADERS=OFF \ + -DCHATTERINO_PLUGINS="$C2_PLUGINS" \ + -DBUILD_WITH_QT6="$C2_BUILD_WITH_QT6" \ + .. + make -j"$(sysctl -n hw.logicalcpu)" + + - name: Download and extract Twitch PubSub Server Test + run: | + mkdir pubsub-server-test + curl -L -o pubsub-server.tar.gz "https://github.com/Chatterino/twitch-pubsub-server-test/releases/download/${{ env.TWITCH_PUBSUB_SERVER_TAG }}/server-${{ env.TWITCH_PUBSUB_SERVER_TAG }}-darwin-amd64.tar.gz" + tar -xzf pubsub-server.tar.gz -C pubsub-server-test + rm pubsub-server.tar.gz + cd pubsub-server-test + curl -L -o server.crt "https://github.com/Chatterino/twitch-pubsub-server-test/raw/${{ env.TWITCH_PUBSUB_SERVER_TAG }}/cmd/server/server.crt" + curl -L -o server.key "https://github.com/Chatterino/twitch-pubsub-server-test/raw/${{ env.TWITCH_PUBSUB_SERVER_TAG }}/cmd/server/server.key" + cd .. + + - name: Cargo Install httpbox + run: | + cargo install --git https://github.com/kevinastone/httpbox --rev 89b971f + + - name: Test + timeout-minutes: 30 + run: | + httpbox --port 9051 & + cd ../pubsub-server-test + ./server 127.0.0.1:9050 & + cd ../build-test + ctest --repeat until-pass:4 --output-on-failure --exclude-regex ClassicEmoteNameFiltering + working-directory: build-test diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml new file mode 100644 index 000000000..7bdd43494 --- /dev/null +++ b/.github/workflows/test-windows.yml @@ -0,0 +1,157 @@ +--- +name: Test Windows + +on: + pull_request: + workflow_dispatch: + merge_group: + +env: + TWITCH_PUBSUB_SERVER_TAG: v1.0.7 + QT_QPA_PLATFORM: minimal + # Last known good conan version + # 2.0.3 has a bug on Windows (conan-io/conan#13606) + CONAN_VERSION: 2.0.2 + +concurrency: + group: test-windows-${{ github.ref }} + cancel-in-progress: true + +jobs: + test-windows: + name: "Test ${{ matrix.os }}, Qt ${{ matrix.qt-version }}" + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [windows-latest] + qt-version: [5.15.2, 6.7.1] + plugins: [false] + skip-artifact: [false] + skip-crashpad: [false] + fail-fast: false + env: + C2_BUILD_WITH_QT6: ${{ startsWith(matrix.qt-version, '6.') && 'ON' || 'OFF' }} + QT_MODULES: ${{ startsWith(matrix.qt-version, '6.') && 'qt5compat qtimageformats' || '' }} + C2_USE_OPENSSL3: ${{ startsWith(matrix.qt-version, '6.') && 'True' || 'False' }} + C2_CONAN_CACHE_SUFFIX: ${{ startsWith(matrix.qt-version, '6.') && '-QT6' || '' }} + + steps: + - name: Enable plugin support + if: matrix.plugins + run: | + echo "C2_PLUGINS=ON" >> "$Env:GITHUB_ENV" + + - name: Set Crashpad + if: matrix.skip-crashpad == false + run: | + echo "C2_ENABLE_CRASHPAD=ON" >> "$Env:GITHUB_ENV" + + - name: Set BUILD_WITH_QT6 + if: startsWith(matrix.qt-version, '6.') + run: | + echo "C2_BUILD_WITH_QT6=ON" >> "$Env:GITHUB_ENV" + + - uses: actions/checkout@v4 + with: + submodules: recursive + fetch-depth: 0 # allows for tags access + + - name: Install Qt + uses: jurplel/install-qt-action@v4.0.0 + with: + cache: true + cache-key-prefix: ${{ runner.os }}-QtCache-${{ matrix.qt-version }}-v2 + modules: ${{ env.QT_MODULES }} + version: ${{ matrix.qt-version }} + + - name: Enable Developer Command Prompt + uses: ilammy/msvc-dev-cmd@v1.13.0 + + - name: Setup sccache + # sccache v0.7.4 + uses: hendrikmuhs/ccache-action@v1.2.14 + with: + variant: sccache + # only save on the default (master) branch + save: ${{ github.event_name == 'push' }} + key: sccache-test-${{ matrix.os }}-${{ matrix.qt-version }}-${{ matrix.skip-crashpad }} + restore-keys: | + sccache-test-${{ matrix.os }}-${{ matrix.qt-version }} + + - name: Cache conan packages + uses: actions/cache@v4 + with: + key: ${{ runner.os }}-conan-user-${{ hashFiles('**/conanfile.py') }}${{ env.C2_CONAN_CACHE_SUFFIX }} + path: ~/.conan2/ + + - name: Install Conan + run: | + python3 -c "import site; import sys; print(f'{site.USER_BASE}\\Python{sys.version_info.major}{sys.version_info.minor}\\Scripts')" >> "$Env:GITHUB_PATH" + pip3 install --user "conan==${{ env.CONAN_VERSION }}" + + - name: Setup Conan + run: | + conan --version + conan profile detect -f + + - name: Install dependencies + run: | + mkdir build-test + cd build-test + conan install .. ` + -s build_type=RelWithDebInfo ` + -c tools.cmake.cmaketoolchain:generator="NMake Makefiles" ` + -b missing ` + --output-folder=. ` + -o with_openssl3="$Env:C2_USE_OPENSSL3" + + # The Windows runners currently use an older version of the CRT + - name: Install CRT + run: | + mkdir -Force build-test/bin + cp "$((ls $Env:VCToolsRedistDir/onecore/x64 -Filter '*.CRT')[0].FullName)/*" build-test/bin + + - name: Build + run: | + cmake ` + -G"NMake Makefiles" ` + -DCMAKE_BUILD_TYPE=RelWithDebInfo ` + -DBUILD_TESTS=On ` + -DBUILD_APP=OFF ` + -DCMAKE_TOOLCHAIN_FILE="conan_toolchain.cmake" ` + -DUSE_PRECOMPILED_HEADERS=On ` + -DBUILD_WITH_CRASHPAD="$Env:C2_ENABLE_CRASHPAD" ` + -DCHATTERINO_PLUGINS="$Env:C2_PLUGINS" ` + -DBUILD_WITH_QT6="$Env:C2_BUILD_WITH_QT6" ` + .. + set cl=/MP + nmake /S /NOLOGO + working-directory: build-test + + - name: Download and extract Twitch PubSub Server Test + run: | + mkdir pubsub-server-test + Invoke-WebRequest -Uri "https://github.com/Chatterino/twitch-pubsub-server-test/releases/download/${{ env.TWITCH_PUBSUB_SERVER_TAG }}/server-${{ env.TWITCH_PUBSUB_SERVER_TAG }}-windows-amd64.zip" -outfile "pubsub-server.zip" + Expand-Archive pubsub-server.zip -DestinationPath pubsub-server-test + rm pubsub-server.zip + cd pubsub-server-test + Invoke-WebRequest -Uri "https://github.com/Chatterino/twitch-pubsub-server-test/raw/${{ env.TWITCH_PUBSUB_SERVER_TAG }}/cmd/server/server.crt" -outfile "server.crt" + Invoke-WebRequest -Uri "https://github.com/Chatterino/twitch-pubsub-server-test/raw/${{ env.TWITCH_PUBSUB_SERVER_TAG }}/cmd/server/server.key" -outfile "server.key" + cd .. + + - name: Cargo Install httpbox + run: | + cargo install --git https://github.com/kevinastone/httpbox --rev 89b971f + + - name: Test + timeout-minutes: 30 + run: | + httpbox --port 9051 & + cd ..\pubsub-server-test + .\server.exe 127.0.0.1:9050 & + cd ..\build-test + ctest --repeat until-pass:4 --output-on-failure --exclude-regex ClassicEmoteNameFiltering + working-directory: build-test + + - name: Clean Conan cache + run: conan cache clean --source --build --download "*" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7da3a0f7b..0451c605f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,90 +1,100 @@ --- -name: Test +name: Test Ubuntu on: pull_request: workflow_dispatch: + merge_group: + push: + branches: + - master + - main env: - TWITCH_PUBSUB_SERVER_IMAGE: ghcr.io/chatterino/twitch-pubsub-server-test:v1.0.3 + TWITCH_PUBSUB_SERVER_TAG: v1.0.7 + QT_QPA_PLATFORM: minimal -concurrency: +concurrency: group: test-${{ github.ref }} cancel-in-progress: true jobs: test: - runs-on: ${{ matrix.os }} + name: "${{ matrix.os }}" + runs-on: ubuntu-latest + container: ${{ matrix.container }} strategy: matrix: - os: [ubuntu-20.04] - qt-version: [5.15.2] + include: + - os: "ubuntu-22.04" + container: ghcr.io/chatterino/chatterino2-build-ubuntu-22.04:latest + qt-version: 6.7.1 + plugins: true fail-fast: false + env: + C2_PLUGINS: ${{ matrix.plugins }} + C2_BUILD_WITH_QT6: ${{ startsWith(matrix.qt-version, '6.') }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive - - name: Cache Qt - id: cache-qt - uses: actions/cache@v3 - with: - path: "${{ github.workspace }}/qt/" - key: ${{ runner.os }}-QtCache-${{ matrix.qt-version }} - - # LINUX - - name: Install Qt - uses: jurplel/install-qt-action@v2 - with: - cached: ${{ steps.cache-qt.outputs.cache-hit }} - version: ${{ matrix.qt-version }} - dir: "${{ github.workspace }}/qt/" - - # LINUX - - name: Install dependencies (Ubuntu) - if: startsWith(matrix.os, 'ubuntu') - run: | - sudo apt-get update - sudo apt-get -y install \ - cmake \ - rapidjson-dev \ - libssl-dev \ - libboost-dev \ - libboost-system-dev \ - libboost-filesystem-dev \ - libpulse-dev \ - libxkbcommon-x11-0 \ - libgstreamer-plugins-base1.0-0 \ - build-essential \ - libgl1-mesa-dev \ - libxcb-icccm4 \ - libxcb-image0 \ - libxcb-keysyms1 \ - libxcb-render-util0 \ - libxcb-xinerama0 - - name: Create build directory (Ubuntu) - if: startsWith(matrix.os, 'ubuntu') + run: mkdir build-test + + - name: Install dependencies run: | - mkdir build-test - shell: bash + sudo apt update + sudo DEBIAN_FRONTEND=noninteractive apt -y --no-install-recommends install \ + libbenchmark-dev gcovr gnupg - name: Build (Ubuntu) - if: startsWith(matrix.os, 'ubuntu') run: | - cmake -DBUILD_TESTS=On -DBUILD_APP=OFF .. - cmake --build . --config Release + cmake \ + -DBUILD_TESTS=On \ + -DBUILD_BENCHMARKS=On \ + -DBUILD_APP=OFF \ + -DCHATTERINO_PLUGINS="$C2_PLUGINS" \ + -DCMAKE_PREFIX_PATH="$Qt6_DIR/lib/cmake" \ + -DBUILD_WITH_QT6="$C2_BUILD_WITH_QT6" \ + -DCHATTERINO_STATIC_QT_BUILD=On \ + -DCHATTERINO_GENERATE_COVERAGE=On \ + -DCMAKE_BUILD_TYPE=Debug \ + .. + cmake --build . working-directory: build-test - shell: bash - - name: Test (Ubuntu) - if: startsWith(matrix.os, 'ubuntu') + - name: Download and extract Twitch PubSub Server Test run: | - docker pull kennethreitz/httpbin - docker pull ${{ env.TWITCH_PUBSUB_SERVER_IMAGE }} - docker run --network=host --detach ${{ env.TWITCH_PUBSUB_SERVER_IMAGE }} - docker run -p 9051:80 --detach kennethreitz/httpbin - ./bin/chatterino-test --platform minimal + mkdir pubsub-server-test + curl -L -o pubsub-server.tar.gz "https://github.com/Chatterino/twitch-pubsub-server-test/releases/download/${{ env.TWITCH_PUBSUB_SERVER_TAG }}/server-${{ env.TWITCH_PUBSUB_SERVER_TAG }}-linux-amd64.tar.gz" + tar -xzf pubsub-server.tar.gz -C pubsub-server-test + rm pubsub-server.tar.gz + cd pubsub-server-test + curl -L -o server.crt "https://github.com/Chatterino/twitch-pubsub-server-test/raw/${{ env.TWITCH_PUBSUB_SERVER_TAG }}/cmd/server/server.crt" + curl -L -o server.key "https://github.com/Chatterino/twitch-pubsub-server-test/raw/${{ env.TWITCH_PUBSUB_SERVER_TAG }}/cmd/server/server.key" + cd .. + + - uses: dtolnay/rust-toolchain@stable + - name: Cargo Install httpbox + run: | + cargo install --git https://github.com/kevinastone/httpbox --rev 89b971f + + - name: Test + timeout-minutes: 30 + run: | + httpbox --port 9051 & + cd ../pubsub-server-test + ./server 127.0.0.1:9050 & + cd ../build-test + ctest --repeat until-pass:4 --output-on-failure --exclude-regex ClassicEmoteNameFiltering working-directory: build-test - shell: bash + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v4.5.0 + with: + token: ${{ secrets.CODECOV_TOKEN }} + plugin: gcov + fail_ci_if_error: true + verbose: true diff --git a/.github/workflows/winget.yml b/.github/workflows/winget.yml new file mode 100644 index 000000000..7e8a5091a --- /dev/null +++ b/.github/workflows/winget.yml @@ -0,0 +1,14 @@ +name: Publish to WinGet +on: + release: + types: [released] +jobs: + publish: + runs-on: windows-latest + if: ${{ startsWith(github.event.release.tag_name, 'v') }} + steps: + - uses: vedantmgoyal2009/winget-releaser@v2 + with: + identifier: ChatterinoTeam.Chatterino + installers-regex: ^Chatterino.Installer.exe$ + token: ${{ secrets.WINGET_TOKEN }} diff --git a/.gitignore b/.gitignore index 00a1d7659..9a55427ca 100644 --- a/.gitignore +++ b/.gitignore @@ -84,6 +84,7 @@ Thumbs.db dependencies .cache .editorconfig +vim.log ### CMake ### CMakeLists.txt.user @@ -111,3 +112,16 @@ Dependencies_* # vcpkg vcpkg_installed/ + +# NatVis files +qt5.natvis +qt6.natvis + +# Autogenerated resource file +resources/resources_autogenerated.qrc + +# Leftovers from running `aqt install` +aqtinstall.log + +# sccache (CI) +.sccache diff --git a/.gitmodules b/.gitmodules index 19a31943c..e58a5bbd4 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,9 +2,6 @@ path = lib/libcommuni url = https://github.com/Chatterino/libcommuni branch = chatterino-cmake -[submodule "lib/qBreakpad"] - path = lib/qBreakpad - url = https://github.com/jiakuan/qBreakpad.git [submodule "lib/WinToast"] path = lib/WinToast url = https://github.com/mohabouje/WinToast.git @@ -35,3 +32,15 @@ [submodule "lib/googletest"] path = lib/googletest url = https://github.com/google/googletest.git +[submodule "lib/miniaudio"] + path = lib/miniaudio + url = https://github.com/mackron/miniaudio.git +[submodule "lib/lua/src"] + path = lib/lua/src + url = https://github.com/lua/lua +[submodule "tools/crash-handler"] + path = tools/crash-handler + url = https://github.com/Chatterino/crash-handler +[submodule "lib/expected-lite"] + path = lib/expected-lite + url = https://github.com/martinmoene/expected-lite diff --git a/.patches/qt5-on-newer-gcc.patch b/.patches/qt5-on-newer-gcc.patch new file mode 100644 index 000000000..2abdc120f --- /dev/null +++ b/.patches/qt5-on-newer-gcc.patch @@ -0,0 +1,20 @@ +This patch ensures Qt 5.15 in particular can build with modern compilers + +See https://bugreports.qt.io/browse/QTBUG-91909 and https://codereview.qt-project.org/c/qt/qtbase/+/339417 +--- + +diff --git a/src/concurrent/qtconcurrentthreadengine.h b/src/concurrent/qtconcurrentthreadengine.h +index cbd8ad04..4cd5b85 100644 +--- a/src/concurrent/qtconcurrentthreadengine.h ++++ b/src/concurrent/qtconcurrentthreadengine.h +@@ -256,8 +256,8 @@ + class ThreadEngineStarter : public ThreadEngineStarterBase + { + public: +- ThreadEngineStarter(ThreadEngine *_threadEngine) +- :ThreadEngineStarterBase(_threadEngine) {} ++ ThreadEngineStarter(ThreadEngine *_threadEngine) ++ : ThreadEngineStarterBase(_threadEngine) {} + + void startBlocking() + { diff --git a/.prettierignore b/.prettierignore index f5fc58936..89270b789 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,6 +1,31 @@ +# JSON resources should not be prettified... +resources/*.json +benchmarks/resources/*.json +tests/resources/*.json +# ...themes should be prettified for readability. +!resources/themes/*.json + # Ignore submodule files lib/*/ conan-pkgs/*/ cmake/sanitizers-cmake/ +tools/crash-handler -.github/ +# Build folders +*build-*/ +[bB]uild +/_build/ + +# Editors +.vscode +.vs +.idea +dependencies +.cache +.editorconfig + +# vcpkg +vcpkg_installed/ + +# Compile commands generated by CMake +compile_commands.json diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 000000000..b73da844a --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +trailingComma: es5 +endOfLine: auto +overrides: + - files: "*.md" + options: + proseWrap: preserve diff --git a/.prettierrc.toml b/.prettierrc.toml deleted file mode 100644 index 004dabdab..000000000 --- a/.prettierrc.toml +++ /dev/null @@ -1,6 +0,0 @@ -trailingComma = "es5" - -[[overrides]] -files = ["*.md"] -[overrides.options] -proseWrap = "preserve" diff --git a/.sanitizers/asan-suppressions b/.sanitizers/asan-suppressions new file mode 100644 index 000000000..8361de184 --- /dev/null +++ b/.sanitizers/asan-suppressions @@ -0,0 +1,2 @@ +# Ignore openssl issues +interceptor_via_lib:libcrypto.so.3 diff --git a/.sanitizers/lsan-suppressions b/.sanitizers/lsan-suppressions new file mode 100644 index 000000000..bd4eb5fbf --- /dev/null +++ b/.sanitizers/lsan-suppressions @@ -0,0 +1,3 @@ +# Ignore openssl issues +leak:libcrypto.so.3 +leak:CRYPTO_zalloc diff --git a/.sanitizers/tsan-suppressions b/.sanitizers/tsan-suppressions new file mode 100644 index 000000000..1f9b93e6f --- /dev/null +++ b/.sanitizers/tsan-suppressions @@ -0,0 +1,17 @@ +race:libdbus-1.so.3 +deadlock:libdbus-1.so.3 +race:libglib-2.0.so.0 +race:libgio-2.0.so.0 + +# Not sure about these suppression +# race:qscopedpointer.h +# race:qarraydata.cpp +# race:qarraydata.h +# race:qarraydataops.h +# race:libQt6Core.so.6 +# race:libQt6Gui.so.6 +# race:libQt6XcbQpa.so.6 +# race:libQt6Network.so.6 + +# very not sure about this one +# race:qstring.h diff --git a/.sanitizers/ubsan-suppressions b/.sanitizers/ubsan-suppressions new file mode 100644 index 000000000..4d35c4bef --- /dev/null +++ b/.sanitizers/ubsan-suppressions @@ -0,0 +1,2 @@ +enum:NetworkResult.hpp +enum:gtest.h diff --git a/BUILDING_ON_FREEBSD.md b/BUILDING_ON_FREEBSD.md index 8a1deeeba..38b603a2c 100644 --- a/BUILDING_ON_FREEBSD.md +++ b/BUILDING_ON_FREEBSD.md @@ -1,23 +1,23 @@ # FreeBSD -Note on Qt version compatibility: If you are installing Qt from a package manager, please ensure the version you are installing is at least **Qt 5.12 or newer**. +For all dependencies below we use Qt 6. Our minimum supported version is Qt 5.15.2, but you are on your own. -## FreeBSD 12.1-RELEASE +## FreeBSD 14.0-RELEASE -Note: This is known to work on FreeBSD 12.1-RELEASE amd64. Chances are +Note: This is known to work on FreeBSD 14.0-RELEASE amd64. Chances are high that this also works on older FreeBSD releases, architectures and -FreeBSD 13.0-CURRENT. +FreeBSD 15.0-SNAP. 1. Install build dependencies from package sources (or build from the - ports tree): `# pkg install qt5-core qt5-multimedia qt5-svg qt5-buildtools gstreamer-plugins-good boost-libs rapidjson cmake` + ports tree): `# pkg install boost-libs git qt6-base qt6-svg qt6-5compat qt6-imageformats qtkeychain-qt6 cmake` 1. In the project directory, create a build directory and enter it ```sh mkdir build cd build ``` -1. Generate build files +1. Generate build files. To enable Lua plugins in your build add `-DCHATTERINO_PLUGINS=ON` to this command. ```sh - cmake .. + cmake -DBUILD_WITH_QT6=ON .. ``` 1. Build the project ```sh diff --git a/BUILDING_ON_LINUX.md b/BUILDING_ON_LINUX.md index 5ee3f4d48..51317ecd1 100644 --- a/BUILDING_ON_LINUX.md +++ b/BUILDING_ON_LINUX.md @@ -1,38 +1,49 @@ # Linux -Note on Qt version compatibility: If you are installing Qt from a package manager, please ensure the version you are installing is at least **Qt 5.12 or newer**. +For all dependencies below we use Qt 6. Our minimum supported version is Qt 5.15.2, but you are on your own. ## Install dependencies -### Ubuntu 20.04 +### Ubuntu -_Most likely works the same for other Debian-like distros_ +Building on Ubuntu requires Docker. -Install all of the dependencies using `sudo apt install qttools5-dev qtmultimedia5-dev libqt5svg5-dev libboost-dev libssl-dev libboost-system-dev libboost-filesystem-dev cmake g++` +Use as your base if you're on Ubuntu 20.04. + +Use if you're on Ubuntu 22.04. + +The built binary should be exportable from the final image & able to run on your system assuming you perform a static build. See our [build.yml GitHub workflow file](.github/workflows/build.yml) for the CMake line used for Ubuntu builds. + +### Debian 12 (bookworm) or later + +```sh +sudo apt install qt6-base-dev qt6-5compat-dev qt6-svg-dev qt6-image-formats-plugins libboost1.81-dev libssl-dev cmake g++ git +``` ### Arch Linux -Install all of the dependencies using `sudo pacman -S --needed qt5-base qt5-multimedia qt5-svg qt5-tools gst-plugins-ugly gst-plugins-good boost rapidjson pkgconf openssl cmake` +```sh +sudo pacman -S --needed qt6-base qt6-tools boost-libs openssl qt6-imageformats qt6-5compat qt6-svg boost rapidjson pkgconf openssl cmake +``` Alternatively you can use the [chatterino2-git](https://aur.archlinux.org/packages/chatterino2-git/) package to build and install Chatterino for you. -### Fedora 28 and above +### Fedora 39 and above _Most likely works the same for other Red Hat-like distros. Substitute `dnf` with `yum`._ -Install all of the dependencies using `sudo dnf install qt5-qtbase-devel qt5-qtmultimedia-devel qt5-qtsvg-devel qt5-linguist libsecret-devel openssl-devel boost-devel cmake` +```sh +sudo dnf install qt6-qtbase-devel qt6-qtimageformats qt6-qtsvg-devel qt6-qt5compat-devel g++ git openssl-devel boost-devel cmake +``` ### NixOS 18.09+ -Enter the development environment with all of the dependencies: `nix-shell -p openssl boost qt5.full pkg-config cmake` +```sh +nix-shell -p openssl boost qt6.full pkg-config cmake +``` ## Compile -### Through Qt Creator - -1. Install C++ IDE Qt Creator by using `sudo apt install qtcreator` -1. Open `CMakeLists.txt` with Qt Creator and select build - ## Manually 1. In the project directory, create a build directory and enter it @@ -40,11 +51,16 @@ Enter the development environment with all of the dependencies: `nix-shell -p op mkdir build cd build ``` -1. Generate build files +1. Generate build files. To enable Lua plugins in your build add `-DCHATTERINO_PLUGINS=ON` to this command. ```sh - cmake .. + cmake -DBUILD_WITH_QT6=ON -DBUILD_WITH_QTKEYCHAIN=OFF .. ``` 1. Build the project ```sh - make + cmake --build . ``` + +### Through Qt Creator + +1. Install C++ IDE Qt Creator by using `sudo apt install qtcreator` (Or whatever equivalent for your distro) +1. Open `CMakeLists.txt` with Qt Creator and select build diff --git a/BUILDING_ON_MAC.md b/BUILDING_ON_MAC.md index cedfc5c0e..d1fc018b9 100644 --- a/BUILDING_ON_MAC.md +++ b/BUILDING_ON_MAC.md @@ -1,32 +1,32 @@ # Building on macOS -#### Note - If you want to develop Chatterino 2 you might also want to install Qt Creator (make sure to install **Qt 5.12 or newer**), it is not required though and any C++ IDE (might require additional setup for cmake to find Qt libraries) or a normal text editor + running cmake from terminal should work as well +Chatterino2 is built in CI on Intel on macOS 13. +Local dev machines for testing are available on Apple Silicon on macOS 13. -#### Note - Chatterino 2 is only tested on macOS 10.14 and above - anything below that is considered unsupported. It may or may not work on earlier versions +## Installing dependencies 1. Install Xcode and Xcode Command Line Utilities 1. Start Xcode, go into Settings -> Locations, and activate your Command Line Tools -1. Install brew https://brew.sh/ -1. Install the dependencies using `brew install boost openssl rapidjson cmake` -1. Install Qt5 using `brew install qt@5` -1. (_OPTIONAL_) Install [ccache](https://ccache.dev) (used to speed up compilation by using cached results from previous builds) using `brew install ccache` -1. Go into the project directory -1. Create a build folder and go into it (`mkdir build && cd build`) -1. Compile using `cmake .. && make` +1. Install [Homebrew](https://brew.sh/#install) + We use this for dependency management on macOS +1. Install all dependencies: + `brew install boost openssl@1.1 rapidjson cmake qt@5` -If the Project does not build at this point, you might need to add additional Paths/Libs, because brew does not install openssl and boost in the common path. You can get their path using +## Building -`brew info openssl` -`brew info boost` +### Building from terminal -If brew doesn't link OpenSSL properly then you should be able to link it yourself by using these two commands: +1. Open a terminal +1. Go to the project directory where you cloned Chatterino2 & its submodules +1. Create a build directory and go into it: + `mkdir build && cd build` +1. Run CMake. To enable Lua plugins in your build add `-DCHATTERINO_PLUGINS=ON` to this command. + `cmake -DCMAKE_PREFIX_PATH=/opt/homebrew/opt/qt@5 -DOPENSSL_ROOT_DIR=/opt/homebrew/opt/openssl@1.1 ..` +1. Build: + `make` -- `ln -s /usr/local/opt/openssl/lib/* /usr/local/lib` -- `ln -s /usr/local/opt/openssl/include/openssl /usr/local/include/openssl` +Your binary can now be found under bin/chatterino.app/Contents/MacOS/chatterino directory -The lines which you need to add to your project file should look similar to this +### Other building methods -``` -INCLUDEPATH += /usr/local/opt/openssl/include -LIBS += -L/usr/local/opt/openssl/lib -``` +You can achieve similar results by using an IDE like Qt Creator, although this is undocumented but if you know the IDE you should have no problems applying the terminal instructions to your IDE. diff --git a/BUILDING_ON_WINDOWS.md b/BUILDING_ON_WINDOWS.md index 8533f02fa..2449a329d 100644 --- a/BUILDING_ON_WINDOWS.md +++ b/BUILDING_ON_WINDOWS.md @@ -1,140 +1,191 @@ # Building on Windows -**Note that installing all of the development prerequisites and libraries will require about 30 GB of free disk space. Please ensure this space is available on your `C:` drive before proceeding.** +**Note that installing all the development prerequisites and libraries will require about 12 GB of free disk space. Please ensure this space is available on your `C:` drive before proceeding.** This guide assumes you are on a 64-bit system. You might need to manually search out alternate download links should you desire to build Chatterino on a 32-bit system. -## Visual Studio 2022 +## Prerequisites -Download and install [Visual Studio 2022 Community](https://visualstudio.microsoft.com/downloads/). In the installer, select "Desktop development with C++" and "Universal Windows Platform development". +### Visual Studio + +Download and install [Visual Studio 2022 Community](https://visualstudio.microsoft.com/downloads/). In the installer, select "Desktop development with C++". Notes: -- This installation will take about 17 GB of disk space +- This installation will take about 8 GB of disk space - You do not need to sign in with a Microsoft account after setup completes. You may simply exit the login dialog. -## Boost +### Qt + +1. Visit the [Qt Open Source Page](https://www.qt.io/download-open-source). +2. Scroll down to the bottom +3. Then select "Download the Qt Online Installer" +4. Within the installer, Qt will prompt you to create an account to access Qt downloads. + +Notes: + +- Installing the latest **stable** Qt version is advised for new installations, but if you want to use your existing installation please ensure you are running **Qt 5.15.2 or later**. + +#### Components + +When prompted which components to install, do the following: + +1. Unfold the tree element that says "Qt" +2. Unfold the top most tree element (latest stable Qt version, e.g. `Qt 6.5.3`) +3. Under this version, select the following entries: + - `MSVC 2019 64-bit` (or `MSVC 2022 64-bit` from Qt 6.8 onwards) + - `Qt 5 Compatibility Module` + - `Additional Libraries` > `Qt Image Formats` +4. Under the "Tools" tree element (at the bottom), ensure that `Qt Creator X.X.X` and `Debugging Tools for Windows` are selected. (they should be checked by default) +5. Continue through the installer and let the installer finish installing Qt. + +Note: This installation will take about 2 GB of disk space. + +Once Qt is done installing, make sure you add its bin directory to your `PATH` (e.g. `C:\Qt\6.5.3\msvc2019_64\bin`) + +
+ How to add Qt to PATH + +1. Type "path" in the Windows start menu and click `Edit the system environment variables`. +2. Click the `Environment Variables...` button bottom right. +3. In the `User variables` (scoped to the current user) or `System variables` (system-wide) section, scroll down until you find `Path` and double click it. +4. Click the `New` button top right and paste in the file path for your Qt installation (e.g. `C:\Qt\6.5.3\msvc2019_64\bin` by default). +5. Click `Ok` + +
+ +### Advanced dependencies + +These dependencies are only required if you are not using a package manager + +
+Boost 1. First, download a boost installer appropriate for your version of Visual Studio. - Visit the downloads list on [SourceForge](https://sourceforge.net/projects/boost/files/boost-binaries/). - Select the latest version from the list. - Download the `.exe` file appropriate to your Visual Studio installation version and system bitness (choose `-64` for 64-bit systems). - Visual Studio versions map as follows: `14.3` in the filename corresponds to MSVC 2022,`14.2` to 2019, `14.1` to 2017, `14.0` to 2015. _Anything prior to Visual Studio 2015 is unsupported. Please upgrade should you have an older installation._ + Visual Studio versions map as follows: `14.3` in the filename corresponds to MSVC 2022. _Anything prior to Visual Studio 2022 is unsupported. Please upgrade should you have an older installation._ - **Convenience link for Visual Studio 2022: [boost_1_79_0-msvc-14.3-64.exe](https://sourceforge.net/projects/boost/files/boost-binaries/1.79.0/boost_1_79_0-msvc-14.3-64.exe/download)** + **Convenience link for Visual Studio 2022: [boost_1_84_0-msvc-14.3-64.exe](https://sourceforge.net/projects/boost/files/boost-binaries/1.84.0/boost_1_84_0-msvc-14.3-64.exe/download)** 2. When prompted where to install Boost, set the location to `C:\local\boost`. 3. After the installation finishes, rename the `C:\local\boost\lib64-msvc-14.3` (or similar) directory to simply `lib` (`C:\local\boost\lib`). Note: This installation will take about 2.1 GB of disk space. -## OpenSSL +
-### For our websocket library, we need OpenSSL 1.1 +## Building -1. Download OpenSSL for windows, version `1.1.1q`: **[Download](https://slproweb.com/download/Win64OpenSSL-1_1_1q.exe)** -2. When prompted, install OpenSSL to `C:\local\openssl` -3. When prompted, copy the OpenSSL DLLs to "The OpenSSL binaries (/bin) directory". +### Using CMake -### For Qt SSL, we need OpenSSL 1.0 +#### Install conan 2 -1. Download OpenSSL for Windows, version `1.0.2u`: **[Download](https://web.archive.org/web/20211109231823/https://slproweb.com/download/Win64OpenSSL-1_0_2u.exe)** -2. When prompted, install it to any arbitrary empty directory. -3. When prompted, copy the OpenSSL DLLs to "The OpenSSL binaries (/bin) directory". -4. Copy the OpenSSL 1.0 files from its `\bin` folder to `C:\local\bin` (You will need to create the folder) -5. Then copy the OpenSSL 1.1 files from its `\bin` folder to `C:\local\bin` (Overwrite any duplicate files) -6. Add `C:\local\bin` to your path folder ([Follow the guide here if you don't know how to do it](https://www.computerhope.com/issues/ch000549.htm#windows10)) +Install [conan 2](https://conan.io/downloads.html) and make sure it's in your `PATH` (default setting). -**If the 1.1.x download link above does not work, try downloading the similar 1.1.x version found [here](https://slproweb.com/products/Win32OpenSSL.html). Note: Don't download the "light" installer, it does not have the required files.** -![Screenshot Slproweb layout](https://user-images.githubusercontent.com/41973452/175827529-97802939-5549-4ab1-95c4-d39f012d06e9.png) +
+ Adding conan to your PATH if you installed it with pip -Note: This installation will take about 200 MB of disk space. +_Note: This will add all Python-scripts to your `PATH`, conan being one of them._ -## Qt +1. Type "path" in the Windows start menu and click `Edit the system environment variables`. +2. Click the `Environment Variables...` button bottom right. +3. In the `System variables` section, scroll down until you find `Path` and double click it. +4. Click the `New` button top right. +5. Open up a terminal `where.exe conan` to find the file path (the folder that contains the conan.exe) to add. +6. Add conan 2's file path (e.g. `C:\Users\example\AppData\Roaming\Python\Python311\Scripts`) to the blank text box that shows up. This is your current Python installation's scripts folder. +7. Click `Ok` -1. Visit the [Qt Open Source Page](https://www.qt.io/download-open-source). -2. Scroll down to the bottom -3. Then select "Download the Qt Online Installer" +
-Notes: +Then in a terminal, configure conan to use `NMake Makefiles` as its generator: -- Installing the latest **stable** Qt version is advised for new installations, but if you want to use your existing installation please ensure you are running **Qt 5.12 or later**. +1. Generate a new profile + `conan profile detect` -### When prompted which components to install: +#### Build -1. Unfold the tree element that says "Qt" -2. Unfold the top most tree element (latest stable Qt version, e.g. `Qt 5.15.2`) -3. Under this version, select the following entries: - - `MSVC 2019 64-bit` (or alternative version if you are using that) - - `Qt WebEngine` (optional) -4. Under the "Tools" tree element (at the bottom), ensure that `Qt Creator X.X.X` and `Debugging Tools for Windows` are selected. (they should be checked by default) -5. Continue through the installer and let the installer finish installing Qt. +Open up your terminal with the Visual Studio environment variables (e.g. `x64 Native Tools Command Prompt for VS 2022`), cd to the cloned chatterino2 directory and run the following commands: -Note: This installation will take about 2 GB of disk space. +```cmd +mkdir build +cd build +conan install .. -s build_type=Release -c tools.cmake.cmaketoolchain:generator="NMake Makefiles" --build=missing --output-folder=. +cmake -G"NMake Makefiles" -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE="conan_toolchain.cmake" -DCMAKE_PREFIX_PATH="C:\Qt\6.5.3\msvc2019_64" .. +nmake +``` -## Compile with Breakpad support (Optional) +To build a debug build, you'll also need to add the `-s compiler.runtime_type=Debug` flag to the `conan install` invocation. See [this StackOverflow post](https://stackoverflow.com/questions/59828611/windeployqt-doesnt-deploy-qwindowsd-dll-for-a-debug-application/75607313#75607313) +To build with plugins add `-DCHATTERINO_PLUGINS=ON` to `cmake` command. -Compiling with Breakpad support enables crash reports that can be of use for developing/beta versions of Chatterino. If you have no interest in reporting crashes anyways, this optional dependency will probably be of no use to you. +#### Deploying Qt libraries -1. Open up `lib/qBreakpad/handler/handler.pro`in Qt Creator -2. Build it in whichever mode you want to build Chatterino in (Debug/Profile/Release) -3. Copy the newly built `qBreakpad.lib` to the following directory: `lib/qBreakpad/build/handler` (You will have to manually create this directory) +Once Chatterino has finished building, to ensure all .dll's are available you can run this from the build directory: +`windeployqt bin/chatterino.exe --release --no-compiler-runtime --no-translations --no-opengl-sw --dir bin/` -## Run the build in Qt Creator +Can't find windeployqt? You forgot to add your Qt bin directory (e.g. `C:\Qt\6.5.3\msvc2019_64\bin`) to your `PATH` + +### Developing in Qt Creator 1. Open the `CMakeLists.txt` file by double-clicking it, or by opening it via Qt Creator. 2. You will be presented with a screen that is titled "Configure Project". In this screen, you should have at least one option present ready to be configured, like this: ![Qt Create Configure Project screenshot](https://user-images.githubusercontent.com/69117321/169887645-2ae0871a-fe8a-4eb9-98db-7b996dea3a54.png) 3. Select the profile(s) you want to build with and click "Configure Project". -### How to run and produce builds +#### Building and running - In the main screen, click the green "play symbol" on the bottom left to run the project directly. - Click the hammer on the bottom left to generate a build (does not run the build though). -Build results will be placed in a folder at the same level as the "chatterino2" project folder (e.g. if your sources are at `C:\Users\example\src\chatterino2`, then the build will be placed in an automatically generated folder under `C:\Users\example\src`, e.g. `C:\Users\example\src\build-chatterino-Desktop_Qt_5_15_2_MSVC2019_64bit-Release`.) +Build results will be placed in a folder at the same level as the "chatterino2" project folder (e.g. if your sources are at `C:\Users\example\src\chatterino2`, then the build will be placed in an automatically generated folder under `C:\Users\example\src`, e.g. `C:\Users\example\src\build-chatterino-Desktop_Qt_6.5.3_MSVC2019_64bit-Release`.) -- Note that if you are building chatterino purely for usage, not for development, it is recommended that you click the "PC" icon above the play icon and select "Release" instead of "Debug". +- Note that if you are building Chatterino purely for usage, not for development, it is recommended that you click the "PC" icon above the play icon and select "Release" instead of "Debug". - Output and error messages produced by the compiler can be seen under the "4 Compile Output" tab in Qt Creator. -## Producing standalone builds +#### Producing standalone builds -If you build chatterino, the result directories will contain a `chatterino.exe` file in the `$OUTPUTDIR\release\` directory. This `.exe` file will not directly run on any given target system, because it will be lacking various Qt runtimes. +If you build Chatterino, the result directories will contain a `chatterino.exe` file in the `$OUTPUTDIR\release\` directory. This `.exe` file will not directly run on any given target system, because it will be lacking various Qt runtimes. -To produce a standalone package, you need to generate all required files using the tool `windeployqt`. This tool can be found in the `bin` directory of your Qt installation, e.g. at `C:\Qt\5.15.2\msvc2019_64\bin\windeployqt.exe`. +To produce a standalone package, you need to generate all required files using the tool `windeployqt`. This tool can be found in the `bin` directory of your Qt installation, e.g. at `C:\Qt\6.5.3\msvc2019_64\bin\windeployqt.exe`. To produce all supplement files for a standalone build, follow these steps (adjust paths as required): -1. Navigate to your build output directory with Windows Explorer, e.g. `C:\Users\example\src\build-chatterino-Desktop_Qt_5_15_2_MSVC2019_64bit-Release` -2. Enter the `release` directory -3. Delete all files except the `chatterino.exe` file. You should be left with a directory only containing `chatterino.exe`. -4. Open a command prompt and execute: +1. Navigate to your build output directory with Windows Explorer, e.g. `C:\Users\example\src\build-chatterino-Desktop_Qt_6.5.3_MSVC2019_64bit-Release` +2. Enter the `release` directory +3. Delete all files except the `chatterino.exe` file. You should be left with a directory only containing `chatterino.exe`. +4. Open a command prompt and execute: + ```cmd + cd C:\Users\example\src\build-chatterino-Desktop_Qt_6.5.3_MSVC2019_64bit-Release\release + windeployqt bin/chatterino.exe --release --no-compiler-runtime --no-translations --no-opengl-sw --dir bin/ + ``` +5. The `releases` directory will now be populated with all the required files to make the Chatterino build standalone. - cd C:\Users\example\src\build-chatterino-Desktop_Qt_5_15_2_MSVC2019_64bit-Release\release - C:\Qt\5.15.2\msvc2019_64\bin\windeployqt.exe chatterino.exe +You can now create a zip archive of all the contents in `releases` and distribute the program as is, without requiring any development tools to be present on the target system. (However, the CRT must be present, as usual - see the [README](README.md)). -5. Go to `C:\local\bin\` and copy these dll's into your `release folder`. +#### Formatting - libssl-1_1-x64.dll - libcrypto-1_1-x64.dll - ssleay32.dll - libeay32.dll +To automatically format your code, do the following: -6. The `releases` directory will now be populated with all the required files to make the chatterino build standalone. +1. Download [LLVM 16.0.6](https://github.com/llvm/llvm-project/releases/download/llvmorg-16.0.6/LLVM-16.0.6-win64.exe) +2. During the installation, make sure to add it to your path +3. In Qt Creator, Select `Tools` > `Options` > `Beautifier` +4. Under `General` select `Tool: ClangFormat` and enable `Automatic Formatting on File Save` +5. Under `Clang Format` select `Use predefined style: File` and `Fallback style: None` -You can now create a zip archive of all the contents in `releases` and distribute the program as is, without requiring any development tools to be present on the target system. (However, the vcredist package must be present, as usual - see the [README](README.md)). +### Building on MSVC with AddressSanitizer -## Using CMake +Make sure you installed `C++ AddressSanitizer` in your VisualStudio installation like described in the [Microsoft Docs](https://learn.microsoft.com/en-us/cpp/sanitizers/asan#install-the-addresssanitizer). -Open up your terminal with the Visual Studio environment variables, then enter the following commands: +To build Chatterino with AddressSanitizer on MSVC, you need to add `-DCMAKE_CXX_FLAGS=/fsanitize=address` to your CMake options. -1. `mkdir build` -2. `cd build` -3. `cmake -G"NMake Makefiles" -DCMAKE_BUILD_TYPE=Release -DUSE_CONAN=ON ..` -4. `nmake` +When you start Chatterino, and it's complaining about `clang_rt.asan_dbg_dynamic-x86_64.dll` missing, +copy the file found in `\VC\Tools\MSVC\\bin\Hostx64\x64\clang_rt.asan_dbg_dynamic-x86_64.dll` to the `Chatterino` folder inside your `build` folder. -## Building/Running in CLion +To learn more about AddressSanitizer and MSVC, visit the [Microsoft Docs](https://learn.microsoft.com/en-us/cpp/sanitizers/asan). + +### Developing in CLion _Note:_ We're using `build` instead of the CLion default `cmake-build-debug` folder. @@ -147,10 +198,9 @@ Clone the repository as described in the readme. Open a terminal in the cloned f Now open the project in CLion. You will be greeted with the _Open Project Wizard_. Set the _CMake Options_ to -``` --DCMAKE_PREFIX_PATH=C:\Qt\5.15.2\msvc2019_64\lib\cmake\Qt5 --DUSE_CONAN=ON --DCMAKE_CXX_FLAGS=/bigobj +```text +-DCMAKE_PREFIX_PATH=C:\Qt\6.5.3\msvc2019_64\lib\cmake\Qt6 +-DCMAKE_TOOLCHAIN_FILE="conan_toolchain.cmake" ``` and the _Build Directory_ to `build`. @@ -166,7 +216,7 @@ After the CMake project is loaded, open the _Run/Debug Configurations_. Select the `CMake Applications > chatterino` configuration and add a new _Run External tool_ task to _Before launch_. -- Set the _Program_ to `C:\Qt\5.15.2\msvc2019_64\bin\windeployqt.exe` +- Set the _Program_ to `C:\Qt\6.5.3\msvc2019_64\bin\windeployqt.exe` - Set the _Arguments_ to `$CMakeCurrentProductFile$ --debug --no-compiler-runtime --no-translations --no-opengl-sw --dir bin/` - Set the _Working directory_ to `$ProjectFileDir$\build` @@ -179,13 +229,37 @@ Select the `CMake Applications > chatterino` configuration and add a new _Run Ex
-Screenshot of chatterino configuration +Screenshot of Chatterino configuration -![Screenshot of chatterino configuration](https://user-images.githubusercontent.com/41973452/160240843-dc0c603c-227f-4f56-98ca-57f03989dfb4.png) +![Screenshot of Chatterino configuration](https://user-images.githubusercontent.com/41973452/160240843-dc0c603c-227f-4f56-98ca-57f03989dfb4.png)
Now you can run the `chatterino | Debug` configuration. -If you want to run the portable version of Chatterino, create a file called `modes` inside of `build/bin` and +If you want to run the portable version of Chatterino, create a file called `modes` inside `build/bin` and write `portable` into it. + +#### Debugging + +To visualize Qt types like `QString`, you need to inform CLion and LLDB +about these types. + +1. Set `Enable NatVis renderers for LLDB option` + in `Settings | Build, Execution, Deployment | Debugger | Data Views | C/C++` (should be enabled by default). +2. Use the official NatVis file for Qt from [`qt-labs/vstools`](https://github.com/qt-labs/vstools) by saving them to + the project root using PowerShell: + + + +```powershell +(iwr "https://github.com/qt-labs/vstools/raw/dev/QtVsTools.Package/qt6.natvis.xml").Content.Replace('##NAMESPACE##::', '') | Out-File qt6.natvis +# [OR] using the permalink +(iwr "https://github.com/qt-labs/vstools/raw/1c8ba533bd88d935be3724667e0087fd0796102c/QtVsTools.Package/qt6.natvis.xml").Content.Replace('##NAMESPACE##::', '') | Out-File qt6.natvis +``` + +Now you can debug the application and see Qt types rendered correctly. +If this didn't work for you, try following +the [tutorial from JetBrains](https://www.jetbrains.com/help/clion/qt-tutorial.html#debug-renderers). diff --git a/BUILDING_ON_WINDOWS_WITH_VCPKG.md b/BUILDING_ON_WINDOWS_WITH_VCPKG.md index 4bfac943d..18748acb7 100644 --- a/BUILDING_ON_WINDOWS_WITH_VCPKG.md +++ b/BUILDING_ON_WINDOWS_WITH_VCPKG.md @@ -1,35 +1,54 @@ # Building on Windows with vcpkg +This will require more than 30 GB of free space on your hard drive. + ## Prerequisites -1. Install [Visual Studio](https://visualstudio.microsoft.com/) with "Desktop development with C++" (~9.66 GB) -1. Install [CMake](https://cmake.org/) (~109 MB) -1. Install [git](https://git-scm.com/) (~264 MB) -1. Install [vcpkg](https://vcpkg.io/) (~80 MB) - - `git clone https://github.com/Microsoft/vcpkg.git` - - `cd .\vcpkg\` - - `.\bootstrap-vcpkg.bat` - - `.\vcpkg integrate install` - - `.\vcpkg integrate powershell` - - `cd ..` -1. Configure the environment for vcpkg - - `set VCPKG_DEFAULT_TRIPLET=x64-windows` - - [default](https://github.com/microsoft/vcpkg/blob/master/docs/users/triplets.md#additional-remarks) is `x86-windows` - - `set VCPKG_ROOT=C:\path\to\vcpkg\` - - `set PATH=%PATH%;%VCPKG_ROOT%` +1. Install [Visual Studio](https://visualstudio.microsoft.com/) with "Desktop development with C++" +1. Install [CMake](https://cmake.org/) +1. Install [git](https://git-scm.com/) +1. Install [vcpkg](https://vcpkg.io/) + + ```shell + git clone https://github.com/Microsoft/vcpkg.git + cd vcpkg + .\bootstrap-vcpkg.bat + .\vcpkg integrate install + .\vcpkg integrate powershell + cd .. + ``` + +1. Configure the environment variables for vcpkg. + Check [this document](https://gist.github.com/mitchmindtree/92c8e37fa80c8dddee5b94fc88d1288b#setting-an-environment-variable-on-windows) for more information for how to set environment variables on Windows. + - Ensure your dependencies are built as 64-bit + e.g. `setx VCPKG_DEFAULT_TRIPLET x64-windows` + See [documentation about Triplets](https://learn.microsoft.com/en-gb/vcpkg/users/triplets) + [default](https://github.com/microsoft/vcpkg/blob/master/docs/users/triplets.md#additional-remarks) is `x86-windows` + - Set VCPKG_ROOT to the vcpkg path + e.g. `setx VCPKG_ROOT ` + See [VCPKG_ROOT documentation](https://learn.microsoft.com/en-gb/vcpkg/users/config-environment#vcpkg_root) + - Append the vcpkg path to your path + e.g. `setx PATH "%PATH%;"` + - For more configurations, see +1. You may need to restart your computer to ensure all your environment variables and what-not are loaded everywhere. ## Building 1. Clone - - `git clone --recurse-submodules https://github.com/Chatterino/chatterino2.git` -1. Install dependencies (~21 GB) - - `cd .\chatterino2\` - - `vcpkg install` + ```shell + git clone --recurse-submodules https://github.com/Chatterino/chatterino2.git + ``` +1. Install dependencies + ```powershell + cd .\chatterino2\ + vcpkg install + ``` 1. Build - - `mkdir .\build\` - - `cd .\build\` - - (cmd) `cmake .. -DCMAKE_TOOLCHAIN_FILE=%VCPKG_ROOT%/scripts/buildsystems/vcpkg.cmake` - - (ps1) `cmake .. -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake"` - - `cmake --build . --parallel --config Release` -1. Run - - `.\bin\chatterino2.exe` + ```powershell + cmake -B build -DCMAKE_TOOLCHAIN_FILE="$Env:VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" + cd build + cmake --build . --parallel --config Release + ``` + When using CMD, use `-DCMAKE_TOOLCHAIN_FILE=%VCPKG_ROOT%/scripts/buildsystems/vcpkg.cmake` to specify the toolchain. + To build with plugins add `-DCHATTERINO_PLUGINS=ON` to `cmake -B build` command. +1. Run `.\bin\chatterino2.exe` diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ed9a7339..939a46754 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,35 +2,612 @@ ## Unversioned -- Major: Added support for Twitch's Chat Replies. [Wiki Page](https://wiki.chatterino.com/Features/#message-replies) (#3722) +- Major: Release plugins alpha. (#5288) +- Major: Improve high-DPI support on Windows. (#4868, #5391) +- Minor: Removed the Ctrl+Shift+L hotkey for toggling the "live only" tab visibility state. (#5530) +- Minor: Moved tab visibility control to a submenu, without any toggle actions. (#5530) +- Minor: Add option to customise Moderation buttons with images. (#5369) +- Minor: Colored usernames now update on the fly when changing the "Color @usernames" setting. (#5300) +- Minor: Added `flags.action` filter variable, allowing you to filter on `/me` messages. (#5397) +- Minor: Added the ability for `/ban`, `/timeout`, `/unban`, and `/untimeout` to specify multiple channels to duplicate the action to. Example: `/timeout --channel id:11148817 --channel testaccount_420 forsen 7m game complaining`. (#5402) +- Minor: The size of the emote popup is now saved. (#5415) +- Minor: Added the ability to duplicate tabs. (#5277) +- Minor: Improved error messages for channel update commands. (#5429) +- Minor: Moderators can now see when users are warned. (#5441) +- Minor: Added support for Brave & google-chrome-stable browsers. (#5452) +- Minor: Added drop indicator line while dragging in tables. (#5256) +- Minor: Add channel points indication for new bits power-up redemptions. (#5471) +- Minor: Added option to log streams by their ID, allowing for easier "per-stream" log analyzing. (#5507) +- Minor: Added `/warn ` command for mods. This prevents the user from chatting until they acknowledge the warning. (#5474) +- Minor: Added option to suppress live notifictions on startup. (#5388) +- Minor: Improve appearance of reply button. (#5491) +- Minor: Introduce HTTP API for plugins. (#5383, #5492, #5494) +- Minor: Support more Firefox variants for incognito link opening. (#5503) +- Minor: Replying to a message will now display the message being replied to. (#4350, #5519) +- Minor: Links can now have prefixes and suffixes such as parentheses. (#5486, #5515) +- Minor: Added support for scrolling in splits with touchscreen panning gestures. (#5524) +- Minor: Removed experimental IRC support. (#5547) +- Minor: Moderators can now see which mods start and cancel raids. (#5563) +- Bugfix: Fixed tab move animation occasionally failing to start after closing a tab. (#5426) +- Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378) +- Bugfix: Fixed restricted users usernames not being clickable. (#5405) +- Bugfix: Fixed a crash that could occur when logging was enabled in IRC servers that were removed. (#5419) +- Bugfix: Fixed message history occasionally not loading after a sleep. (#5457) +- Bugfix: Fixed a crash when tab completing while having an invalid plugin loaded. (#5401) +- Bugfix: Fixed windows on Windows not saving correctly when snapping them to the edges. (#5478) +- Bugfix: Fixed user info card popups adding duplicate line to log files. (#5499) +- Bugfix: Fixed tooltips and input completion popups not working after moving a split. (#5541, #5576) +- Bugfix: Fixed rare issue on shutdown where the client would hang. (#5557) +- Bugfix: Fixed `/clearmessages` not working with more than one window. (#5489) +- Bugfix: Fixed splits staying paused after unfocusing Chatterino in certain configurations. (#5504) +- Bugfix: Links with invalid characters in the domain are no longer detected. (#5509) +- Bugfix: Fixed janky selection for messages with RTL segments (selection is still wrong, but consistently wrong). (#5525) +- Bugfix: Fixed tab visibility being controllable in the emote popup. (#5530) +- Bugfix: Fixed account switch not being saved if no other settings were changed. (#5558) +- Bugfix: Fixed some tooltips not being readable. (#5578) +- Dev: Update Windows build from Qt 6.5.0 to Qt 6.7.1. (#5420) +- Dev: Update vcpkg build Qt from 6.5.0 to 6.7.0, boost from 1.83.0 to 1.85.0, openssl from 3.1.3 to 3.3.0. (#5422) +- Dev: Unsingletonize `ISoundController`. (#5462) +- Dev: Use Qt's high DPI scaling. (#4868, #5400) +- Dev: Add doxygen build target. (#5377) +- Dev: Make printing of strings in tests easier. (#5379) +- Dev: Refactor and document `Scrollbar`. (#5334, #5393) +- Dev: Refactor `TwitchIrcServer`, making it abstracted. (#5421, #5435) +- Dev: Reduced the amount of scale events. (#5404, #5406) +- Dev: Removed unused timegate settings. (#5361) +- Dev: Add `Channel::addSystemMessage` helper function, allowing us to avoid the common `channel->addMessage(makeSystemMessage(...));` pattern. (#5500) +- Dev: Unsingletonize `Resources2`. (#5460) +- Dev: All Lua globals now show in the `c2` global in the LuaLS metadata. (#5385) +- Dev: Images are now loaded in worker threads. (#5431) +- Dev: Fixed broken `SignalVector::operator[]` implementation. (#5556) +- Dev: Qt Creator now auto-configures Conan when loading the project and skips vcpkg. (#5305) +- Dev: The MSVC CRT is now bundled with Chatterino as it depends on having a recent version installed. (#5447) +- Dev: Refactor/unsingletonize `UserDataController`. (#5459) +- Dev: Cleanup `BrowserExtension`. (#5465) +- Dev: Deprecate Qt 5.12. (#5396) +- Dev: Refactored `MessageFlag` into its own file. (#5549) +- Dev: The running Qt version is now shown in the about page if it differs from the compiled version. (#5501) +- Dev: `FlagsEnum` is now `constexpr`. (#5510) +- Dev: Documented and added tests to RTL handling. (#5473) +- Dev: Refactored 7TV/BTTV definitions out of `TwitchIrcServer` into `Application`. (#5532) +- Dev: Refactored code that's responsible for deleting old update files. (#5535) +- Dev: Cleanly exit on shutdown. (#5537) +- Dev: Removed the `getTwitchAbstract` method in `Application`. (#5560) +- Dev: Renamed threads created by Chatterino on Linux and Windows. (#5538, #5539, #5544) +- Dev: Refactored a few `#define`s into `const(expr)` and cleaned includes. (#5527) +- Dev: Added `FlagsEnum::isEmpty`. (#5550) +- Dev: Prepared for Qt 6.8 by addressing some deprecations. (#5529) +- Dev: Moved some responsibility away from Application into WindowManager. (#5551) +- Dev: Fixed benchmarks segfaulting on run. (#5559) +- Dev: Refactored `MessageBuilder` to be a single class. (#5548) +- Dev: Recent changes are now shown in the nightly release description. (#5553, #5554) +- Dev: The timer for `StreamerMode` is now destroyed on the correct thread. (#5571) + +## 2.5.1 + +- Bugfix: Fixed links without a protocol not being clickable. (#5345) + +## 2.5.0 + +- Major: Twitch follower emotes can now be correctly tabbed in other channels when you are subscribed to the channel the emote is from. (#4922) +- Major: Added `/automod` split to track automod caught messages across all open channels the user moderates. (#4986, #5026) +- Major: Moderators can now see restricted chat messages and suspicious treatment updates. (#5056, #5060) +- Minor: Migrated to the new Get Channel Followers Helix endpoint, fixing follower count not showing up in usercards. (#4809) +- Minor: Moderation commands such as `/ban`, `/timeout`, `/unban`, and `/untimeout` can now be used via User IDs by using the `id:123` syntax (e.g. `/timeout id:22484632 1m stop winning`). (#4945, #4956, #4957) +- Minor: The `/usercard` command now accepts user ids. (`/usercard id:22484632`) (#4934) +- Minor: Added menu actions to reply directly to a message or the original thread root. (#4923) +- Minor: The `/reply` command now replies to the latest message from the user. Due to this change, the message you intended to reply to is now shown in the reply context, instead of the first message in a thread. (#4919) +- Minor: The chatter list button is now hidden if you don't have moderator privileges. (#5245) +- Minor: Live streams that are marked as reruns now mark a tab as yellow instead of red. (#5176, #5237) +- Minor: Allowed theming of tab live and rerun indicators. (#5188) +- Minor: The _Restart on crash_ setting works again on Windows. (#5012) +- Minor: Added an option to use new experimental smarter emote completion. (#4987) +- Minor: Added support for FrankerFaceZ channel badges. These can be configured at https://www.frankerfacez.com/channel/mine - currently only supports bot badges for your chat bots. (#5119) +- Minor: Added support to send /announce[color] commands. Colored announcements only appear with the chosen color in Twitch chat. (#5250) +- Minor: The whisper highlight color can now be configured through the settings. (#5053) +- Minor: Added an option to always include the broadcaster in user completions. This is enabled by default. (#5193, #5244) +- Minor: Added a warning message if you have multiple commands with the same trigger. (#4322) +- Minor: Chatters from message history are now added to autocompletion. (#5116) +- Minor: Added support for the `{input.text}` placeholder in the **Split** -> **Run a command** hotkey. (#5130) +- Minor: Added `--activate ` (or `-a`) command line option to focus or add a certain Twitch channel on startup. (#5111) +- Minor: Added the `--incognito/--no-incognito` options to the `/openurl` command, allowing you to override the "Open links in incognito/private mode" setting. (#5149, #5197) +- Minor: Added the ability to change the top-most status of a window regardless of the _Always on top_ setting (right click the notebook). (#5135) +- Minor: Added the ability to show AutoMod caught messages in mentions. (#5215) +- Minor: Added the ability to configure the color of highlighted AutoMod caught messages. (#5215) +- Minor: Updated to Emoji v15.1. Google emojis are now used as the fallback instead of Twitter emojis. (#5182) +- Minor: Added icons for newer versions of macOS. (#5148) +- Minor: Added more menu items in macOS menu bar. (#5266) +- Minor: Improved color selection and display. (#5057) +- Minor: Added a _System_ theme setting that updates according to the system's color scheme (requires Qt 6.5). (#5118) +- Minor: Normalized the input padding between light & dark themes. (#5095) +- Minor: The account switcher is now styled to match your theme. (#4817) +- Minor: Added a fallback theme field to custom themes that will be used in case the custom theme does not contain a color Chatterino needs. If no fallback theme is specified, we'll pull the color from the included Dark or Light theme. (#5198) +- Minor: Added a new completion API for experimental plugins feature. (#5000, #5047) +- Minor: Added a new Channel API for experimental plugins feature. (#5141, #5184, #5187) +- Minor: Introduce `c2.later()` function to Lua API. (#5154) +- Minor: Added `--safe-mode` command line option that can be used for troubleshooting when Chatterino is misbehaving or is misconfigured. It disables hiding the settings button & prevents plugins from loading. (#4985) +- Minor: Added wrappers for Lua `io` library for experimental plugins feature. (#5231) +- Minor: Added permissions to experimental plugins feature. (#5231) +- Minor: Added missing periods at various moderator messages and commands. (#5061) +- Minor: Improved Streamlink documentation in the settings dialog. (#5076) +- Minor: The installer now checks for the VC Runtime version and shows more info when it's outdated. (#4847) +- Minor: All sound capabilities can now be disabled by setting your "Sound backend" setting to "Null" and restarting Chatterino. (#4978) +- Minor: Added an invisible resize handle to the bottom of frameless user info popups and reply thread popups. (#4795) +- Minor: Updated the flatpakref link included with nightly builds to point to up-to-date flathub-beta builds. (#5008) +- Minor: Image links now reflect the scale of their image instead of an internal label. (#5201) +- Minor: IPC files are now stored in the Chatterino directory instead of system directories on Windows. (#5226) +- Minor: 7TV emotes now have a 4x image rather than a 3x image. (#5209) +- Minor: Add `reward.cost` `reward.id`, `reward.title` filter variables. (#5275) +- Minor: Change Lua `CompletionRequested` handler to use an event table. (#5280) +- Minor: Changed the layout of the about page. (#5287) +- Minor: Add duration to multi-month anon sub gift messages. (#5293) +- Minor: Added context menu action to toggle visibility of offline tabs. (#5318) +- Minor: Report sub duration for more multi-month gift cases. (#5319) +- Minor: Improved error reporting for the automatic streamer mode detection on Linux and macOS. (#5321) +- Bugfix: Fixed a crash that could occur on Wayland when using the image uploader. (#5314) +- Bugfix: Fixed split tooltip getting stuck in some cases. (#5309) +- Bugfix: Fixed the version string not showing up as expected in Finder on macOS. (#5311) +- Bugfix: Fixed links having `http://` added to the beginning in certain cases. (#5323) +- Bugfix: Fixed topmost windows from losing their status after opening dialogs on Windows. (#5330) +- Bugfix: Fixed a gap appearing when using filters on `/watching`. (#5329) +- Bugfix: Removed the remnant "Show chatter list" menu entry for non-moderators. (#5336) +- Bugfix: Fixed an issue where certain emojis did not send to Twitch chat correctly. (#4840) +- Bugfix: Fixed the `/shoutout` command not working with usernames starting with @'s (e.g. `/shoutout @forsen`). (#4800) +- Bugfix: Fixed capitalized channel names in log inclusion list not being logged. (#4848) +- Bugfix: Trimmed custom streamlink paths on all platforms making sure you don't accidentally add spaces at the beginning or end of its path. (#4834) +- Bugfix: Fixed a performance issue when displaying replies to certain messages. (#4807) +- Bugfix: Fixed an issue where certain parts of the split input wouldn't focus the split when clicked. (#4958) +- Bugfix: Fixed an issue in the `/live` split that caused some channels to not get grayed-out when they went offline. (#5172)\ +- Bugfix: User text input within watch streak notices now correctly shows up. (#5029) +- Bugfix: Fixed selection of tabs after closing a tab when using "Live Tabs Only". (#4770) +- Bugfix: Fixed input in the reply thread popup losing focus when dragging said window. (#4815) +- Bugfix: Fixed the Quick Switcher (CTRL+K) sometimes showing up on the wrong window. (#4819) +- Bugfix: Fixed the font switcher not remembering what font you had previously selected. (#5224) +- Bugfix: Fixed too much text being copied when copying chat messages. (#4812, #4830, #4839) +- Bugfix: Fixed issue on Windows preventing the title bar from being dragged in the top left corner. (#4873) +- Bugfix: Fixed an issue where Streamer Mode did not detect that OBS was running on MacOS. (#5260) +- Bugfix: Remove ":" from the message the user is replying to if it's a /me message. (#5263) +- Bugfix: Fixed the "Cancel" button in the settings dialog only working after opening the settings dialog twice. (#5229) +- Bugfix: Fixed an issue where the setting `Only search for emote autocompletion at the start of emote names` wouldn't disable if it was enabled when the client started. (#4855) +- Bugfix: Fixed an empty page being added when showing the out of bounds dialog. (#4849) +- Bugfix: Fixed an issue preventing searching a redemption by it's title when the redemption contained user text input. (#5117) +- Bugfix: Fixed an issue where reply context didn't render correctly if an emoji was touching text. (#4875, #4977, #5174) +- Bugfix: Fixed the input completion popup sometimes disappearing when clicking on it on Windows and macOS. (#4876) +- Bugfix: Fixed Twitch badges not loading correctly in the badge highlighting setting page. (#5223) +- Bugfix: Fixed double-click text selection moving its position with each new message. (#4898) +- Bugfix: Fixed an issue where notifications on Windows would contain no or an old avatar. (#4899) +- Bugfix: Fixed headers of tables in the settings switching to bold text when selected. (#4913) +- Bugfix: Fixed tooltips appearing too large and/or away from the cursor. (#4920) +- Bugfix: Fixed thread popup window missing messages for nested threads. (#4923) +- Bugfix: Fixed an occasional crash for channel point redemptions with text input. (#4949) +- Bugfix: Fixed triple-click on message also selecting moderation buttons. (#4961) +- Bugfix: Fixed badge highlight changes not immediately being reflected. (#5110) +- Bugfix: Fixed emotes being reloaded when pressing "Cancel" in the settings dialog, causing a slowdown. (#5240) +- Bugfix: Fixed double-click selection not correctly selecting words that were split onto multiple lines. (#5243) +- Bugfix: Fixed some emotes not appearing when using _Ignores_. (#4965, #5126) +- Bugfix: Fixed a freeze from a bad regex in _Ignores_. (#4965, #5126) +- Bugfix: Fixed lookahead/-behind not working in _Ignores_. (#4965, #5126) +- Bugfix: Fixed Image Uploader accidentally deleting images with some hosts when link resolver was enabled. (#4971) +- Bugfix: Fixed a rare crash with the Image Uploader when closing a split right after starting an upload. (#4971) +- Bugfix: Fixed an issue on macOS where the Image Uploader would keep prompting the user even after they clicked "Yes, don't ask again". (#5011) +- Bugfix: The usercard button is now hidden in the User Info Popup when in special channels. (#4972) +- Bugfix: Fixed support for Windows 11 Snap layouts. (#4994, #5175) +- Bugfix: Fixed some windows appearing between screens. (#4797) +- Bugfix: Fixed a crash that could occur when clicking `More messages below` button in a usercard and closing it quickly. (#4933) +- Bugfix: Fixed a crash that could occur when using certain features in a Usercard after closing the split from which it was created. (#5034, #5051) +- Bugfix: Fixed a crash that could occur when using certain features in a Reply popup after closing the split from which it was created. (#5036, #5051) +- Bugfix: Fixed a bug on Wayland where tooltips would spawn as separate windows instead of behaving like tooltips. (#4998, #5040) +- Bugfix: Fixes to section deletion in text input fields. (#5013) +- Bugfix: Fixed avatar in usercard and moderation button triggering when releasing the mouse outside their area. (#5052) +- Bugfix: Fixed a bug where buttons would remain in a hovered state after leaving them. (#5077) +- Bugfix: Fixed an issue where you had to click the `reply` button twice if you already had that users @ in your input box. (#5173) +- Bugfix: Fixed popup windows not persisting between restarts. (#5081) +- Bugfix: Fixed splits not retaining their focus after minimizing. (#5080) +- Bugfix: Fixed _Copy message_ copying the channel name in global search. (#5106) +- Bugfix: Fixed some Twitch emotes sizes being wrong at certain zoom levels. (#5279, #5291) +- Bugfix: Fixed a missing space when the image uploader provided a delete link. (#5269) +- Bugfix: Reply contexts now use the color of the replied-to message. (#5145) +- Bugfix: Fixed top-level window getting stuck after opening settings. (#5161, #5166) +- Bugfix: Fixed link info not updating without moving the cursor. (#5178) +- Bugfix: Fixed an upload sometimes failing when copying an image from a browser if it contained extra properties. (#5156) +- Bugfix: Fixed tooltips getting out of bounds when loading images. (#5186) +- Bugfix: Fixed split header tooltips showing in the wrong position on Windows. (#5230) +- Bugfix: Fixed split header tooltips appearing too tall. (#5232) +- Bugfix: Fixed past messages not showing in the search popup after adding a channel. (#5248) +- Bugfix: Fixed pause indicator not disappearing in some cases. (#5265) +- Bugfix: Fixed the usercard popup not floating on tiling WMs on Linux when "Automatically close user popup when it loses focus" setting is enabled. (#3511) +- Bugfix: Fixed moderator-only topics being subscribed to for non-moderators. (#5056) +- Bugfix: Truncated IRC messages to be at most 512 bytes. (#5246) +- Bugfix: Fixed a data race when disconnecting from Twitch PubSub. (#4771) +- Bugfix: Fixed messages not immediately disappearing when clearing the chat. (#5282) +- Bugfix: Fixed highlights triggering for ignored users in announcements. (#5295) +- Dev: Changed the order of the query parameters for Twitch player URLs. (#5326) +- Dev: Run miniaudio in a separate thread, and simplify it to not manage the device ourselves. There's a chance the simplification is a bad idea. (#4978) +- Dev: Change clang-format from v14 to v16. (#4929) +- Dev: Fixed UTF16 encoding of `modes` file for the installer. (#4791) +- Dev: Temporarily disable High DPI scaling on Qt6 builds on Windows. (#4767) +- Dev: Tests now run on Ubuntu 22.04 instead of 20.04 to loosen C++ restrictions in tests. (#4774) +- Dev: Do a pretty major refactor of the Settings classes. List settings (e.g. highlights) are most heavily modified, and should have an extra eye kept on them. (#4775) +- Dev: conan: Update Boost to 1.83 & OpenSSL to 3.2.0. (#5007) +- Dev: Remove `boost::noncopyable` use & `boost_random` dependency. (#4776) +- Dev: Fix clang-tidy `cppcoreguidelines-pro-type-member-init` warnings. (#4426) +- Dev: Immediate layout for invisible `ChannelView`s is skipped. (#4811) +- Dev: Refactor `Image` & Image's `Frames`. (#4773) +- Dev: Add `WindowManager::getLastSelectedWindow()` to replace `getMainWindow()`. (#4816) +- Dev: Clarify signal connection lifetimes where applicable. (#4818) +- Dev: Laid the groundwork for advanced input completion strategies. (#4639, #4846, #4853, #4893) +- Dev: Fixed flickering when running with Direct2D on Windows. (#4851) +- Dev: Fix qtkeychain include for Qt6 users. (#4863) +- Dev: Add a compile-time flag `CHATTERINO_UPDATER` which can be turned off to disable update checks. (#4854) +- Dev: Add a compile-time flag `USE_SYSTEM_MINIAUDIO` which can be turned on to use the system miniaudio. (#4867) +- Dev: Update vcpkg to use Qt6. (#4872) +- Dev: Update `magic_enum` to v0.9.5. (#4992) +- Dev: Replace `boost::optional` with `std::optional`. (#4877) +- Dev: Improve performance of selecting text. (#4889, #4911) +- Dev: Removed direct dependency on Qt 5 compatibility module. (#4906) +- Dev: Added unit test capabilities to SplitInput. (#5179) +- Dev: Refactor `Emoji`'s EmojiMap into a vector. (#4980) +- Dev: Refactor `DebugCount` and add copy button to debug popup. (#4921) +- Dev: Refactor `common/Credentials`. (#4979) +- Dev: Refactor chat logger. (#5058) +- Dev: Refactor Twitch PubSub client. (#5059) +- Dev: Changed lifetime of context menus. (#4924) +- Dev: Renamed `tools` directory to `scripts`. (#5035) +- Dev: Refactor `ChannelView`, removing a bunch of clang-tidy warnings. (#4926) +- Dev: Refactor `IrcMessageHandler`, removing a bunch of clang-tidy warnings & changing its public API. (#4927) +- Dev: Removed almost all raw accesses into Application. (#5104) +- Dev: `Details` file properties tab is now populated on Windows. (#4912) +- Dev: Removed `Outcome` from network requests. (#4959) +- Dev: Added Tests for Windows and MacOS in CI. (#4970, #5032) +- Dev: Added "Copy message as JSON" option when shift-right-clicking a message. (#5150) +- Dev: Windows now builds with Qt6 by default. (#5155) +- Dev: Conan now uses OpenSSL 3 by default. (#5159) +- Dev: Move `clang-tidy` checker to its own CI job. (#4996) +- Dev: Refactored the Image Uploader feature. (#4971) +- Dev: Refactored the SplitOverlay code. (#5082) +- Dev: Refactored the Fonts code, making it less of a singleton. (#5228) +- Dev: Refactored the TwitchBadges structure, making it less of a singleton. (#5096, #5144) +- Dev: Refactored emotes out of TwitchIrcServer. (#5120, #5146) +- Dev: Refactored the ChatterinoBadges structure, making it less of a singleton. (#5103) +- Dev: Refactored the ColorProvider class a bit. (#5112) +- Dev: Moved the Network files to their own folder. (#5089) +- Dev: Fixed deadlock and use-after-free in tests. (#4981) +- Dev: Moved all `.clang-format` files to the root directory. (#5037) +- Dev: Load less message history upon reconnects. (#5001, #5018) +- Dev: Removed the `NullablePtr` class. (#5091) +- Dev: BREAKING: Replace custom `import()` with normal Lua `require()`. (#5014, #5108) +- Dev: Compile Lua as a C library. (#5251) +- Dev: Fixed most compiler warnings. (#5028, #5137) +- Dev: Added the ability to show `ChannelView`s without a `Split`. (#4747) +- Dev: Refactor Args to be less of a singleton. (#5041) +- Dev: Channels without any animated elements on screen will skip updates from the GIF timer. (#5042, #5043, #5045) +- Dev: Autogenerate docs/plugin-meta.lua. (#5055, #5283) +- Dev: Changed Ubuntu & AppImage builders to statically link Qt. (#5151) +- Dev: Refactor `NetworkPrivate`. (#5063) +- Dev: Refactor `Paths` & `Updates`, focusing on reducing their singletoniability. (#5092, #5102) +- Dev: Removed duplicate scale in settings dialog. (#5069) +- Dev: Fix `NotebookTab` emitting updates for every message. (#5068) +- Dev: Added benchmark for parsing and building recent messages. (#5071) +- Dev: Boost is depended on as a header-only library when using conan. (#5107) +- Dev: Added signal to invalidate paint buffers of channel views without forcing a relayout. (#5123) +- Dev: Specialize `Atomic>` if underlying standard library supports it. (#5133) +- Dev: Added the `developer_name` field to the Linux AppData specification. (#5138) +- Dev: Twitch messages can be sent using Twitch's Helix API instead of IRC (disabled by default). (#5200, #5276) +- Dev: Added estimation for image sizes to avoid layout shifts. (#5192) +- Dev: Added the `launachable` entry to Linux AppData. (#5210) +- Dev: Cleaned up and optimized resources. (#5222) +- Dev: Refactor `StreamerMode`. (#5216, #5236) +- Dev: Cleaned up unused code in `MessageElement` and `MessageLayoutElement`. (#5225) +- Dev: Adapted `magic_enum` to Qt's Utf-16 strings. (#5258) +- Dev: `NetworkManager`'s statics are now created in its `init` method. (#5254, #5297) +- Dev: `clang-tidy` CI now uses Qt 6. (#5273) +- Dev: Enabled `InsertNewlineAtEOF` in `clang-format`. (#5278) + +## 2.4.6 + +- Minor: Migrate to the new Get Channel Followers Helix endpoint, fixing follower count not showing up in usercards. (#4809) +- Bugfix: Update Qt version, fixing a security issue with webp loading (see https://www.qt.io/blog/two-qt-security-advisorys-gdi-font-engine-webp-image-format) (#4843) +- Dev: Temporarily disable High DPI scaling on Qt6 builds on Windows. (#4767) + +## 2.4.5 + +- Major: AutoMod term management messages (e.g. testaccount added "noob" as a blocked term on AutoMod.) are now hidden in Streamer Mode if you have the "Hide moderation actions" setting enabled. (#4758) +- Minor: Added `/shoutout ` command to shoutout a specified user. Note: This is only the /command, we are still unable to display when a shoutout happens. (#4638) +- Minor: Added a setting to only show tabs with live channels (default toggle hotkey: Ctrl+Shift+L). (#4358) +- Minor: Added an option to subscribe to and unsubscribe from reply threads. (#4680, #4739) +- Minor: Added the ability to pin Reply threads to stay open while using the setting "Automatically close reply thread popup when it loses focus". (#4680) +- Minor: Highlights loaded from message history will now correctly appear in the /mentions tab. (#4475) +- Minor: Added hotkey Action for pinning usercards and reply threads. (#4692) +- Minor: Added missing hotkey Action for Open Player in Browser. (#4756) +- Minor: Added an icon showing when streamer mode is enabled (#4410, #4690) +- Minor: Message input is now focused when clicking on emotes. (#4719) +- Minor: Changed viewer list to chatter list to more match Twitch's terminology. (#4732) +- Minor: Added currency & duration to Hype Chat messages. (#4715) +- Minor: Added `is:hype-chat` search option. (#4766) +- Minor: Added `flags.hype_chat` filter variable. (#4766) +- Minor: Nicknames are now taken into consideration when searching for messages. (#4663, #4742) +- Minor: Added a message for when Chatterino joins a channel (#4616) +- Minor: 7TV badges now automatically update upon changing them. (#4512) +- Minor: Removed restriction on Go To Message on system messages from search. (#4614) +- Minor: Channel point redemptions without custom text are now shown in the usercard. (#4557) +- Minor: Added settings for customizing the behavior of `Right Click`ing a usernames. (#4622, #4751) +- Minor: The input completion and quick switcher are now styled to match your theme. (#4671) +- Minor: All channels opened in browser tabs are synced when using the extension for quicker switching between tabs. (#4741) +- Minor: Added support for opening incognito links in firefox-esr and chromium. (#4745) +- Minor: Added support for opening incognito links under Linux/BSD using XDG. (#4745) +- Minor: Added accelerators to the right click menu for messages (#4705) +- Minor: Improved editing hotkeys. (#4628) +- Minor: Added `/c2-theme-autoreload` command to automatically reload a custom theme. This is useful for when you're developing your own theme. (#4718) +- Bugfix: Fixed an issue where Subscriptions & Announcements that contained ignored phrases would still appear if the Block option was enabled. (#4748) +- Bugfix: Increased amount of blocked users loaded from 100 to 1,000. (#4721) +- Bugfix: Fixed pings firing for the "Your username" highlight when not signed in. (#4698) +- Bugfix: Fixed a crash that could happen when closing splits before their display name was updated. This was especially noticeable after the live controller changes. (#4731) +- Bugfix: Fixed highlights sometimes not working after changing sound device, or switching users in your operating system. (#4729) +- Bugfix: Fixed a spacing issue with mentions inside RTL text. (#4677) +- Bugfix: Fixed a crash when opening and closing a reply thread and switching the user. (#4675) +- Bugfix: Fixed a crash that could occur when closing the usercard too quickly after blocking or unblocking a user. (#4711) +- Bugfix: Fixed visual glitches with smooth scrolling. (#4501) +- Bugfix: Fixed key bindings not showing in context menus on Mac. (#4722) +- Bugfix: Fixed timeouts from message history not behaving consistently. (#4760) +- Bugfix: Fixed partially broken filters on Qt 6 builds. (#4702) +- Bugfix: Fixed tooltips & popups sometimes showing up on the wrong monitor. (#4740) +- Bugfix: Fixed some network errors having `0` as their HTTP status. (#4704) +- Bugfix: Fixed tab completion rarely completing the wrong word. (#4735) +- Bugfix: Fixed generation of crashdumps by the browser-extension process when the browser was closed. (#4667) +- Dev: Stream status requests are now batched. (#4713) +- Dev: Added command to set Qt's logging filter/rules at runtime (`/c2-set-logging-rules`). (#4637) +- Dev: Added the ability to see & load custom themes from the Themes directory. No stable promises are made of this feature, changes might be made that breaks custom themes without notice. (#4570) +- Dev: Added test cases for emote and tab completion. (#4644) +- Dev: Fixed `clang-tidy-review` action not picking up dependencies. (#4648) +- Dev: Expanded upon `$$$` test channels. (#4655) +- Dev: Added tools to help debug image GC. (#4578) +- Dev: Removed duplicate license when having plugins enabled. (#4665) +- Dev: Replace our QObjectRef class with Qt's QPointer class. (#4666) +- Dev: Fixed warnings about QWidgets already having a QLayout. (#4672) +- Dev: Fixed undefined behavior when loading non-existent credentials. (#4673) +- Dev: Small refactor of the recent-messages API, splitting its internal API and its internal implementation up into separate files. (#4763) +- Dev: Added support for compiling with `sccache`. (#4678) +- Dev: Added `sccache` in Windows CI. (#4678) +- Dev: Moved preprocessor Git and date definitions to executables only. (#4681) +- Dev: Refactored tests to be able to use `ctest` and run in debug builds. (#4700) +- Dev: Added the ability to use an alternate linker using the `-DUSE_ALTERNATE_LINKER=...` CMake parameter. (#4711) +- Dev: The Windows installer is now built in CI. (#4408) +- Dev: Removed `getApp` and `getSettings` calls from message rendering. (#4535) +- Dev: Get the default browser executable instead of the entire command line when opening incognito links. (#4745) +- Dev: Removed unused code hidden behind the USEWEBENGINE define (#4757) + +## 2.4.4 + +- Minor: Added a Send button in the input box so you can click to send a message. This is disabled by default and can be enabled with the "Show send message button" setting. (#4607) +- Minor: Improved error messages when the updater fails a download. (#4594) +- Minor: Added `/shield` and `/shieldoff` commands to toggle shield mode. (#4580) +- Bugfix: Fixed the menu warping on macOS on Qt6. (#4595) +- Bugfix: Fixed link tooltips not showing unless the thumbnail setting was enabled. (#4597) +- Bugfix: Domains starting with `http` are now parsed as links again. (#4598) +- Bugfix: Reduced the size of the update prompt to prevent it from going off the users screen. (#4626) +- Bugfix: Fixed click effects on buttons not being antialiased. (#4473) +- Bugfix: Fixed Ctrl+Backspace not working after Select All in chat search popup. (#4461) +- Bugfix: Fixed crash when scrolling up really fast. (#4621) +- Dev: Added the ability to control the `followRedirect` mode for requests. (#4594) + +## 2.4.3 + +- Major: Added support for FrankerFaceZ animated emotes. (#4434) +- Minor: Added the ability to reply to a message by `Shift + Right Click`ing the username. (#4424) +- Minor: Reply context now censors blocked users. (#4502) +- Minor: Migrated the viewer list to Helix API. (#4117) +- Minor: Migrated badges to Helix API. (#4537) +- Minor: Added `/lowtrust` command to open the suspicious user activity feed in browser. (#4542) +- Minor: Added better filter validation and error messages. (#4364) +- Minor: Updated the look of the Black Theme to be more in line with the other themes. (#4523) +- Minor: Re-added leading @mentions from replies in chat logs. These were accidentally removed during the reply overhaul. (#4420) +- Minor: Added a local backup of the Twitch Badges API in case the request fails. (#4463) +- Minor: Updated the macOS icon to be consistent with the design of other applications on macOS. (#4577) +- Bugfix: Fixed an issue where Chatterino could lose track of the sound device in certain scenarios. (#4549) +- Bugfix: Fixed an issue where animated emotes would render on top of zero-width emotes. (#4314) +- Bugfix: Fixed an issue where it was difficult to hover a zero-width emote. (#4314) +- Bugfix: Fixed an issue where context-menu items for zero-width emotes displayed the wrong provider. (#4460) +- Bugfix: Fixed an issue where the "Enable zero-width emotes" setting was showing the inverse state. (#4462) +- Bugfix: Fixed blocked user list being empty when opening the settings dialog for the first time. (#4437) +- Bugfix: Fixed blocked user list sticking around when switching from a logged in user to being logged out. (#4437) +- Bugfix: Fixed search popup ignoring setting for message scrollback limit. (#4496) +- Bugfix: Fixed a memory leak that occurred when loading message history. This was mostly noticeable with unstable internet connections where reconnections were frequent or long-running instances of Chatterino. (#4499) +- Bugfix: Fixed Twitch channel-specific filters not being applied correctly. (#4529) +- Bugfix: Fixed `/mods` displaying incorrectly when the channel has no mods. (#4546) +- Bugfix: Fixed emote & badge tooltips not showing up when thumbnails were hidden. (#4509) +- Bugfix: Fixed links with invalid IPv4 addresses being parsed. (#4576) +- Bugfix: Fixed the macOS icon changing to the wrong icon when the application is open. (#4577) +- Dev: Disabling precompiled headers on Windows is now tested in CI. (#4472) +- Dev: Themes are now stored as JSON files in `resources/themes`. (#4471, #4533) +- Dev: Ignore unhandled BTTV user-events. (#4438) +- Dev: Only log debug messages when NDEBUG is not defined. (#4442) +- Dev: Cleaned up theme related code. (#4450) +- Dev: Ensure tests have default-initialized settings. (#4498) +- Dev: Add scripting capabilities with Lua (#4341, #4504) +- Dev: Conan 2.0 is now used instead of Conan 1.0. (#4417) +- Dev: Added tests and benchmarks for `LinkParser`. (#4436) +- Dev: Removed redundant parsing of links. (#4507) +- Dev: Experimental builds with Qt 6 are now provided. (#4522, #4551, #4553, #4554, #4555, #4556) +- Dev: Fixed username rendering in Qt 6. (#4476, #4568) +- Dev: Fixed placeholder color in Qt 6. (#4477) +- Dev: Removed `CHATTERINO_TEST` definitions. (#4526) +- Dev: Builds for macOS now have `macos` in their name (previously: `osx`). (#4550) +- Dev: Fixed a crash when dragging rows in table-views in builds with Qt 6. (#4567) + +## 2.4.2 + +- Minor: Added `/banid` command that allows banning by user ID. (#4411) +- Bugfix: Fixed FrankerFaceZ emotes/badges not loading due to an API change. (#4432) +- Bugfix: Fixed uploaded AppImage not being able to execute most web requests. (#4400) +- Bugfix: Fixed a potential race condition due to using the wrong lock when loading 7TV badges. (#4402) +- Dev: Delete all but the last 5 crashdumps on application start. (#4392) +- Dev: Added capability to build Chatterino with Qt6. (#4393) +- Dev: Fixed homebrew update action. (#4394) + +## 2.4.1 + +- Major: Added live emote updates for BTTV. (#4147) +- Minor: Added setting to turn off rendering of reply context. (#4224) +- Minor: Changed the highlight order to prioritize Message highlights over User highlights. (#4303) +- Minor: Added a setting to highlight your own messages in `Highlights -> Users`. (#3833) +- Minor: Added the ability to negate search options by prefixing it with an exclamation mark (e.g. `!badge:mod` to search for messages where the author does not have the moderator badge). (#4207) +- Minor: Search window input will automatically use currently selected text if present. (#4178) +- Minor: Grouped highlight sound columns together and improved wording for the default sound setting. (#4194) +- Minor: Tables in settings window will now scroll to newly added rows. (#4216) +- Minor: Added setting to select which channels to log. (#4302) +- Minor: Added channel name to /mentions log entries. (#4371) +- Minor: Added link to streamlink docs for easier user setup. (#4217) +- Minor: Added support for HTTP and Socks5 proxies through environment variables. (#4321) +- Minor: Added crashpad to capture crashes on Windows locally. See PR for build/crash analysis instructions. (#4351) +- Minor: Github releases now include flatpakref files for nightly builds +- Bugfix: Fixed User Card moderation actions not working after Twitch IRC chat command deprecation. (#4378) +- Bugfix: Fixed User Card broadcaster actions (mod, unmod, vip, unvip) not working after Twitch IRC chat command deprecation. (#4387) +- Bugfix: Fixed crash that would occur when performing certain actions after removing all tabs. (#4271) +- Bugfix: Fixed highlight sounds not reloading on change properly. (#4194) +- Bugfix: Fixed CTRL + C not working in reply thread popups. (#4209) +- Bugfix: Fixed message input showing as red after removing a message that was more than 500 characters. (#4204) +- Bugfix: Fixed unnecessary saving of windows layout. (#4201) +- Bugfix: Fixed Reply window missing selection clear behaviour between chat and input box. (#4218) +- Bugfix: Fixed crash that could occur when changing Tab layout and utilizing multiple windows. (#4248) +- Bugfix: Fixed text sometimes not pasting properly when image uploader was disabled. (#4246) +- Bugfix: Fixed text cursor(caret) not showing in open channel dialog. (#4196) +- Bugfix: Fixed tooltip images not appearing if mouse hovered only first pixel. (#4268) +- Bugfix: Fixed crash that could occur when closing down a split at the wrong time. (#4277) +- Bugfix: Fixed crash that could occur when closing down the last of a channel when reloading emotes. (#4278) +- Bugfix: Fixed scrollbar highlight colors when changing message history limit. (#4288) +- Bugfix: Fixed the split "Search" menu action not opening the correct search window. (#4305) +- Bugfix: Fixed an issue on Windows when opening links in incognito mode that contained forward slashes in hash (#4307) +- Bugfix: Fixed an issue where beta versions wouldn't update to stable versions correctly. (#4329) +- Bugfix: Fixed builds from GitHub showing up as modified. (#4384) +- Bugfix: Avoided crash that could occur when receiving channel point reward information. (#4360) +- Dev: Changed sound backend from Qt to miniaudio. (#4334) +- Dev: Removed sending part of the multipart emoji workaround. (#4361) +- Dev: Removed protocol from QApplication's Organization Domain (so changed from `https://www.chatterino.com` to `chatterino.com`). (#4256) +- Dev: Ignore `WM_SHOWWINDOW` hide events, causing fewer attempted rescales. (#4198) +- Dev: Migrated to C++ 20 (#4252, #4257) +- Dev: Enable LTO for main branch builds. (#4258, #4260) +- Dev: Removed unused include directives. (#4266, #4275, #4294) +- Dev: Removed TooltipPreviewImage. (#4268) +- Dev: Removed unused operators in `Image` (#4267) +- Dev: Removed usage of deprecated `QDesktopWidget` (#4287) +- Dev: Bump Cirrus CI FreeBSD image from 12.1 to 13.1. (#4295) +- Dev: Fixed `inconsistent-missing-override` warnings. (#4296) +- Dev: Fixed `final-dtor-non-final-class` warnings. (#4296) +- Dev: Fixed `ambiguous-reversed-operator` warnings. (#4296) +- Dev: Format YAML and JSON files with prettier. (#4304) +- Dev: Added CMake Install Support on Windows. (#4300) +- Dev: Changed conan generator to [`CMakeDeps`](https://docs.conan.io/en/latest/reference/conanfile/tools/cmake/cmakedeps.html) and [`CMakeToolchain`](https://docs.conan.io/en/latest/reference/conanfile/tools/cmake/cmaketoolchain.html). See PR for migration notes. (#4335) +- Dev: Refactored 7TV EventAPI implementation. (#4342) +- Dev: Disabled ImageExpirationPool in tests. (#4363) +- Dev: Don't rely on undocumented registry keys to find the default browser on Windows. (#4362) +- Dev: Use `QEnterEvent` for `QWidget::enterEvent` on Qt 6. (#4365) +- Dev: Use `qintptr` in `QWidget::nativeEvent` on Qt 6. (#4376) + +## 2.4.0 + +- Major: Added support for emotes, badges, and live emote updates from [7TV](https://7tv.app). [Wiki Page](https://wiki.chatterino.com/Third_party_services/#7tv) (#4002, #4062, #4090) +- Major: Added support for Right-to-Left Languages (#3958, #4139, #4168) +- Major: Added support for Twitch's Chat Replies. [Wiki Page](https://wiki.chatterino.com/Features/#message-replies) (#3722, #3989, #4041, #4047, #4055, #4067, #4077, #3905, #4131) - Major: Added multi-channel searching to search dialog via keyboard shortcut. (Ctrl+Shift+F by default) (#3694, #3875) -- Minor: Added option to display tabs on the right and bottom. (#3847) -- Minor: Added `is:first-msg` search option. (#3700) -- Minor: Added quotation marks in the permitted/blocked Automod messages for clarity. (#3654) -- Minor: Added a `Scroll to top` keyboard shortcut for splits. (#3802) -- Minor: Adjust large stream thumbnail to 16:9 (#3655) -- Minor: Fixed being unable to load Twitch Usercards from the `/mentions` tab. (#3623) -- Minor: Add information about the user's operating system in the About page. (#3663) -- Minor: Added chatter count for each category in viewer list. (#3683, #3719) -- Minor: Sorted usernames in /vips message to be case-insensitive. (#3696) -- Minor: Strip leading @ and trailing , from usernames in the `/block` and `/unblock` commands. (#3816) -- Minor: Added option to open a user's chat in a new tab from the usercard profile picture context menu. (#3625) -- Minor: Fixed tag parsing for consecutive escaped characters. (#3711) -- Minor: Prevent user from entering incorrect characters in Live Notifications channels list. (#3715, #3730) -- Minor: Fixed automod caught message notice appearing twice for mods. (#3717) -- Minor: Streamer mode now automatically detects if XSplit, PRISM Live Studio, Twitch Studio, or vMix are running. (#3740) -- Minor: Add scrollbar to `Select filters` dialog. (#3737) -- Minor: Added `/requests` command. Usage: `/requests [channel]`. Opens the channel points requests queue for the provided channel or the current channel if no input is provided. (#3746) -- Minor: Added ability to execute commands on chat messages using the message context menu. (#3738, #3765) -- Minor: Added `/copy` command. Usage: `/copy `. Copies provided text to clipboard - can be useful with custom commands. (#3763) +- Minor: Added setting to keep more message history in splits. (#3811) +- Minor: Added setting to keep more message history in usercards. (#3811) +- Minor: Added ability to pin Usercards to stay open even if it loses focus. Only available if "Automatically close usercard when it loses focus" is enabled. (#3884) +- Minor: Allow hiding moderation actions in streamer mode. (#3926) +- Minor: Added highlights for `Elevated Messages`. (#4016) - Minor: Removed total views from the usercard, as Twitch no longer updates the number. (#3792) -- Minor: Add Quick Switcher item to open a channel in a new popup window. (#3828) -- Minor: Warn when parsing an environment variable fails. (#3904) - Minor: Load missing messages from Recent Messages API upon reconnecting (#3878, #3932) -- Minor: Add settings to toggle BTTV/FFZ global/channel emotes (#3935) -- Bugfix: Fix crash that can occur when closing and quickly reopening a split, then running a command. (#3852) +- Minor: Reduced image memory usage when running Chatterino for a long time. (#3915) +- Minor: Added the ability to execute commands on chat messages using the message context menu. (#3738, #3765) +- Minor: Added settings to toggle BTTV/FFZ global/channel emotes (#3935, #3990) +- Minor: Added an option to display tabs on the right and bottom. (#3847) +- Minor: Added a `Scroll to top` keyboard shortcut for splits. (#3802) +- Minor: Added `/copy` command. Usage: `/copy `. Copies provided text to clipboard - can be useful with custom commands. (#3763) +- Minor: Added `/requests` command. Usage: `/requests [channel]`. Opens the channel points requests queue for the provided channel or the current channel if no input is provided. (#3746) +- Minor: Added `Go to message` context menu action to search popup, mentions, usercard and reply threads. (#3953) +- Minor: Clicking `A message from x was deleted` messages will now jump to the message in question. (#3953) +- Minor: Added `is:first-msg` search option. (#3700) +- Minor: Added `is:elevated-msg` search option. (#4018) +- Minor: Added `is:cheer-msg` search option. (#4069) +- Minor: Added `is:redemption` search option. (#4118) +- Minor: Added `is:reply` search option. (#4119) +- Minor: Added `subtier:` search option (e.g. `subtier:3` to find Tier 3 subs). (#4013) +- Minor: Added `badge:` search option (e.g. `badge:mod` to users with the moderator badge). (#4013) +- Minor: Added AutoMod message flag filter. (#3938) +- Minor: Added `showInMentions` toggle for Badge Highlights. (#4034) +- Minor: Added chatter count for each category in viewer list. (#3683, #3719) +- Minor: Added option to open a user's chat in a new tab from the usercard profile picture context menu. (#3625) +- Minor: Added scrollbar to `Select filters` dialog. (#3737) +- Minor: Added quotation marks in the permitted/blocked Automod messages for clarity. (#3654) +- Minor: Added Quick Switcher item to open a channel in a new popup window. (#3828) +- Minor: Added information about the user's operating system in the About page. (#3663) +- Minor: Added option to hide inline whispers in streamer mode (#4076) +- Minor: Adjusted large stream thumbnail to 16:9 (#3655) +- Minor: Prevented user from entering incorrect characters in Live Notifications channels list. (#3715, #3730) +- Minor: Sorted usernames in /vips message to be case-insensitive. (#3696) +- Minor: Streamer mode now automatically detects if XSplit, PRISM Live Studio, Twitch Studio, or vMix are running. (#3740) +- Minor: Fixed automod caught message notice appearing twice for mods. (#3717) +- Minor: Fixed being unable to load Twitch Usercards from the `/mentions` tab. (#3623) +- Minor: Strip leading @ and trailing , from usernames in the `/block` and `/unblock` commands. (#3816) +- Minor: Fixed tag parsing for consecutive escaped characters. (#3711) +- Minor: Reduced GIF frame window from 30ms to 20ms, causing fewer frame skips in animated emotes. (#3886, #3907) +- Minor: Warn when parsing an environment variable fails. (#3904) +- Minor: Migrated /announce command to Helix API. (#4003) +- Minor: Migrated /clear command to Helix API. (#3994) +- Minor: Migrated /color command to Helix API. (#3988) +- Minor: Migrated /delete command to Helix API. (#3999) +- Minor: Migrated /emoteonly command to Helix API. (#4015) +- Minor: Migrated /emoteonlyoff command to Helix API. (#4015) +- Minor: Migrated /mod command to Helix API. (#4000) +- Minor: Migrated /unmod command to Helix API. (#4001) +- Minor: Migrated /vip command to Helix API. (#4010) +- Minor: Migrated /unvip command to Helix API. (#4025) +- Minor: Migrated /untimeout to Helix API. (#4026) +- Minor: Migrated /unban to Helix API. (#4026, #4050) +- Minor: Migrated /subscribers to Helix API. (#4040) +- Minor: Migrated /subscribersoff to Helix API. (#4040) +- Minor: Migrated /slow to Helix API. (#4040) +- Minor: Migrated /slowoff to Helix API. (#4040) +- Minor: Migrated /followers to Helix API. (#4040) +- Minor: Migrated /followersoff to Helix API. (#4040) +- Minor: Migrated /raid command to Helix API. Chat command will continue to be used until February 11th 2023. (#4029) +- Minor: Migrated /unraid command to Helix API. Chat command will continue to be used until February 11th 2023. (#4030) +- Minor: Migrated /ban to Helix API. (#4049, #4164) +- Minor: Migrated /timeout to Helix API. (#4049, #4164) +- Minor: Migrated /w to Helix API. Chat command will continue to be used until February 11th 2023. (#4052) +- Minor: Migrated /vips to Helix API. Chat command will continue to be used until February 11th 2023. (#4053) +- Minor: Migrated /uniquechat and /r9kbeta to Helix API. (#4057) +- Minor: Migrated /uniquechatoff and /r9kbetaoff to Helix API. (#4057) +- Minor: Migrated /commercial to Helix API. (#4094, #4141) +- Minor: Added stream titles to windows live toast notifications. (#1297) +- Minor: Make menus and placeholders display appropriate custom key combos. (#4045) +- Minor: Migrated /chatters to Helix API. (#4088, #4097, #4114) +- Minor: Migrated /mods to Helix API. (#4103) +- Minor: Improved text selection to match Windows native behaviour. (#4127) +- Minor: Add settings tooltips. (#3437) +- Minor: Add setting to limit message input length. (#3418) +- Minor: Make built-in commands work in IRC channels. (#4160) +- Minor: Add support for `echo-message` capabilities for IRC. (#4157) +- Minor: Add proper support for IRC private messages. (#4158) +- Minor: Improved look of tabs when using a layout other than top. (#3925, #4152) +- Minor: Added support for Nicknames on IRC. (#4170) +- Bugfix: Fixed crash happening when QuickSwitcher is used with a popout window. (#4187) +- Bugfix: Fixed low contrast of text in settings tooltips. (#4188) +- Bugfix: Fixed being unable to see the usercard of VIPs who have Asian language display names. (#4174) +- Bugfix: Fixed whispers always being shown in the /mentions split. (#4389) +- Bugfix: Fixed messages where Right-to-Left order is mixed in multiple lines. (#4173) +- Bugfix: Fixed the wrong right-click menu showing in the chat input box. (#4177) +- Bugfix: Fixed popup windows not appearing/minimizing correctly on the Windows taskbar. (#4181) +- Bugfix: Fixed white border appearing around maximized window on Windows. (#4190) +- Bugfix: Fixed window scaling being applied too many times on startup, causing windows like Settings to be slow. (#4193) +- Bugfix: Fixed input text cursor flickering when selecting text in a split. (#4197) +- Bugfix: Fixed shipped resources having incorrect ICC profile (#4199) +- Bugfix: Fixed channels with two leading `#`s not being usable on IRC (#4154) +- Bugfix: Fixed `Add new account` dialog causing main chatterino window to be non movable. (#4121) - Bugfix: Connection to Twitch PubSub now recovers more reliably. (#3643, #3716) -- Bugfix: Fix crash that can occur when changing channels. (#3799) +- Bugfix: Fixed `Smooth scrolling on new messages` setting sometimes hiding messages. (#4028) +- Bugfix: Fixed context menu not opening when username is right clicked from usercard/search/reply window. (#4122) +- Bugfix: Fixed a crash that can occur when closing and quickly reopening a split, then running a command. (#3852) +- Bugfix: Fixed a crash that can occur when changing channels. (#3799) - Bugfix: Fixed viewers list search not working when used before loading finishes. (#3774) - Bugfix: Fixed live notifications for usernames containing uppercase characters. (#3646) - Bugfix: Fixed live notifications not getting updated for closed streams going offline. (#3678) @@ -41,21 +618,37 @@ - Bugfix: Fixed existing emote popups not being raised from behind other windows when refocusing them on macOS (#3713) - Bugfix: Fixed automod queue pubsub topic persisting after user change. (#3718) - Bugfix: Fixed viewer list not closing after pressing escape key. (#3734) +- Bugfix: Fixed users being assigned duplicate FrankerFaceZ badges. (#4155) - Bugfix: Fixed links with no thumbnail having previous link's thumbnail. (#3720) - Bugfix: Fixed message only showing a maximum of one global FrankerFaceZ badge even if the user has multiple. (#3818) -- Bugfix: Add icon in the CMake macOS bundle. (#3832) -- Bugfix: Adopt popup windows in order to force floating behavior on some window managers. (#3836) -- Bugfix: Fix split focusing being broken in certain circumstances when the "Show input when it's empty" setting was disabled. (#3838, #3860) +- Bugfix: Added an icon in the CMake macOS bundle. (#3832) +- Bugfix: Adopted popup windows in order to force floating behavior on some window managers. (#3836) +- Bugfix: Fixed split focusing being broken in certain circumstances when the "Show input when it's empty" setting was disabled. (#3838, #3860) - Bugfix: Always refresh tab when a contained split's channel is set. (#3849) -- Bugfix: Drop trailing whitespace from Twitch system messages. (#3888) -- Bugfix: Fix crash related to logging IRC channels (#3918) +- Bugfix: Fixed an issue where Anonymous gift messages appeared larger than normal gift messages. (#3888) +- Bugfix: Fixed crash related to logging IRC channels (#3918) - Bugfix: Mentions of "You" in timeouts will link to your own user now instead of the user "You". (#3922) -- Dev: Remove official support for QMake. (#3839, #3883) -- Dev: Rewrite LimitedQueue (#3798) -- Dev: Overhaul highlight system by moving all checks into a Controller allowing for easier tests. (#3399, #3801, #3835) +- Bugfix: Fixed emoji popup not being shown in IRC channels (#4021) +- Bugfix: Display sent IRC messages like received ones (#4027) +- Bugfix: Fixed non-global FrankerFaceZ emotes from being loaded as global emotes. (#3921) +- Bugfix: Fixed trailing spaces from preventing Nicknames from working correctly. (#3946) +- Bugfix: Fixed crashes that can occur while selecting/copying messages and they are removed. (#4153) +- Bugfix: Fixed trailing spaces from preventing User Highlights from working correctly. (#4051) +- Bugfix: Fixed channel-based popups from rewriting messages to file log (#4060) +- Bugfix: Fixed invalid/dangling completion when cycling through previous messages or replying (#4072) +- Bugfix: Fixed incorrect .desktop icon path. (#4078) +- Bugfix: Mark bad or invalid images as empty. (#4151) +- Bugfix: Fixed `/watching` channel jumping around. (#4169) +- Dev: Got rid of BaseTheme (#4132) +- Dev: Removed official support for QMake. (#3839, #3883) +- Dev: Rewrote LimitedQueue (#3798) +- Dev: Set cmake `QT_DISABLE_DEPRECATED_BEFORE` to disable deprecated APIs up to Qt 5.15.0 (#4133) +- Dev: Overhauled highlight system by moving all checks into a Controller allowing for easier tests. (#3399, #3801, #3835) - Dev: Use Game Name returned by Get Streams instead of querying it from the Get Games API. (#3662) -- Dev: Batch checking live status for all channels after startup. (#3757, #3762, #3767) -- Dev: Move most command context into the command controller. (#3824) +- Dev: Batched checking live status for all channels after startup. (#3757, #3762, #3767) +- Dev: Moved most command context into the command controller. (#3824) +- Dev: Error NetworkResults now include the body data. (#3987) +- Dev: Automatically generate resources files with cmake. (#4159, #4167) ## 2.3.5 @@ -82,6 +675,8 @@ - Minor: Fixed `/streamlink` command not stripping leading @'s or #'s (#3215) - Minor: Strip leading @ and trailing , from username in `/popout` command. (#3217) - Minor: Added `flags.reward_message` filter variable (#3231) +- Minor: Added `flags.elevated_message` filter variable. (#4017) +- Minor: Added `flags.cheer_message` filter variable. (#4069) - Minor: Added chatter count to viewer list popout (#3261) - Minor: Ignore out of bounds check for tiling wms (#3270) - Minor: Add clear cache button to cache settings section (#3277) diff --git a/CMakeLists.txt b/CMakeLists.txt index 571bb1637..6fb323286 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,6 @@ -cmake_minimum_required(VERSION 3.8) -cmake_policy(SET CMP0087 NEW) +cmake_minimum_required(VERSION 3.15) +cmake_policy(SET CMP0087 NEW) # evaluates generator expressions in `install(CODE/SCRIPT)` +cmake_policy(SET CMP0091 NEW) # select MSVC runtime library through `CMAKE_MSVC_RUNTIME_LIBRARY` include(FeatureSummary) list(APPEND CMAKE_MODULE_PATH @@ -7,8 +8,6 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/sanitizers-cmake/cmake" ) -project(chatterino VERSION 2.3.5) - option(BUILD_APP "Build Chatterino" ON) option(BUILD_TESTS "Build the tests for Chatterino" OFF) option(BUILD_BENCHMARKS "Build the benchmarks for Chatterino" OFF) @@ -16,10 +15,44 @@ option(USE_SYSTEM_PAJLADA_SETTINGS "Use system pajlada settings library" OFF) option(USE_SYSTEM_LIBCOMMUNI "Use system communi library" OFF) option(USE_SYSTEM_QTKEYCHAIN "Use system QtKeychain library" OFF) option(BUILD_WITH_QTKEYCHAIN "Build Chatterino with support for your system key chain" ON) +option(USE_SYSTEM_MINIAUDIO "Build Chatterino with your system miniaudio" OFF) +option(BUILD_WITH_CRASHPAD "Build chatterino with crashpad" OFF) option(USE_PRECOMPILED_HEADERS "Use precompiled headers" ON) -option(BUILD_WITH_QT6 "Use Qt6 instead of default Qt5" OFF) +if(WIN32) + option(BUILD_WITH_QT6 "Build with Qt6, default on for Windows" On) +else() + option(BUILD_WITH_QT6 "Use Qt6 instead of default Qt5" OFF) +endif() +option(CHATTERINO_GENERATE_COVERAGE "Generate coverage files" OFF) +# We don't use translations, and we don't want qtkeychain to build translations +option(BUILD_TRANSLATIONS "" OFF) +option(BUILD_SHARED_LIBS "" OFF) +option(CHATTERINO_LTO "Enable LTO for all targets" OFF) +option(CHATTERINO_PLUGINS "Enable ALPHA plugin support in Chatterino" OFF) -option(USE_CONAN "Use conan" OFF) +option(CHATTERINO_UPDATER "Enable update checks" ON) +mark_as_advanced(CHATTERINO_UPDATER) + +if(BUILD_TESTS) + list(APPEND VCPKG_MANIFEST_FEATURES "tests") +endif() +if(BUILD_BENCHMARKS) + list(APPEND VCPKG_MANIFEST_FEATURES "benchmarks") +endif() + +project(chatterino + VERSION 2.5.1 + DESCRIPTION "Chat client for twitch.tv" + HOMEPAGE_URL "https://chatterino.com/" +) + +if(CHATTERINO_LTO) + include(CheckIPOSupported) + check_ipo_supported(RESULT CHATTERINO_ENABLE_LTO OUTPUT IPO_ERROR) + message(STATUS "LTO: Enabled (Supported: ${CHATTERINO_ENABLE_LTO} - ${IPO_ERROR})") +else() + message(STATUS "LTO: Disabled") +endif() if (BUILD_WITH_QT6) set(MAJOR_QT_VERSION "6") @@ -27,18 +60,51 @@ else() set(MAJOR_QT_VERSION "5") endif() -if (USE_CONAN OR CONAN_EXPORTED) - include(${CMAKE_CURRENT_BINARY_DIR}/conanbuildinfo.cmake) - conan_basic_setup(TARGETS NO_OUTPUT_DIRS) -else () - set(QT_CREATOR_SKIP_CONAN_SETUP ON) +find_program(CCACHE_PROGRAM ccache) +find_program(SCCACHE_PROGRAM sccache) +if (SCCACHE_PROGRAM) + set(_compiler_launcher ${SCCACHE_PROGRAM}) +elseif (CCACHE_PROGRAM) + set(_compiler_launcher ${CCACHE_PROGRAM}) +endif () + + +# Alternate linker code taken from heavyai/heavydb +# https://github.com/heavyai/heavydb/blob/0517d99b467806f6af7b4c969e351368a667497d/CMakeLists.txt#L87-L103 +macro(set_alternate_linker linker) + find_program(LINKER_EXECUTABLE ld.${USE_ALTERNATE_LINKER} ${USE_ALTERNATE_LINKER}) + if(LINKER_EXECUTABLE) + if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang" AND "${CMAKE_CXX_COMPILER_VERSION}" VERSION_LESS 12.0.0) + add_link_options("-ld-path=${USE_ALTERNATE_LINKER}") + else() + add_link_options("-fuse-ld=${USE_ALTERNATE_LINKER}") + endif() + else() + set(USE_ALTERNATE_LINKER "" CACHE STRING "Use alternate linker" FORCE) + endif() +endmacro() + +set(USE_ALTERNATE_LINKER "" CACHE STRING "Use alternate linker. Leave empty for system default; alternatives are 'gold', 'lld', 'bfd', 'mold'") +if(NOT "${USE_ALTERNATE_LINKER}" STREQUAL "") + set_alternate_linker(${USE_ALTERNATE_LINKER}) endif() -find_program(CCACHE_PROGRAM ccache) -if (CCACHE_PROGRAM) - set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}") - message("Using ${CCACHE_PROGRAM} for speeding up build") -endif () +if (_compiler_launcher) + set(CMAKE_CXX_COMPILER_LAUNCHER "${_compiler_launcher}" CACHE STRING "CXX compiler launcher") + message(STATUS "Using ${_compiler_launcher} for speeding up build") + + if (MSVC) + # /Zi can't be used with (s)ccache + # Use /Z7 instead (debug info in object files) + if(CMAKE_BUILD_TYPE STREQUAL "Debug") + string(REPLACE "/Zi" "/Z7" CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG}") + elseif(CMAKE_BUILD_TYPE STREQUAL "Release") + string(REPLACE "/Zi" "/Z7" CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE}") + elseif(CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo") + string(REPLACE "/Zi" "/Z7" CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO}") + endif() + endif() +endif() include(${CMAKE_CURRENT_LIST_DIR}/cmake/GIT.cmake) @@ -48,20 +114,20 @@ find_package(Qt${MAJOR_QT_VERSION} REQUIRED Widgets Gui Network - Multimedia Svg Concurrent ) +message(STATUS "Qt version: ${Qt${MAJOR_QT_VERSION}_VERSION}") + if (WIN32) - find_package(WinToast REQUIRED) + add_subdirectory(lib/WinToast EXCLUDE_FROM_ALL) endif () -find_package(Sanitizers) +find_package(Sanitizers QUIET) # Find boost on the system -find_package(Boost REQUIRED) -find_package(Boost COMPONENTS random) +find_package(Boost REQUIRED OPTIONAL_COMPONENTS headers) # Find OpenSSL on the system find_package(OpenSSL REQUIRED) @@ -83,7 +149,6 @@ endif() if (BUILD_WITH_QTKEYCHAIN) # Link QtKeychain statically - option(QTKEYCHAIN_STATIC "" ON) if (USE_SYSTEM_QTKEYCHAIN) find_package(Qt${MAJOR_QT_VERSION}Keychain REQUIRED) else() @@ -104,12 +169,17 @@ find_package(RapidJSON REQUIRED) find_package(Websocketpp REQUIRED) if (BUILD_TESTS) + include(GoogleTest) + # For MSVC: Prevent overriding the parent project's compiler/linker settings + # See https://github.com/google/googletest/blob/main/googletest/README.md#visual-studio-dynamic-vs-static-runtimes + set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) + add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/lib/googletest" "lib/googletest") mark_as_advanced( - BUILD_GMOCK BUILD_GTEST BUILD_SHARED_LIBS - gmock_build_tests gtest_build_samples gtest_build_tests - gtest_disable_pthreads gtest_force_shared_crt gtest_hide_internal_symbols + BUILD_GMOCK BUILD_GTEST BUILD_SHARED_LIBS + gmock_build_tests gtest_build_samples gtest_build_tests + gtest_disable_pthreads gtest_force_shared_crt gtest_hide_internal_symbols ) set_target_properties(gtest PROPERTIES FOLDER lib) @@ -127,6 +197,7 @@ find_package(PajladaSerialize REQUIRED) find_package(PajladaSignals REQUIRED) find_package(LRUCache REQUIRED) find_package(MagicEnum REQUIRED) +find_package(Doxygen) if (USE_SYSTEM_PAJLADA_SETTINGS) find_package(PajladaSettings REQUIRED) @@ -138,15 +209,37 @@ else() add_subdirectory("${CMAKE_SOURCE_DIR}/lib/settings" EXCLUDE_FROM_ALL) endif() -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) +if (CHATTERINO_PLUGINS) + set(LUA_INCLUDE_DIRS "${CMAKE_SOURCE_DIR}/lib/lua/src") + add_subdirectory(lib/lua) +endif() -if (BUILD_TESTS OR BUILD_BENCHMARKS) - add_definitions(-DCHATTERINO_TEST) +if (BUILD_WITH_CRASHPAD) + add_subdirectory("${CMAKE_SOURCE_DIR}/tools/crash-handler") +endif() + +# Used to provide a date of build in the About page (for nightly builds). Getting the actual time of +# compilation in CMake is a more involved, as documented in https://stackoverflow.com/q/24292898. +# For CI runs, however, the date of build file generation should be consistent with the date of +# compilation so this approximation is "good enough" for our purpose. +if (DEFINED ENV{CHATTERINO_SKIP_DATE_GEN}) + set(cmake_gen_date "1970-01-01") +else () + string(TIMESTAMP cmake_gen_date "%Y-%m-%d") endif () +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Generate resource files +include(cmake/resources/generate_resources.cmake) + add_subdirectory(src) +if (BUILD_TESTS OR BUILD_BENCHMARKS) + add_subdirectory(mocks) +endif () + if (BUILD_TESTS) enable_testing() add_subdirectory(tests) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index af5339196..52bdd8f49 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,6 +2,10 @@ This is a set of guidelines for contributing to Chatterino. The goal is to teach programmers without a C++ background (java/python/etc.), people who haven't used Qt, or otherwise have different experience, the idioms of the codebase. Thus we will focus on those which are different from those other environments. There are extra guidelines available [here](https://hackmd.io/@fourtf/chatterino-pendantic-guidelines) but they are considered as extras and not as important. +### General (non-code related) guidelines for contributing to Chatterino + +- Make a specific branch for your pull request instead of using the master, main, or mainline branch. This will prevent future problems with updating your branch after your PR is merged. + # Tooling ## Formatting @@ -31,7 +35,7 @@ int compare(const QString &a, const QString &b); ```cpp /* - * Matches a link and returns boost::none if it failed and a + * Matches a link and returns std::nullopt if it failed and a * QRegularExpressionMatch on success. * ^^^ This comment just repeats the function signature!!! * @@ -39,7 +43,7 @@ int compare(const QString &a, const QString &b); * link * ^^^ No need to repeat the obvious. */ -boost::optional matchLink(const QString &text); +std::optional matchLink(const QString &text); ``` # Code diff --git a/QtCreatorPackageManager.cmake b/QtCreatorPackageManager.cmake new file mode 100644 index 000000000..f26a970c8 --- /dev/null +++ b/QtCreatorPackageManager.cmake @@ -0,0 +1,5 @@ +# https://www.qt.io/blog/qt-creator-cmake-package-manager-auto-setup + +# set(QT_CREATOR_SKIP_PACKAGE_MANAGER_SETUP ON) # skip both conan and vcpkg auto-setups +# set(QT_CREATOR_SKIP_CONAN_SETUP ON) # skip conan auto-setup +set(QT_CREATOR_SKIP_VCPKG_SETUP ON) # skip vcpkg auto-setup diff --git a/README.md b/README.md index 675abe2a8..0a046198d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![alt text](https://fourtf.com/img/chatterino-icon-64.png) +![chatterinoLogo](https://user-images.githubusercontent.com/41973452/272541622-52457e89-5f16-4c83-93e7-91866c25b606.png) Chatterino 2 [![GitHub Actions Build (Windows, Ubuntu, MacOS)](https://github.com/Chatterino/chatterino2/workflows/Build/badge.svg?branch=master)](https://github.com/Chatterino/chatterino2/actions?query=workflow%3ABuild+branch%3Amaster) [![Cirrus CI Build (FreeBSD only)](https://api.cirrus-ci.com/github/Chatterino/chatterino2.svg?branch=master)](https://cirrus-ci.com/github/Chatterino/chatterino2/master) [![Chocolatey Package](https://img.shields.io/chocolatey/v/chatterino?include_prereleases)](https://chocolatey.org/packages/chatterino) [![Flatpak Package](https://img.shields.io/flathub/v/com.chatterino.chatterino)](https://flathub.org/apps/details/com.chatterino.chatterino) ============ @@ -22,43 +22,40 @@ If you still receive an error about `MSVCR120.dll missing`, then you should inst To get source code with required submodules run: -``` +```shell git clone --recurse-submodules https://github.com/Chatterino/chatterino2.git ``` or -``` +```shell git clone https://github.com/Chatterino/chatterino2.git cd chatterino2 git submodule update --init --recursive ``` -[Building on Windows](../master/BUILDING_ON_WINDOWS.md) +- [Building on Windows](../master/BUILDING_ON_WINDOWS.md) +- [Building on Windows with vcpkg](../master/BUILDING_ON_WINDOWS_WITH_VCPKG.md) +- [Building on Linux](../master/BUILDING_ON_LINUX.md) +- [Building on macOS](../master/BUILDING_ON_MAC.md) +- [Building on FreeBSD](../master/BUILDING_ON_FREEBSD.md) -[Building on Windows with vcpkg](../master/BUILDING_ON_WINDOWS_WITH_VCPKG.md) +## Git blame -[Building on Linux](../master/BUILDING_ON_LINUX.md) +This project has big commits in the history which touch most files while only doing stylistic changes. To improve the output of git-blame, consider setting: -[Building on Mac](../master/BUILDING_ON_MAC.md) +```shell +git config blame.ignoreRevsFile .git-blame-ignore-revs +``` -[Building on FreeBSD](../master/BUILDING_ON_FREEBSD.md) +This will ignore all revisions mentioned in the [`.git-blame-ignore-revs` +file](./.git-blame-ignore-revs). GitHub does this by default. ## Code style -The code is formatted using clang format in Qt Creator. [.clang-format](src/.clang-format) contains the style file for clang format. +The code is formatted using [clang-format](https://clang.llvm.org/docs/ClangFormat.html). Our configuration is found in the [.clang-format](.clang-format) file in the repository root directory. -### Get it automated with QT Creator + Beautifier + Clang Format - -1. Download LLVM: https://github.com/llvm/llvm-project/releases/download/llvmorg-11.0.0/LLVM-11.0.0-win64.exe -2. During the installation, make sure to add it to your path -3. In QT Creator, select `Help` > `About Plugins` > `C++` > `Beautifier` to enable the plugin -4. Restart QT Creator -5. Select `Tools` > `Options` > `Beautifier` -6. Under `General` select `Tool: ClangFormat` and enable `Automatic Formatting on File Save` -7. Under `Clang Format` select `Use predefined style: File` and `Fallback style: None` - -Qt creator should now format the documents when saving it. +For more contribution guidelines, take a look at [the wiki](https://wiki.chatterino.com/Contributing%20for%20Developers/). ## Doxygen diff --git a/benchmarks/.clang-format b/benchmarks/.clang-format deleted file mode 100644 index f34c1465b..000000000 --- a/benchmarks/.clang-format +++ /dev/null @@ -1,35 +0,0 @@ -Language: Cpp - -AccessModifierOffset: -4 -AlignEscapedNewlinesLeft: true -AllowShortFunctionsOnASingleLine: false -AllowShortIfStatementsOnASingleLine: false -AllowShortLambdasOnASingleLine: Empty -AllowShortLoopsOnASingleLine: false -AlwaysBreakAfterDefinitionReturnType: false -AlwaysBreakBeforeMultilineStrings: false -BasedOnStyle: Google -BraceWrapping: { - AfterClass: 'true' - AfterControlStatement: 'true' - AfterFunction: 'true' - AfterNamespace: 'false' - BeforeCatch: 'true' - BeforeElse: 'true' -} -BreakBeforeBraces: Custom -BreakConstructorInitializersBeforeComma: true -ColumnLimit: 80 -ConstructorInitializerAllOnOneLineOrOnePerLine: false -DerivePointerBinding: false -FixNamespaceComments: true -IndentCaseLabels: true -IndentWidth: 4 -IndentWrappedFunctionNames: true -IndentPPDirectives: AfterHash -IncludeBlocks: Preserve -NamespaceIndentation: Inner -PointerBindsToType: false -SpacesBeforeTrailingComments: 2 -Standard: Auto -ReflowComments: false diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt index 1bc975fd0..a0e73332e 100644 --- a/benchmarks/CMakeLists.txt +++ b/benchmarks/CMakeLists.txt @@ -1,12 +1,16 @@ project(chatterino-benchmark) set(benchmark_SOURCES - ${CMAKE_CURRENT_LIST_DIR}/src/main.cpp - ${CMAKE_CURRENT_LIST_DIR}/src/Emojis.cpp - ${CMAKE_CURRENT_LIST_DIR}/src/Highlights.cpp - ${CMAKE_CURRENT_LIST_DIR}/src/FormatTime.cpp - ${CMAKE_CURRENT_LIST_DIR}/src/Helpers.cpp - ${CMAKE_CURRENT_LIST_DIR}/src/LimitedQueue.cpp + src/main.cpp + resources/bench.qrc + + src/Emojis.cpp + src/Highlights.cpp + src/FormatTime.cpp + src/Helpers.cpp + src/LimitedQueue.cpp + src/LinkParser.cpp + src/RecentMessages.cpp # Add your new file above this line! ) @@ -14,13 +18,10 @@ add_executable(${PROJECT_NAME} ${benchmark_SOURCES}) add_sanitizers(${PROJECT_NAME}) target_link_libraries(${PROJECT_NAME} PRIVATE chatterino-lib) +target_link_libraries(${PROJECT_NAME} PRIVATE chatterino-mocks) target_link_libraries(${PROJECT_NAME} PRIVATE benchmark::benchmark) -target_compile_definitions(${PROJECT_NAME} PRIVATE - CHATTERINO_TEST - ) - set_target_properties(${PROJECT_NAME} PROPERTIES ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" @@ -29,4 +30,12 @@ set_target_properties(${PROJECT_NAME} RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}/bin" RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}/bin" RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO "${CMAKE_BINARY_DIR}/bin" + AUTORCC ON ) + +if (CHATTERINO_STATIC_QT_BUILD) + qt_import_plugins(${PROJECT_NAME} INCLUDE_BY_TYPE + platforms Qt::QXcbIntegrationPlugin + Qt::QMinimalIntegrationPlugin + ) +endif () diff --git a/benchmarks/resources/bench.qrc b/benchmarks/resources/bench.qrc new file mode 100644 index 000000000..557b32e69 --- /dev/null +++ b/benchmarks/resources/bench.qrc @@ -0,0 +1,6 @@ + + + recentmessages-nymn.json + seventvemotes-nymn.json + + diff --git a/benchmarks/resources/recentmessages-nymn.json b/benchmarks/resources/recentmessages-nymn.json new file mode 100644 index 000000000..cd8bc7f43 --- /dev/null +++ b/benchmarks/resources/recentmessages-nymn.json @@ -0,0 +1 @@ +{"messages":["@returning-chatter=0;rm-received-ts=1704557984273;first-msg=0;color=#8A2BE2;historical=1;room-id=62300805;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170,176-186,192-202;flags=;badges=no_audio/1;mod=0;client-nonce=ec682d2912f0ae95f60b53ce821ee88e;user-id=253596827;tmi-sent-ts=1704557984095;turbo=0;subscriber=0;display-name=Purple_Geco;user-type=;id=33445373-4dcf-46a5-b2a9-2eb9db2386a5;badge-info= :purple_geco!purple_geco@purple_geco.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty","@room-id=62300805;historical=1;mod=0;badges=;subscriber=0;rm-received-ts=1704557984412;emotes=;user-id=417575252;display-name=mon_dieud;color=#D2691E;tmi-sent-ts=1704557984239;first-msg=0;id=4e6674ad-8d04-4171-a8cc-0454332d415c;user-type=;turbo=0;badge-info=;client-nonce=6de2caeea0c45faad38061f86f463ff1;flags=;returning-chatter=0 :mon_dieud!mon_dieud@mon_dieud.tmi.twitch.tv PRIVMSG #nymn :AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM","@tmi-sent-ts=1704557984398;turbo=0;mod=0;badge-info=subscriber/38;returning-chatter=0;historical=1;user-type=;badges=subscriber/36,twitch-recap-2023/1;color=#63BD68;rm-received-ts=1704557984567;subscriber=1;id=8f921b5d-29b1-4c70-bc87-7a88a9d32ba8;display-name=jontEmillian;flags=;emotes=;user-id=433352132;first-msg=0;room-id=62300805 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn forsenParty","@tmi-sent-ts=1704557984491;room-id=62300805;client-nonce=acb3234f197f58640cd9515dfd41fcdc;user-id=51967700;badges=;color=#FF0000;display-name=Patixxl;flags=;rm-received-ts=1704557984666;id=def31555-deb2-49f6-97fb-304bd233cf3f;first-msg=0;emotes=;turbo=0;badge-info=;historical=1;user-type=;returning-chatter=0;subscriber=0;mod=0 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@turbo=0;returning-chatter=0;mod=0;first-msg=0;badges=subscriber/0,premium/1;client-nonce=e66501430b4420408e38fa1cf21d0f67;subscriber=1;badge-info=subscriber/2;flags=;id=bb26e6c4-4056-48f4-90fb-7ae9c6f06737;display-name=mnqn18;user-type=;historical=1;rm-received-ts=1704557985211;user-id=474204887;tmi-sent-ts=1704557985016;emotes=;room-id=62300805;color=#1E90FF :mnqn18!mnqn18@mnqn18.tmi.twitch.tv PRIVMSG #nymn :forsen people","@first-msg=0;historical=1;flags=;user-id=417575252;user-type=;mod=0;tmi-sent-ts=1704557985841;returning-chatter=0;rm-received-ts=1704557986007;badge-info=;display-name=mon_dieud;color=#D2691E;room-id=62300805;turbo=0;id=d9c2ceae-09f7-4fdf-9261-7234f0900800;emotes=;subscriber=0;client-nonce=1c42d8d6d4fee35cef628973bd8dc1ea;badges= :mon_dieud!mon_dieud@mon_dieud.tmi.twitch.tv PRIVMSG #nymn :AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM 󠀀","@vip=1;color=#D52AFF;user-type=;rm-received-ts=1704557986137;historical=1;flags=;first-msg=0;turbo=0;display-name=Joshlad;subscriber=1;badges=vip/1,subscriber/72,rplace-2023/1;mod=0;room-id=62300805;badge-info=subscriber/77;emotes=;user-id=87120320;tmi-sent-ts=1704557985954;returning-chatter=0;id=84223bac-92d3-4859-876d-95a6446514b0 :joshlad!joshlad@joshlad.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@user-type=;first-msg=0;badge-info=;room-id=62300805;returning-chatter=0;user-id=216144449;client-nonce=ef3bd8f842f993fd74398ba06c624482;badges=turbo/1;flags=;mod=0;subscriber=0;color=#00FF7F;rm-received-ts=1704557986610;turbo=1;display-name=FollowProtoBuddy;historical=1;id=3bfff24b-2558-4ea2-a7eb-fb92830a2cdc;tmi-sent-ts=1704557986421;emotes= :followprotobuddy!followprotobuddy@followprotobuddy.tmi.twitch.tv PRIVMSG #nymn nimeDance","@first-msg=0;tmi-sent-ts=1704557986461;client-nonce=8bd58a70f05ba89714ae8ca6834ab3f6;color=#7B00FF;user-type=;flags=;display-name=imav1ctor;badges=premium/1;badge-info=;historical=1;room-id=62300805;turbo=0;rm-received-ts=1704557986681;id=4e37eeed-c138-4672-96d5-4e2dd5661c73;returning-chatter=0;subscriber=0;mod=0;emotes=;user-id=120451539 :imav1ctor!imav1ctor@imav1ctor.tmi.twitch.tv PRIVMSG #nymn :Ratge RaveTime Ratge RaveTime Ratge RaveTime Ratge RaveTime Ratge RaveTime","@tmi-sent-ts=1704557987015;emotes=;badges=;color=#D2691E;client-nonce=7ede2a5fadadc7f92b35fd5696c40cfd;rm-received-ts=1704557987196;id=18ee5b36-02c0-4fe1-a8cf-42b4934fa3e0;historical=1;user-type=;subscriber=0;badge-info=;first-msg=0;flags=;mod=0;display-name=mon_dieud;turbo=0;room-id=62300805;user-id=417575252;returning-chatter=0 :mon_dieud!mon_dieud@mon_dieud.tmi.twitch.tv PRIVMSG #nymn :AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM","@mod=0;id=ed93d522-7a36-4727-83cb-1033711ff242;turbo=0;subscriber=0;user-type=;rm-received-ts=1704557987243;first-msg=0;room-id=62300805;user-id=232078107;badges=no_audio/1;historical=1;display-name=BastunGuy1;emotes=;flags=;returning-chatter=0;badge-info=;color=#008000;client-nonce=423588d77ef5b064925c6a8f5fb50319;tmi-sent-ts=1704557987056 :bastunguy1!bastunguy1@bastunguy1.tmi.twitch.tv PRIVMSG #nymn deadassPls","@user-id=51967700;display-name=Patixxl;id=bb07d0d4-6c39-4ecd-962a-4a749cd45706;user-type=;mod=0;first-msg=0;returning-chatter=0;badges=;tmi-sent-ts=1704557987523;emotes=;color=#FF0000;turbo=0;historical=1;flags=;rm-received-ts=1704557987689;room-id=62300805;badge-info=;client-nonce=2cd274776cf33689752d1c3b9adbb4ce;subscriber=0 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@badges=subscriber/9,turbo/1;room-id=62300805;rm-received-ts=1704557988576;id=d9b7133c-6a97-4308-855f-72d286a7e643;client-nonce=304b11f12b785482c42b5f2338263531;badge-info=subscriber/9;user-type=;tmi-sent-ts=1704557988374;color=#FFFF00;subscriber=1;flags=;mod=0;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170,176-186,192-202,208-218,224-234;display-name=Kotzblitz20;returning-chatter=0;user-id=40037186;historical=1;first-msg=0;turbo=1 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@display-name=mon_dieud;room-id=62300805;color=#D2691E;id=5819375b-b4b8-4d51-92c7-cee7feda5609;emotes=;historical=1;rm-received-ts=1704557988684;user-id=417575252;tmi-sent-ts=1704557988522;first-msg=0;badge-info=;flags=;turbo=0;subscriber=0;mod=0;user-type=;client-nonce=b5ee53eaa0c3d50bb5b2ab3c29fb4ec4;returning-chatter=0;badges= :mon_dieud!mon_dieud@mon_dieud.tmi.twitch.tv PRIVMSG #nymn :AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM 󠀀","@rm-received-ts=1704557990215;badges=;tmi-sent-ts=1704557990036;first-msg=0;emotes=;historical=1;client-nonce=a6876056b3fad57628aa921e21cd6408;turbo=0;id=0a8b223f-94f8-4e29-978a-2f8fc93e2390;user-id=417575252;subscriber=0;display-name=mon_dieud;badge-info=;mod=0;color=#D2691E;returning-chatter=0;user-type=;room-id=62300805;flags= :mon_dieud!mon_dieud@mon_dieud.tmi.twitch.tv PRIVMSG #nymn :AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM","@turbo=0;user-type=;id=bcc61d76-afc4-472e-aac3-915e54f06989;emotes=;user-id=51967700;flags=;returning-chatter=0;rm-received-ts=1704557990289;tmi-sent-ts=1704557990105;client-nonce=8345d33b6e38c943a8bfdbd769016e29;badges=;room-id=62300805;mod=0;color=#FF0000;display-name=Patixxl;badge-info=;historical=1;subscriber=0;first-msg=0 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@subscriber=0;user-type=;user-id=103665668;emotes=;id=f1ae4c8f-9949-4ceb-b6a4-ed2b54ac6f2a;display-name=Intel_power;badges=bits-charity/1;mod=0;tmi-sent-ts=1704557990704;color=#0000FF;returning-chatter=0;flags=;historical=1;room-id=62300805;rm-received-ts=1704557990887;badge-info=;first-msg=0;turbo=0;client-nonce=fa12c0d9435ab3ac61deb047475983cc :intel_power!intel_power@intel_power.tmi.twitch.tv PRIVMSG #nymn CiGrip","@emotes=;flags=;user-type=mod;first-msg=0;subscriber=1;room-id=62300805;badge-info=subscriber/67;user-id=41157245;turbo=0;rm-received-ts=1704557991255;badges=moderator/1,subscriber/60,rplace-2023/1;returning-chatter=0;color=#9146FF;mod=1;display-name=Mr0lle;id=fa40a3ab-c746-460e-8c16-e0203207a453;tmi-sent-ts=1704557991080;historical=1 :mr0lle!mr0lle@mr0lle.tmi.twitch.tv PRIVMSG #nymn forsenParty","@user-id=253596827;turbo=0;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170,176-186,192-202;flags=;mod=0;badges=no_audio/1;badge-info=;tmi-sent-ts=1704557991166;room-id=62300805;rm-received-ts=1704557991345;subscriber=0;display-name=Purple_Geco;client-nonce=9c371e28f86cb9d4d39ca8e40d13aa6b;returning-chatter=0;first-msg=0;id=0185c023-3c26-4c0d-9b70-2689c3c77a45;color=#8A2BE2;user-type=;historical=1 :purple_geco!purple_geco@purple_geco.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@first-msg=0;display-name=Kotzblitz20;returning-chatter=0;tmi-sent-ts=1704557991172;client-nonce=9903d8256de6710e8ac83ae36d722d6b;mod=0;historical=1;user-id=40037186;id=506ab750-aa2a-4c1f-88d6-24c748c4737d;badge-info=subscriber/9;rm-received-ts=1704557991358;badges=subscriber/9,turbo/1;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122;color=#FFFF00;turbo=1;room-id=62300805;subscriber=1;flags=;user-type= :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@color=#DAA520;first-msg=0;room-id=62300805;badges=;historical=1;returning-chatter=0;id=230487e9-c54c-4efb-8934-5d4c07ef701f;mod=0;emotes=;user-type=;client-nonce=7e7b25253a46f0d481c3857d756ac487;user-id=428888588;badge-info=;flags=;display-name=3amo_magdy;subscriber=0;rm-received-ts=1704557991585;turbo=0;tmi-sent-ts=1704557991426 :3amo_magdy!3amo_magdy@3amo_magdy.tmi.twitch.tv PRIVMSG #nymn :this is a good game","@room-id=62300805;user-id=433352132;mod=0;emotes=;subscriber=1;turbo=0;tmi-sent-ts=1704557991423;first-msg=0;returning-chatter=0;color=#63BD68;badge-info=subscriber/38;user-type=;historical=1;flags=;id=c0393303-4786-4b8e-8505-9c281b48f3eb;display-name=jontEmillian;rm-received-ts=1704557991601;badges=subscriber/36,twitch-recap-2023/1 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn :forsenParty 󠀀","@tmi-sent-ts=1704557991551;badges=turbo/1;id=dcd1dd03-409a-4750-a4ae-5fff3f4c4c75;display-name=FollowProtoBuddy;returning-chatter=0;subscriber=0;mod=0;badge-info=;client-nonce=019b560830972e4b42600df04bacdb10;turbo=1;first-msg=0;room-id=62300805;flags=;user-id=216144449;color=#00FF7F;user-type=;emotes=;historical=1;rm-received-ts=1704557991703 :followprotobuddy!followprotobuddy@followprotobuddy.tmi.twitch.tv PRIVMSG #nymn :nimeDance 󠀀","@mod=0;badges=;user-type=;id=a9fd3702-dc3b-4c98-8915-5d88c1b2b7ff;color=#D2691E;returning-chatter=0;first-msg=0;room-id=62300805;user-id=417575252;flags=;badge-info=;historical=1;turbo=0;emotes=;display-name=mon_dieud;subscriber=0;rm-received-ts=1704557991781;tmi-sent-ts=1704557991620;client-nonce=5ff313ccf46c19310e907133a2ae2801 :mon_dieud!mon_dieud@mon_dieud.tmi.twitch.tv PRIVMSG #nymn :AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM 󠀀","@client-nonce=166aa46f441d99819e1e4b25d26be078;tmi-sent-ts=1704557993019;first-msg=0;rm-received-ts=1704557993207;turbo=1;historical=1;badges=subscriber/9,turbo/1;badge-info=subscriber/9;user-id=40037186;display-name=Kotzblitz20;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170,176-186,192-202,208-218;room-id=62300805;color=#FFFF00;id=1fafd51e-693e-43d7-8568-f5c07959bc33;subscriber=1;user-type=;mod=0;flags=;returning-chatter=0 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@user-id=417575252;subscriber=0;color=#D2691E;display-name=mon_dieud;mod=0;tmi-sent-ts=1704557993098;flags=;rm-received-ts=1704557993281;first-msg=0;user-type=;badges=;historical=1;emotes=;id=127d7915-b6a8-47cb-9107-941701e8b8d7;returning-chatter=0;client-nonce=0ab3c100f1f6ce2f2e1b019e2ea999dc;room-id=62300805;turbo=0;badge-info= :mon_dieud!mon_dieud@mon_dieud.tmi.twitch.tv PRIVMSG #nymn :AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM","@user-id=417575252;flags=;tmi-sent-ts=1704557994778;badge-info=;mod=0;id=9a46a043-5707-42c4-875d-5acbdcd1cf33;room-id=62300805;color=#D2691E;client-nonce=efca80e3fb1f92a3586b69f4d67f94c1;returning-chatter=0;first-msg=0;historical=1;rm-received-ts=1704557994985;user-type=;badges=;display-name=mon_dieud;subscriber=0;emotes=;turbo=0 :mon_dieud!mon_dieud@mon_dieud.tmi.twitch.tv PRIVMSG #nymn :AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM 󠀀","@color=#D52AFF;returning-chatter=0;flags=;rm-received-ts=1704557995631;display-name=Joshlad;user-type=;subscriber=1;id=c7260e08-aef7-43ce-a645-d637c3f760af;badges=vip/1,subscriber/72,rplace-2023/1;tmi-sent-ts=1704557995447;user-id=87120320;turbo=0;mod=0;emotes=;room-id=62300805;first-msg=0;vip=1;badge-info=subscriber/77;historical=1 :joshlad!joshlad@joshlad.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@id=2a1c62b2-1563-4a66-8ffd-70a8bb0fb3e4;badges=;client-nonce=c78b3277523c7a97f35dddff37ed7a30;badge-info=;display-name=Patixxl;tmi-sent-ts=1704557995496;first-msg=0;turbo=0;flags=;returning-chatter=0;color=#FF0000;subscriber=0;emotes=;user-id=51967700;room-id=62300805;user-type=;historical=1;rm-received-ts=1704557995678;mod=0 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@room-id=62300805;subscriber=1;rm-received-ts=1704557996656;returning-chatter=0;mod=0;turbo=1;color=#FFFF00;display-name=Kotzblitz20;id=230bff50-0808-41ee-ae9a-83673110162a;historical=1;badges=subscriber/9,turbo/1;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170,176-186,192-202,208-218,224-234,240-250,256-266,272-282,288-298,304-314;user-id=40037186;flags=;client-nonce=27a522a1e13ade81f9f92b93b230c2ee;tmi-sent-ts=1704557996472;badge-info=subscriber/9;user-type=;first-msg=0 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@emotes=;display-name=Intel_power;first-msg=0;user-type=;client-nonce=e3059469d8c3d8b7fdcc7a19ab4ab020;returning-chatter=0;user-id=103665668;rm-received-ts=1704557996898;id=c8353b25-bddf-4048-a8d6-0ba6700111d8;turbo=0;badges=bits-charity/1;color=#0000FF;historical=1;badge-info=;mod=0;tmi-sent-ts=1704557996719;subscriber=0;room-id=62300805;flags= :intel_power!intel_power@intel_power.tmi.twitch.tv PRIVMSG #nymn DonkSass","@mod=1;turbo=0;badge-info=subscriber/67;tmi-sent-ts=1704557996810;color=#9146FF;historical=1;badges=moderator/1,subscriber/60,rplace-2023/1;emotes=;returning-chatter=0;room-id=62300805;flags=;subscriber=1;user-id=41157245;id=7e5ba407-e616-49cd-998f-84384b86aa4a;user-type=mod;rm-received-ts=1704557996977;display-name=Mr0lle;first-msg=0 :mr0lle!mr0lle@mr0lle.tmi.twitch.tv PRIVMSG #nymn forsenParty","@color=#00FF7F;mod=0;returning-chatter=0;badge-info=;id=b40886e1-140e-4fbb-b76e-c65836992b2e;display-name=FollowProtoBuddy;flags=;first-msg=0;emotes=;user-type=;client-nonce=8c8ca6a4a778beb315a3329ca670e49e;subscriber=0;user-id=216144449;rm-received-ts=1704557997445;room-id=62300805;turbo=1;historical=1;tmi-sent-ts=1704557997276;badges=turbo/1 :followprotobuddy!followprotobuddy@followprotobuddy.tmi.twitch.tv PRIVMSG #nymn nimeDance","@id=aebd5676-2e69-4847-9c26-34f347ba5d1b;flags=;badge-info=;room-id=62300805;tmi-sent-ts=1704557997458;user-id=417575252;user-type=;rm-received-ts=1704557997647;turbo=0;client-nonce=60b389debdba4fe307d86fcd0c8a5f51;display-name=mon_dieud;subscriber=0;badges=;mod=0;returning-chatter=0;emotes=;first-msg=0;color=#D2691E;historical=1 :mon_dieud!mon_dieud@mon_dieud.tmi.twitch.tv PRIVMSG #nymn :AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM","@id=ff15824b-3217-4231-9064-9b4ee5fdc48e;user-id=433352132;historical=1;emotes=;mod=0;color=#63BD68;display-name=jontEmillian;returning-chatter=0;badges=subscriber/36,twitch-recap-2023/1;room-id=62300805;rm-received-ts=1704557997706;subscriber=1;user-type=;first-msg=0;tmi-sent-ts=1704557997520;flags=;badge-info=subscriber/38;turbo=0 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn forsenParty","@emotes=;display-name=MaxThurian;historical=1;id=4f72611e-3ca3-4f45-8703-818757fbec7a;flags=;badge-info=subscriber/43;first-msg=0;user-id=60181947;subscriber=1;mod=0;turbo=0;user-type=;color=#00ED2A;tmi-sent-ts=1704557997998;badges=subscriber/42,twitch-recap-2023/1;room-id=62300805;returning-chatter=0;client-nonce=a78a33084735b9a68231d896da549eef;rm-received-ts=1704557998168 :maxthurian!maxthurian@maxthurian.tmi.twitch.tv PRIVMSG #nymn eww","@room-id=62300805;display-name=orange_bean;badges=subscriber/48;first-msg=0;emotes=;flags=;turbo=0;user-type=;mod=0;tmi-sent-ts=1704557999129;subscriber=1;historical=1;rm-received-ts=1704557999305;badge-info=subscriber/53;color=#FF7F50;returning-chatter=0;user-id=29649547;id=b326070a-5f36-4589-aaf9-c32ef898fcbb :orange_bean!orange_bean@orange_bean.tmi.twitch.tv PRIVMSG #nymn CiGrip","@display-name=mon_dieud;turbo=0;badges=;first-msg=0;user-id=417575252;mod=0;tmi-sent-ts=1704557999374;id=1e26272a-fa52-4215-a60f-22d15dff98dc;returning-chatter=0;historical=1;color=#D2691E;flags=;user-type=;client-nonce=f511f48d12a3e7080722a8760daf344b;room-id=62300805;emotes=;badge-info=;subscriber=0;rm-received-ts=1704557999528 :mon_dieud!mon_dieud@mon_dieud.tmi.twitch.tv PRIVMSG #nymn :AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM 󠀀","@color=#FF0000;returning-chatter=0;rm-received-ts=1704557999849;id=f4bb63c6-b621-41eb-a136-9df81cf3223a;first-msg=0;tmi-sent-ts=1704557999676;badge-info=;client-nonce=d0b3b564ed74f99ab9a36f126e1e6156;display-name=Patixxl;subscriber=0;room-id=62300805;flags=;user-type=;mod=0;badges=;turbo=0;emotes=;user-id=51967700;historical=1 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@color=#FFFF00;historical=1;rm-received-ts=1704558000776;id=e97ac476-2caa-46e1-a988-ba521547fd62;badges=subscriber/9,turbo/1;user-id=40037186;tmi-sent-ts=1704558000593;first-msg=0;turbo=1;flags=;returning-chatter=0;client-nonce=7ec45a704b329629df711316464b5ffe;display-name=Kotzblitz20;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154;room-id=62300805;user-type=;badge-info=subscriber/9;mod=0;subscriber=1 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170,176-186,192-202;badges=no_audio/1;user-type=;mod=0;id=2a302114-af71-49ec-a963-0e25f9d49164;client-nonce=2d783d514caa880eeb225528c71cc7a3;tmi-sent-ts=1704558000678;display-name=Purple_Geco;user-id=253596827;room-id=62300805;flags=;badge-info=;subscriber=0;rm-received-ts=1704558000870;returning-chatter=0;first-msg=0;turbo=0;historical=1;color=#8A2BE2 :purple_geco!purple_geco@purple_geco.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty","@flags=;color=#CC007A;mod=0;rm-received-ts=1704558001213;client-nonce=af3230efdc41428528cc30149982ffd4;tmi-sent-ts=1704558001035;user-type=;user-id=24556440;room-id=62300805;id=8aebfab2-9d26-4c55-a64a-656f5b552510;display-name=BaireiPL;turbo=0;badge-info=subscriber/5;returning-chatter=0;emotes=;first-msg=0;badges=subscriber/3;subscriber=1;historical=1 :baireipl!baireipl@baireipl.tmi.twitch.tv PRIVMSG #nymn eww","@room-id=62300805;emotes=;historical=1;rm-received-ts=1704558001290;subscriber=0;badge-info=;first-msg=0;display-name=mon_dieud;color=#D2691E;id=3fe233ef-8c04-4093-ac3f-4c5da5704fa9;user-type=;client-nonce=bb83ff31758bdac4d0d36ee91dcd92c9;turbo=0;flags=;returning-chatter=0;badges=;user-id=417575252;tmi-sent-ts=1704558001122;mod=0 :mon_dieud!mon_dieud@mon_dieud.tmi.twitch.tv PRIVMSG #nymn :AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM","@emotes=;room-id=62300805;flags=;returning-chatter=0;display-name=FollowProtoBuddy;turbo=1;badge-info=;first-msg=0;user-type=;rm-received-ts=1704558002365;client-nonce=a7262d67f1c0d41e2aa578a88e1f9924;tmi-sent-ts=1704558002191;user-id=216144449;subscriber=0;mod=0;color=#00FF7F;historical=1;badges=turbo/1;id=2bf87d8e-a236-4caa-8710-ee6e3badfc3e :followprotobuddy!followprotobuddy@followprotobuddy.tmi.twitch.tv PRIVMSG #nymn :nimeDance 󠀀","@user-type=;display-name=Joshlad;emotes=;historical=1;rm-received-ts=1704558003044;tmi-sent-ts=1704558002867;first-msg=0;vip=1;user-id=87120320;subscriber=1;id=06094525-101c-46c4-bc29-0d9d41c8d605;room-id=62300805;turbo=0;flags=;color=#D52AFF;badges=vip/1,subscriber/72,rplace-2023/1;badge-info=subscriber/77;returning-chatter=0;mod=0 :joshlad!joshlad@joshlad.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@historical=1;color=#D2691E;turbo=0;user-id=417575252;emotes=;badge-info=;user-type=;rm-received-ts=1704558003291;id=ff96194a-5bd0-4ed8-9cfc-b687b4171a3c;subscriber=0;client-nonce=5e78fb34007e14ddf2620e6c8e1e826e;mod=0;room-id=62300805;display-name=mon_dieud;badges=;flags=;returning-chatter=0;first-msg=0;tmi-sent-ts=1704558003107 :mon_dieud!mon_dieud@mon_dieud.tmi.twitch.tv PRIVMSG #nymn :AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM 󠀀","@rm-received-ts=1704558003756;returning-chatter=0;display-name=jontEmillian;color=#63BD68;subscriber=1;mod=0;user-id=433352132;flags=;badges=subscriber/36,twitch-recap-2023/1;emotes=;tmi-sent-ts=1704558003596;turbo=0;user-type=;room-id=62300805;historical=1;id=472ad1eb-0112-4425-9c14-622495222183;badge-info=subscriber/38;first-msg=0 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn :forsenParty 󠀀","@display-name=Patixxl;subscriber=0;badge-info=;room-id=62300805;mod=0;turbo=0;flags=;badges=;rm-received-ts=1704558004015;returning-chatter=0;client-nonce=3216a3ec6f920de48a36dae221309cbe;id=d190574e-e8fd-4898-94fc-dd20ef561fd2;emotes=;first-msg=0;color=#FF0000;user-id=51967700;historical=1;user-type=;tmi-sent-ts=1704558003848 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@badges=moderator/1,subscriber/60,rplace-2023/1;color=#9146FF;badge-info=subscriber/67;mod=1;id=ee39481c-fc74-4359-a0f0-48649b1c9c33;first-msg=0;display-name=Mr0lle;rm-received-ts=1704558004133;user-id=41157245;turbo=0;room-id=62300805;emotes=;flags=;subscriber=1;historical=1;returning-chatter=0;user-type=mod;tmi-sent-ts=1704558003961 :mr0lle!mr0lle@mr0lle.tmi.twitch.tv PRIVMSG #nymn forsenParty","@first-msg=0;turbo=0;returning-chatter=0;user-type=;user-id=190828982;badges=subscriber/18,twitch-recap-2023/1;room-id=62300805;emotes=;subscriber=1;tmi-sent-ts=1704558004863;display-name=Near____________;badge-info=subscriber/20;mod=0;color=#23CE3F;flags=;rm-received-ts=1704558005064;client-nonce=a9721aff359d4e401827030e87200fd8;id=59fcc234-d701-49c6-af55-49ec172a3c6d;historical=1 :near____________!near____________@near____________.tmi.twitch.tv PRIVMSG #nymn CiGrip","@flags=;tmi-sent-ts=1704558005426;user-type=;badges=;display-name=mon_dieud;room-id=62300805;badge-info=;id=1ccf04ab-c88a-4951-a4e8-dafeb920c7b0;user-id=417575252;returning-chatter=0;color=#D2691E;client-nonce=8ae1b700fd218282a757b649cd037a34;first-msg=0;mod=0;emotes=;turbo=0;subscriber=0;historical=1;rm-received-ts=1704558005601 :mon_dieud!mon_dieud@mon_dieud.tmi.twitch.tv PRIVMSG #nymn :AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM AlienTechno RaveTime EDM","@first-msg=0;color=#008000;id=a656baa8-1ec3-4cfd-a827-813a606db4ec;emotes=;room-id=62300805;user-id=278896263;user-type=;flags=;turbo=0;display-name=Phant0mBlades;historical=1;badges=subscriber/9,chatter-cs-go-2022/1;badge-info=subscriber/9;rm-received-ts=1704558006805;mod=0;returning-chatter=0;tmi-sent-ts=1704558006634;subscriber=1 :phant0mblades!phant0mblades@phant0mblades.tmi.twitch.tv PRIVMSG #nymn :MEGALUL 👇 COME TO THE BOG","@returning-chatter=0;emotes=;id=4979b8f0-eefa-4e7a-aca1-61a12e2274c3;mod=0;subscriber=1;turbo=0;display-name=jqxlol;user-id=80542722;badges=subscriber/0,no_video/1;user-type=;client-nonce=877481e211c51ab4934f8a969c2a5368;rm-received-ts=1704558008593;badge-info=subscriber/2;room-id=62300805;flags=;tmi-sent-ts=1704558008350;color=#00FF7F;first-msg=0;historical=1 :jqxlol!jqxlol@jqxlol.tmi.twitch.tv PRIVMSG #nymn :———————————————————————— You have been permanently danked from this channel FeelsDankMan ————————————————————————","@vip=1;flags=;historical=1;turbo=0;mod=0;subscriber=1;room-id=62300805;tmi-sent-ts=1704558010496;id=dad54acd-090a-4c8d-8685-485ff2796ed6;emotes=;rm-received-ts=1704558010661;color=#D52AFF;user-type=;display-name=Joshlad;user-id=87120320;badges=vip/1,subscriber/72,rplace-2023/1;badge-info=subscriber/77;returning-chatter=0;first-msg=0 :joshlad!joshlad@joshlad.tmi.twitch.tv PRIVMSG #nymn nice","@subscriber=1;color=#9146FF;id=3cdedab1-f8b2-488f-b1e7-ba018daf5ca9;display-name=Mr0lle;mod=1;returning-chatter=0;flags=;historical=1;rm-received-ts=1704558011651;room-id=62300805;emotes=;tmi-sent-ts=1704558011457;badges=moderator/1,subscriber/60,rplace-2023/1;user-type=mod;turbo=0;user-id=41157245;badge-info=subscriber/67;first-msg=0 :mr0lle!mr0lle@mr0lle.tmi.twitch.tv PRIVMSG #nymn forsenParty","@badge-info=;subscriber=0;user-type=;first-msg=0;user-id=45424213;display-name=vaka7a_;flags=;id=2769ecf0-0f4f-4526-947b-07d05e96e1bf;turbo=0;badges=twitch-recap-2023/1;room-id=62300805;mod=0;tmi-sent-ts=1704558011827;emotes=;color=#FF0000;client-nonce=3ce320613658448479c4ecd5ac064b1c;historical=1;rm-received-ts=1704558012025;returning-chatter=0 :vaka7a_!vaka7a_@vaka7a_.tmi.twitch.tv PRIVMSG #nymn nnysJam","@turbo=0;id=6c5ab78e-eeac-438e-a861-7d16a583c2c0;user-type=;rm-received-ts=1704558012415;emotes=;user-id=51967700;badges=;tmi-sent-ts=1704558012244;first-msg=0;client-nonce=e2e71df927d4b25302eac39b7288dba8;badge-info=;color=#FF0000;subscriber=0;mod=0;returning-chatter=0;historical=1;display-name=Patixxl;room-id=62300805;flags= :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn nice","@historical=1;color=#008000;rm-received-ts=1704558012459;returning-chatter=0;mod=0;subscriber=1;tmi-sent-ts=1704558012280;id=d960514c-5fbe-46fa-82c7-059235baa092;turbo=0;user-id=522665984;user-type=;emotes=;room-id=62300805;display-name=SylvrOne;badge-info=subscriber/1;badges=subscriber/0,premium/1;flags=;first-msg=0 :sylvrone!sylvrone@sylvrone.tmi.twitch.tv PRIVMSG #nymn nice","@user-id=159210800;badge-info=subscriber/49;flags=;id=3a7fb120-92d7-437e-ab72-1118aa2ea886;badges=subscriber/48,bits/25000;color=#FF2424;historical=1;turbo=0;tmi-sent-ts=1704558012677;room-id=62300805;rm-received-ts=1704558012857;user-type=;client-nonce=3123da7329d7d27de459ec30bcba243d;subscriber=1;returning-chatter=0;mod=0;first-msg=0;display-name=ME_ME;emotes= :me_me!me_me@me_me.tmi.twitch.tv PRIVMSG #nymn Ratge","@client-nonce=92f1f6efce0113e27514d8c99eb38e3d;mod=0;badges=no_audio/1;display-name=Obiwun;historical=1;tmi-sent-ts=1704558013056;user-type=;turbo=0;returning-chatter=0;color=#8A2BE2;room-id=62300805;first-msg=0;rm-received-ts=1704558013245;flags=5-7:S.5;subscriber=0;badge-info=;emotes=;id=5f28d547-1330-4099-90d8-4b79e6a623d7;user-id=46199261 :obiwun!obiwun@obiwun.tmi.twitch.tv PRIVMSG #nymn :haha sex","@badges=subscriber/9,chatter-cs-go-2022/1;flags=;id=8d79f525-ba1d-426b-a557-1176909932d9;room-id=62300805;subscriber=1;user-id=278896263;first-msg=0;user-type=;display-name=Phant0mBlades;tmi-sent-ts=1704558013093;historical=1;badge-info=subscriber/9;returning-chatter=0;mod=0;turbo=0;color=#008000;emotes=;rm-received-ts=1704558013281 :phant0mblades!phant0mblades@phant0mblades.tmi.twitch.tv PRIVMSG #nymn :FeelsDankMan woah","@flags=;returning-chatter=0;subscriber=1;historical=1;tmi-sent-ts=1704558013350;user-id=92529125;emotes=;mod=0;user-type=;rm-received-ts=1704558013528;color=#FF0000;client-nonce=c213fdbca720f4adac75c4ee0d20f08d;badges=subscriber/42,twitch-recap-2023/1;display-name=Mawsonator;turbo=0;room-id=62300805;id=5508f583-15f7-4958-88b4-45181a93b25b;badge-info=subscriber/47;first-msg=0 :mawsonator!mawsonator@mawsonator.tmi.twitch.tv PRIVMSG #nymn PagMan","@subscriber=0;color=#10E2E2;turbo=0;tmi-sent-ts=1704558013427;badges=twitch-recap-2023/1;room-id=62300805;flags=;first-msg=0;badge-info=;emotes=;user-id=167633177;historical=1;mod=0;returning-chatter=0;client-nonce=e59164adf564eff1641d28918e66de80;rm-received-ts=1704558013623;user-type=;id=450ceb46-02b9-45b5-9371-f18a72430c02;display-name=ALotOfChickens :alotofchickens!alotofchickens@alotofchickens.tmi.twitch.tv PRIVMSG #nymn PagMan","@id=c434541a-df33-40f5-8b15-a7533ba8312c;turbo=0;subscriber=1;user-type=;client-nonce=1b69c74519ca839c7347ac1e74e6c8a7;returning-chatter=0;flags=;room-id=62300805;color=#8A2BE2;badge-info=subscriber/21;user-id=57104832;badges=subscriber/18,no_video/1;historical=1;rm-received-ts=1704558014199;emotes=;tmi-sent-ts=1704558014007;mod=0;first-msg=0;display-name=Tomer247 :tomer247!tomer247@tomer247.tmi.twitch.tv PRIVMSG #nymn nicw","@tmi-sent-ts=1704558014024;user-id=83365099;badges=glitchcon2020/1;client-nonce=7f06610d323d66cf4e292310f86cc5f2;badge-info=;user-type=;returning-chatter=0;emotes=;flags=;first-msg=0;display-name=OmniValor;historical=1;turbo=0;room-id=62300805;id=3216c7c2-b0a9-4e9d-93a7-b3423533a2bd;rm-received-ts=1704558014216;mod=0;subscriber=0;color=#00FF7F :omnivalor!omnivalor@omnivalor.tmi.twitch.tv PRIVMSG #nymn nice","@room-id=62300805;tmi-sent-ts=1704558014457;badges=;first-msg=0;id=d5182849-ef34-49f1-a7a3-3850f9eb8995;turbo=0;display-name=3amo_magdy;historical=1;badge-info=;rm-received-ts=1704558014624;mod=0;returning-chatter=0;flags=;user-type=;emotes=;color=#DAA520;subscriber=0;client-nonce=9721493e5ad8995a4a1194c3dd9d8174;user-id=428888588 :3amo_magdy!3amo_magdy@3amo_magdy.tmi.twitch.tv PRIVMSG #nymn nice","@user-type=;client-nonce=2bf1672a8de2d2e768b3cc7c1bed1714;color=#00FF7F;subscriber=0;user-id=216144449;flags=;emotes=;display-name=FollowProtoBuddy;badges=turbo/1;tmi-sent-ts=1704558015154;turbo=1;returning-chatter=0;historical=1;id=63ceb7ca-65b7-47cd-a137-788adffc8756;rm-received-ts=1704558015333;badge-info=;mod=0;first-msg=0;room-id=62300805 :followprotobuddy!followprotobuddy@followprotobuddy.tmi.twitch.tv PRIVMSG #nymn MegaLUL","@tmi-sent-ts=1704558015379;historical=1;user-type=;returning-chatter=0;flags=;mod=0;emotes=;client-nonce=b62d3e2ed3a21dc135106f7372fc9527;first-msg=0;display-name=DontCagePlebs;badge-info=subscriber/37;room-id=62300805;subscriber=1;badges=subscriber/36,no_audio/1;rm-received-ts=1704558015559;color=#DAA520;id=5781b828-c442-4e3e-9682-dff515eda839;turbo=0;user-id=85837900 :dontcageplebs!dontcageplebs@dontcageplebs.tmi.twitch.tv PRIVMSG #nymn SOLO","@turbo=0;emotes=;color=#7A56A3;first-msg=0;tmi-sent-ts=1704558016033;display-name=d0mr;subscriber=0;id=0786e35f-63d4-43f1-b1f9-807e39148c90;mod=0;flags=;badges=gold-pixel-heart/1;user-type=;badge-info=;user-id=58819477;room-id=62300805;historical=1;rm-received-ts=1704558016285;client-nonce=30357690aaa55b2e7fc6da87104b5afb;returning-chatter=0 :d0mr!d0mr@d0mr.tmi.twitch.tv PRIVMSG #nymn nice","@id=8f03e236-7700-4b04-9d69-d002ee8c557d;first-msg=0;user-type=;rm-received-ts=1704558016930;subscriber=1;client-nonce=0d6591fbb5bc13045531de3ab726f6c1;user-id=57104832;flags=;badge-info=subscriber/21;room-id=62300805;display-name=Tomer247;color=#8A2BE2;emotes=;returning-chatter=0;tmi-sent-ts=1704558016761;mod=0;turbo=0;historical=1;badges=subscriber/18,no_video/1 :tomer247!tomer247@tomer247.tmi.twitch.tv PRIVMSG #nymn nice","@flags=;badge-info=subscriber/2;historical=1;client-nonce=b7963a3b7ae7995e53c6aad70e3cfc7d;emotes=;id=99bf3d3a-69e9-49f5-80ad-a378c0a570b6;display-name=mnqn18;rm-received-ts=1704558017884;turbo=0;room-id=62300805;user-id=474204887;badges=subscriber/0,premium/1;first-msg=0;color=#1E90FF;user-type=;tmi-sent-ts=1704558017693;mod=0;returning-chatter=0;subscriber=1 :mnqn18!mnqn18@mnqn18.tmi.twitch.tv PRIVMSG #nymn :SOLO bejba","@id=429a1ba9-bc02-4507-9627-a7fcc5965e64;badge-info=;color=#FF0000;badges=twitch-recap-2023/1;emotes=;subscriber=0;tmi-sent-ts=1704558017775;rm-received-ts=1704558017990;client-nonce=509880927d35ba879249721f67130c02;turbo=0;first-msg=0;user-id=91515163;historical=1;room-id=62300805;flags=;display-name=Rattge;mod=0;returning-chatter=0;user-type= :rattge!rattge@rattge.tmi.twitch.tv PRIVMSG #nymn nice","@rm-received-ts=1704558018845;historical=1;flags=;room-id=62300805;id=a1999d28-3302-4091-a491-94b8dae5b3c8;user-id=423574282;subscriber=1;tmi-sent-ts=1704558018651;mod=0;returning-chatter=0;turbo=0;badges=subscriber/9,gold-pixel-heart/1;emotes=;color=#D2FFD6;rm-deleted=1;badge-info=subscriber/9;first-msg=0;display-name=e7om;user-type= :e7om!e7om@e7om.tmi.twitch.tv PRIVMSG #nymn FDM","@historical=1;id=2217882b-ec83-4245-88ac-7e684f695e09;turbo=0;user-id=151423066;mod=0;tmi-sent-ts=1704558019447;user-type=;emotes=;color=#FF69B4;badge-info=;display-name=forsenkkona_;room-id=62300805;returning-chatter=0;badges=;rm-received-ts=1704558019646;flags=;subscriber=0;first-msg=0 :forsenkkona_!forsenkkona_@forsenkkona_.tmi.twitch.tv PRIVMSG #nymn 🔨","@color=#7B00FF;mod=0;room-id=62300805;badge-info=;emotes=;id=2d8d69ae-711f-4d3b-a1ca-0eb2a082196d;returning-chatter=0;historical=1;rm-received-ts=1704558019901;subscriber=0;turbo=0;first-msg=0;flags=;client-nonce=281586a259405039efca0c4c2990baba;user-type=;tmi-sent-ts=1704558019735;badges=premium/1;display-name=imav1ctor;user-id=120451539 :imav1ctor!imav1ctor@imav1ctor.tmi.twitch.tv PRIVMSG #nymn EZ","@flags=;badges=subscriber/36,twitch-recap-2023/1;display-name=jontEmillian;badge-info=subscriber/38;color=#63BD68;returning-chatter=0;turbo=0;first-msg=0;mod=0;tmi-sent-ts=1704558020337;user-type=;subscriber=1;room-id=62300805;user-id=433352132;id=04e594cc-dcf1-43b6-97c4-de66c4211525;emotes=;rm-received-ts=1704558020531;historical=1 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn zulul","@emotes=;color=#0000FF;first-msg=0;rm-received-ts=1704558021122;id=c2533aaf-3f15-4e02-ab7e-81cc196ae724;user-type=;user-id=103665668;client-nonce=ddbc9fc47d199749d08cf1fa970e87d8;turbo=0;historical=1;display-name=Intel_power;mod=0;badge-info=;room-id=62300805;flags=;tmi-sent-ts=1704558020942;subscriber=0;badges=bits-charity/1;returning-chatter=0 :intel_power!intel_power@intel_power.tmi.twitch.tv PRIVMSG #nymn ZULUL","@badge-info=subscriber/9;user-id=423574282;color=#D2FFD6;tmi-sent-ts=1704558021113;turbo=0;rm-deleted=1;emotes=;id=affa2505-e5e5-43f8-a343-e119b8f937a5;historical=1;subscriber=1;room-id=62300805;display-name=e7om;mod=0;rm-received-ts=1704558021284;flags=;first-msg=0;badges=subscriber/9,gold-pixel-heart/1;returning-chatter=0;user-type= :e7om!e7om@e7om.tmi.twitch.tv PRIVMSG #nymn ZULUL","@id=6fe0b0c0-d1f2-4eb2-9f0e-022cbe9ff40b;first-msg=0;user-type=;mod=0;rm-received-ts=1704558021289;client-nonce=419911fb3221efddc8738a32aa950fbc;returning-chatter=0;subscriber=0;flags=;user-id=810718356;tmi-sent-ts=1704558021127;badge-info=;emotes=;badges=;room-id=62300805;turbo=0;color=#FF0000;display-name=holy4uck;historical=1 :holy4uck!holy4uck@holy4uck.tmi.twitch.tv PRIVMSG #nymn ZULUL","@returning-chatter=0;historical=1;turbo=0;rm-received-ts=1704558021474;id=6bd664e1-0047-4ccf-a040-be85ee3756ce;display-name=Barbap;flags=;first-msg=0;badges=subscriber/48,twitch-recap-2023/1;tmi-sent-ts=1704558021290;color=#00FF7F;emotes=;user-id=59674528;user-type=;mod=0;badge-info=subscriber/50;room-id=62300805;subscriber=1;client-nonce=ac90bcb97a612501e003da5db61320c6 :barbap!barbap@barbap.tmi.twitch.tv PRIVMSG #nymn ZULUL","@flags=;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170,176-186,192-202,208-218,224-234;turbo=1;badge-info=subscriber/9;user-type=;rm-received-ts=1704558023525;badges=subscriber/9,turbo/1;mod=0;client-nonce=38e68c55ab4d2d8ef1508808be0889f9;room-id=62300805;display-name=Kotzblitz20;tmi-sent-ts=1704558023219;color=#FFFF00;first-msg=0;returning-chatter=0;id=17dda281-4dce-4b45-93ec-80084c0f7bc2;user-id=40037186;subscriber=1;historical=1 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@turbo=0;returning-chatter=0;client-nonce=5ae9d9dd701b1f8c29209bb63ea360d1;badges=;id=f01ed706-ff78-4882-b61f-541122cf16d2;mod=0;user-id=51967700;emotes=;user-type=;display-name=Patixxl;historical=1;badge-info=;rm-received-ts=1704558025169;subscriber=0;color=#FF0000;first-msg=0;tmi-sent-ts=1704558024981;flags=;room-id=62300805 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@mod=1;badges=moderator/1,subscriber/60,rplace-2023/1;color=#9146FF;subscriber=1;turbo=0;badge-info=subscriber/67;id=9305ed9d-0a1d-421f-91fb-2b8d9da40d6f;historical=1;flags=;tmi-sent-ts=1704558025353;first-msg=0;user-type=mod;emotes=;display-name=Mr0lle;user-id=41157245;returning-chatter=0;rm-received-ts=1704558025527;room-id=62300805 :mr0lle!mr0lle@mr0lle.tmi.twitch.tv PRIVMSG #nymn forsenParty","@flags=;id=ba43500e-1616-4011-97d3-05ee5af6bc27;returning-chatter=0;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170,176-186,192-202;color=#FFFF00;first-msg=0;user-id=40037186;tmi-sent-ts=1704558025651;historical=1;user-type=;badges=subscriber/9,turbo/1;room-id=62300805;display-name=Kotzblitz20;mod=0;subscriber=1;turbo=1;badge-info=subscriber/9;client-nonce=3cbc02126e77913ca22d8b9d0a32f90d;rm-received-ts=1704558025828 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@user-type=;emotes=;flags=;badges=turbo/1;client-nonce=706b5514213672abd347a23b99797857;mod=0;turbo=1;returning-chatter=0;id=e1d9c71e-d83e-496f-8932-b3ef7e3c8430;color=#00FF7F;first-msg=0;badge-info=;rm-received-ts=1704558027062;tmi-sent-ts=1704558026857;user-id=216144449;historical=1;display-name=FollowProtoBuddy;subscriber=0;room-id=62300805 :followprotobuddy!followprotobuddy@followprotobuddy.tmi.twitch.tv PRIVMSG #nymn nimeDance","@tmi-sent-ts=1704558026932;badges=;room-id=62300805;flags=;returning-chatter=0;badge-info=;color=#FF0000;client-nonce=a3a25cb9768411360980ae5adb4490d3;historical=1;id=0cdebf66-aced-45a1-8984-d0493282effd;rm-received-ts=1704558027109;user-id=51967700;turbo=0;user-type=;first-msg=0;emotes=;subscriber=0;display-name=Patixxl;mod=0 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@turbo=1;color=#FFFF00;user-type=;returning-chatter=0;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170,176-186,192-202,208-218,224-234,240-250,256-266,272-282;user-id=40037186;badges=subscriber/9,turbo/1;id=6c0165dd-54db-4cf6-be81-2176dbc6f237;rm-received-ts=1704558028571;first-msg=0;client-nonce=c01848bdbd9eb679d2e7b467bdb1d63e;badge-info=subscriber/9;flags=;subscriber=1;tmi-sent-ts=1704558028378;room-id=62300805;display-name=Kotzblitz20;mod=0;historical=1 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@room-id=62300805;mod=0;user-id=159210800;flags=;rm-received-ts=1704558028613;badge-info=subscriber/49;client-nonce=3751d2954c6916384762012de5635f7c;returning-chatter=0;display-name=ME_ME;user-type=;historical=1;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10;color=#FF2424;id=b3675782-ba49-4335-88da-9425c31a8ff6;first-msg=0;badges=subscriber/48,bits/25000;turbo=0;tmi-sent-ts=1704558028432;subscriber=1 :me_me!me_me@me_me.tmi.twitch.tv PRIVMSG #nymn :forsenParty ASYLUM RAVE","@user-id=75144877;badges=subscriber/3,no_video/1;display-name=buong1;rm-received-ts=1704558028620;tmi-sent-ts=1704558028449;client-nonce=7636ed0d0d89f9e42ae4227049bfad4d;flags=;user-type=;color=#FF0000;first-msg=0;id=92b43266-0adc-4d45-82ee-8b6ed3f30b29;historical=1;mod=0;emotes=;room-id=62300805;returning-chatter=0;badge-info=subscriber/5;turbo=0;subscriber=1 :buong1!buong1@buong1.tmi.twitch.tv PRIVMSG #nymn :forsenParty 󠀀","@rm-received-ts=1704558028621;first-msg=0;id=93a058e0-6c51-48fa-8bc7-fb0129658096;vip=1;user-type=;historical=1;display-name=Joshlad;tmi-sent-ts=1704558028450;turbo=0;emotes=;badges=vip/1,subscriber/72,rplace-2023/1;room-id=62300805;badge-info=subscriber/77;returning-chatter=0;mod=0;flags=;subscriber=1;user-id=87120320;color=#D52AFF :joshlad!joshlad@joshlad.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@turbo=0;flags=;badges=subscriber/36,twitch-recap-2023/1;display-name=jontEmillian;user-type=;id=82ce266b-58de-4970-b274-33eaa54c2027;mod=0;room-id=62300805;user-id=433352132;emotes=;returning-chatter=0;subscriber=1;color=#63BD68;first-msg=0;tmi-sent-ts=1704558028622;rm-received-ts=1704558028798;badge-info=subscriber/38;historical=1 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn forsenParty","@first-msg=0;emotes=;mod=0;turbo=0;id=5655953f-16de-45ef-800e-07d392779c3f;color=#008000;tmi-sent-ts=1704558028690;client-nonce=9dbb3929a3a9386fa64393019ce28767;room-id=62300805;badges=twitch-recap-2023/1;historical=1;display-name=bovabova;rm-received-ts=1704558028876;badge-info=;user-id=11654373;user-type=;flags=;subscriber=0;returning-chatter=0 :bovabova!bovabova@bovabova.tmi.twitch.tv PRIVMSG #nymn forsen","@badge-info=;historical=1;emotes=;turbo=0;mod=0;first-msg=0;returning-chatter=0;rm-received-ts=1704558029126;tmi-sent-ts=1704558028948;subscriber=0;user-id=810718356;flags=;client-nonce=676c8bc7be7d070150febd27bb2f6d17;id=f58031f7-eaaf-4f92-9181-a08f5a0f5655;user-type=;room-id=62300805;display-name=holy4uck;color=#FF0000;badges= :holy4uck!holy4uck@holy4uck.tmi.twitch.tv PRIVMSG #nymn forsenParty","@flags=;display-name=Patixxl;first-msg=0;color=#FF0000;badge-info=;id=c425fbfd-2b0b-4e68-8484-c5b9ddcd63f3;emotes=;room-id=62300805;subscriber=0;user-id=51967700;user-type=;badges=;returning-chatter=0;tmi-sent-ts=1704558028969;mod=0;historical=1;rm-received-ts=1704558029148;client-nonce=a5257d469f1c32fb23582cf567bd4687;turbo=0 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@user-id=120451539;badges=premium/1;color=#7B00FF;mod=0;returning-chatter=0;historical=1;subscriber=0;client-nonce=7e91d88e582a0d50584ec986f080d557;turbo=0;rm-received-ts=1704558029193;user-type=;flags=;first-msg=0;tmi-sent-ts=1704558029021;display-name=imav1ctor;badge-info=;id=80252541-a1bc-414e-94ee-e039def29008;emotes=;room-id=62300805 :imav1ctor!imav1ctor@imav1ctor.tmi.twitch.tv PRIVMSG #nymn :EDM Ratge RaveTime EDM Ratge RaveTime EDM Ratge RaveTime EDM Ratge RaveTime EDM Ratge RaveTime EDM Ratge RaveTime EDM Ratge RaveTime","@display-name=DontCagePlebs;badge-info=subscriber/37;id=ccdadfca-8c7e-47c8-b1f6-9f97322ddba1;user-id=85837900;historical=1;tmi-sent-ts=1704558029039;returning-chatter=0;turbo=0;color=#DAA520;user-type=;badges=subscriber/36,no_audio/1;emotes=emotesv2_2ce848d8d4cb42cbb94ba47b9dd8183e:12-18,32-38,52-58,72-78;rm-received-ts=1704558029230;room-id=62300805;client-nonce=81cafe8c3a5fc28211ac15c5af0b2633;flags=;subscriber=1;mod=0;first-msg=0 :dontcageplebs!dontcageplebs@dontcageplebs.tmi.twitch.tv PRIVMSG #nymn :forsenParty nymnEDM forsenParty nymnEDM forsenParty nymnEDM forsenParty nymnEDM","@first-msg=0;rm-received-ts=1704558029520;room-id=62300805;subscriber=0;color=#00FF7F;historical=1;badges=turbo/1;returning-chatter=0;turbo=1;display-name=FollowProtoBuddy;user-id=216144449;emotes=;mod=0;id=58cc3dab-6266-4dd6-95fb-68b56480650c;tmi-sent-ts=1704558029363;user-type=;badge-info=;client-nonce=65c02289086270dba12e88e3ed6ccc82;flags= :followprotobuddy!followprotobuddy@followprotobuddy.tmi.twitch.tv PRIVMSG #nymn :nimeDance 󠀀","@user-id=278896263;flags=;tmi-sent-ts=1704558030263;display-name=Phant0mBlades;mod=0;room-id=62300805;turbo=0;id=3a2f9416-2380-4993-8684-41e14537f5d3;color=#008000;badges=subscriber/9,chatter-cs-go-2022/1;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10;rm-received-ts=1704558030438;returning-chatter=0;user-type=;badge-info=subscriber/9;subscriber=1;first-msg=0;historical=1 :phant0mblades!phant0mblades@phant0mblades.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM","@badges=;user-type=;id=d0b1e9a3-c1fe-4118-83da-1445daea3674;display-name=Patixxl;first-msg=0;returning-chatter=0;subscriber=0;emotes=;rm-received-ts=1704558030661;historical=1;badge-info=;flags=;client-nonce=8dbcddf9498c0a2398a307e686221500;user-id=51967700;color=#FF0000;turbo=0;room-id=62300805;mod=0;tmi-sent-ts=1704558030479 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@tmi-sent-ts=1704558030901;flags=;badge-info=;returning-chatter=0;user-id=205837377;rm-received-ts=1704558031068;display-name=Duchene;turbo=0;first-msg=0;subscriber=0;user-type=;client-nonce=f1ed8e4ed90f2065b6b627b7daaa27fe;mod=0;id=196e5a3c-00d4-49a7-b081-2a5fffde74b9;color=#000000;room-id=62300805;historical=1;badges=;emotes= :duchene!duchene@duchene.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@historical=1;badges=subscriber/48,twitch-recap-2023/1;turbo=0;badge-info=subscriber/50;subscriber=1;first-msg=0;user-id=59674528;client-nonce=6b7a9511d1c73fd4e902f38da9cf0f02;returning-chatter=0;flags=;user-type=;display-name=Barbap;emotes=;id=b2a5c5bc-5d27-4315-a157-a014e1e0802a;color=#00FF7F;tmi-sent-ts=1704558031008;rm-received-ts=1704558031197;mod=0;room-id=62300805 :barbap!barbap@barbap.tmi.twitch.tv PRIVMSG #nymn forsenParty","@badges=subscriber/9,turbo/1;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170;tmi-sent-ts=1704558031357;subscriber=1;client-nonce=5ce491281ca3f403412c3e633bd959eb;first-msg=0;mod=0;user-id=40037186;badge-info=subscriber/9;id=ee46210f-52b7-4e1a-a1a1-e60ab7754e30;user-type=;flags=;display-name=Kotzblitz20;rm-received-ts=1704558031537;color=#FFFF00;room-id=62300805;returning-chatter=0;historical=1;turbo=1 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@id=94b585c7-e3e8-4960-b46b-a7f9c89282ee;tmi-sent-ts=1704558031579;first-msg=0;mod=0;color=#008000;historical=1;display-name=terning;user-type=;client-nonce=aeffb7553dc6c932ef6da297a272755d;subscriber=1;rm-received-ts=1704558031751;emotes=;returning-chatter=0;turbo=0;badges=subscriber/9,rplace-2023/1;user-id=135571016;flags=;badge-info=subscriber/11;room-id=62300805 :terning!terning@terning.tmi.twitch.tv PRIVMSG #nymn BOOMIES","@user-type=;first-msg=0;flags=;badge-info=subscriber/9;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10;user-id=278896263;mod=0;turbo=0;display-name=Phant0mBlades;subscriber=1;room-id=62300805;id=0f65912b-bf73-4f45-b938-089c44b5ae7c;tmi-sent-ts=1704558031893;badges=subscriber/9,chatter-cs-go-2022/1;returning-chatter=0;historical=1;rm-received-ts=1704558032078;color=#008000 :phant0mblades!phant0mblades@phant0mblades.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM","@user-id=51967700;client-nonce=e388bf68406561a507e5b8334e0ff436;subscriber=0;mod=0;badge-info=;room-id=62300805;display-name=Patixxl;tmi-sent-ts=1704558032051;turbo=0;historical=1;user-type=;returning-chatter=0;first-msg=0;flags=;emotes=;id=cf632154-1501-481e-910e-950fed34064e;rm-received-ts=1704558032229;color=#FF0000;badges= :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@rm-received-ts=1704558033234;display-name=Joshlad;turbo=0;flags=;room-id=62300805;subscriber=1;tmi-sent-ts=1704558033052;color=#D52AFF;emotes=;badge-info=subscriber/77;badges=vip/1,subscriber/72,rplace-2023/1;user-type=;mod=0;historical=1;user-id=87120320;vip=1;returning-chatter=0;id=63b30720-5149-482d-b3ec-d7467bb984a1;first-msg=0 :joshlad!joshlad@joshlad.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@flags=;mod=0;id=b0205480-7c9e-4954-953d-984b28b4c739;subscriber=1;user-type=;room-id=62300805;user-id=433352132;badge-info=subscriber/38;turbo=0;returning-chatter=0;tmi-sent-ts=1704558033276;badges=subscriber/36,twitch-recap-2023/1;display-name=jontEmillian;historical=1;emotes=;rm-received-ts=1704558033451;color=#63BD68;first-msg=0 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn :forsenParty 󠀀","@historical=1;client-nonce=2fc10af2e22479ac486657faecda946e;color=#FF0000;flags=;badge-info=;room-id=62300805;mod=0;user-type=;emotes=;rm-received-ts=1704558033996;first-msg=0;display-name=Patixxl;badges=;tmi-sent-ts=1704558033832;subscriber=0;returning-chatter=0;user-id=51967700;turbo=0;id=3e05dd46-22f4-4b57-b33e-cd70520fbdfc :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@first-msg=0;badge-info=subscriber/9;id=5a41b2c5-128a-45ca-ade7-f2cc0b5836c2;historical=1;client-nonce=a21ddd820f57ad20c03e7855adbe0b83;turbo=1;tmi-sent-ts=1704558033822;color=#FFFF00;returning-chatter=0;subscriber=1;badges=subscriber/9,turbo/1;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170,176-186,192-202,208-218,224-234,240-250;user-id=40037186;room-id=62300805;flags=;mod=0;user-type=;display-name=Kotzblitz20;rm-received-ts=1704558034004 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@flags=;historical=1;client-nonce=64acd9ccd3454654365ba8643900de23;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10;room-id=62300805;badge-info=subscriber/49;badges=subscriber/48,bits/25000;turbo=0;user-id=159210800;user-type=;returning-chatter=0;mod=0;rm-received-ts=1704558034080;id=0d3b6b8e-7fb0-40b3-b59a-ed7394d0dec8;subscriber=1;display-name=ME_ME;color=#FF2424;first-msg=0;tmi-sent-ts=1704558033916 :me_me!me_me@me_me.tmi.twitch.tv PRIVMSG #nymn :forsenParty ASYLUM RAVE 󠀀","@flags=;color=#DAA520;client-nonce=9e505ea825ceac4ad63ff1ca9874660f;returning-chatter=0;turbo=0;subscriber=1;historical=1;room-id=62300805;badges=subscriber/36,no_audio/1;mod=0;tmi-sent-ts=1704558034303;rm-received-ts=1704558034498;user-id=85837900;badge-info=subscriber/37;emotes=emotesv2_2ce848d8d4cb42cbb94ba47b9dd8183e:12-18,32-38,52-58,72-78,92-98,112-118,132-138;first-msg=0;user-type=;id=9f7c81c9-2994-4f1c-ab81-c50cdbc8d350;display-name=DontCagePlebs :dontcageplebs!dontcageplebs@dontcageplebs.tmi.twitch.tv PRIVMSG #nymn :forsenParty nymnEDM forsenParty nymnEDM forsenParty nymnEDM forsenParty nymnEDM forsenParty nymnEDM forsenParty nymnEDM forsenParty nymnEDM","@subscriber=0;client-nonce=e772f97754ad088032bb776e17460619;badge-info=;user-id=216144449;historical=1;turbo=1;id=71f5ea26-ff8f-475f-95cd-cac668094862;first-msg=0;mod=0;badges=turbo/1;room-id=62300805;display-name=FollowProtoBuddy;emotes=;flags=;user-type=;color=#00FF7F;returning-chatter=0;rm-received-ts=1704558034595;tmi-sent-ts=1704558034423 :followprotobuddy!followprotobuddy@followprotobuddy.tmi.twitch.tv PRIVMSG #nymn nimeDance","@returning-chatter=0;color=#7B00FF;client-nonce=425adea45108f51970486fd4f1eead88;rm-received-ts=1704558034617;user-type=;badges=premium/1;subscriber=0;first-msg=0;id=358e604f-6e0b-4291-a0c3-ce511e428e64;mod=0;badge-info=;emotes=;tmi-sent-ts=1704558034446;turbo=0;historical=1;user-id=120451539;display-name=imav1ctor;flags=;room-id=62300805 :imav1ctor!imav1ctor@imav1ctor.tmi.twitch.tv PRIVMSG #nymn :EDM Ratge RaveTime EDM Ratge RaveTime EDM Ratge RaveTime EDM Ratge RaveTime EDM Ratge RaveTime EDM Ratge RaveTime EDM Ratge RaveTime EDM Ratge RaveTime","@client-nonce=3c99caa1d14ecc222e3473d42dcccd1f;emotes=;historical=1;room-id=62300805;subscriber=1;badges=subscriber/6,chatter-cs-go-2022/1;tmi-sent-ts=1704558034490;color=#B22222;first-msg=0;flags=;user-id=222340799;rm-received-ts=1704558034676;returning-chatter=0;mod=0;user-type=;turbo=0;display-name=crazyjuni0r_;badge-info=subscriber/7;id=02d673e7-433e-4064-9f50-4fd139acc525 :crazyjuni0r_!crazyjuni0r_@crazyjuni0r_.tmi.twitch.tv PRIVMSG #nymn docalmostnotL","@user-type=mod;rm-received-ts=1704558034731;room-id=62300805;emotes=;first-msg=0;id=ba3285b6-cadc-4820-b8f5-c788a3a9bdd5;display-name=Mr0lle;flags=;badge-info=subscriber/67;mod=1;returning-chatter=0;color=#9146FF;badges=moderator/1,subscriber/60,rplace-2023/1;subscriber=1;turbo=0;tmi-sent-ts=1704558034561;historical=1;user-id=41157245 :mr0lle!mr0lle@mr0lle.tmi.twitch.tv PRIVMSG #nymn forsenParty","@display-name=3amo_magdy;returning-chatter=0;id=2c50cfeb-16f8-4ab2-b21a-93950ada20c1;user-id=428888588;first-msg=0;emotes=;rm-received-ts=1704558035018;client-nonce=cbe29c8b5ddd74d78bdaf8208cdf92ca;badges=;mod=0;color=#DAA520;subscriber=0;flags=;historical=1;user-type=;room-id=62300805;tmi-sent-ts=1704558034850;badge-info=;turbo=0 :3amo_magdy!3amo_magdy@3amo_magdy.tmi.twitch.tv PRIVMSG #nymn :EDM Ratge RaveTime EDM Ratge RaveTime EDM Ratge RaveTime EDM Ratge RaveTime EDM Ratge RaveTime EDM Ratge RaveTime EDM Ratge RaveTime","@tmi-sent-ts=1704558035165;subscriber=1;emote-only=1;display-name=Near____________;mod=0;historical=1;user-type=;returning-chatter=0;color=#23CE3F;badges=subscriber/18,twitch-recap-2023/1;id=1acb1299-837c-4e7e-9444-2344317ecdbc;first-msg=0;room-id=62300805;rm-received-ts=1704558035363;flags=;user-id=190828982;turbo=0;client-nonce=0638b522c108236879816713649bc8c7;badge-info=subscriber/20;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10/emotesv2_2ce848d8d4cb42cbb94ba47b9dd8183e:12-18 :near____________!near____________@near____________.tmi.twitch.tv PRIVMSG #nymn :forsenParty nymnEDM","@turbo=0;mod=0;first-msg=0;badge-info=;id=26b95220-6cf2-4e36-b92b-d5e1787556c8;historical=1;subscriber=0;tmi-sent-ts=1704558035379;badges=;display-name=bomberman2442;user-type=;color=;flags=;user-id=59060199;client-nonce=a8ac9f75873f8166afec09cf7cebc319;room-id=62300805;emotes=;rm-received-ts=1704558035549;returning-chatter=0 :bomberman2442!bomberman2442@bomberman2442.tmi.twitch.tv PRIVMSG #nymn :i mean makes sense","@badge-info=;user-type=;subscriber=0;returning-chatter=0;emotes=;color=#FF0000;turbo=0;first-msg=0;badges=;id=2beefbc3-5c98-4625-92b2-7b12b1abae24;client-nonce=70c499e679b2fcdaf434745236f8f92d;flags=;historical=1;room-id=62300805;user-id=51967700;mod=0;tmi-sent-ts=1704558035404;rm-received-ts=1704558035589;display-name=Patixxl :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@badge-info=;rm-received-ts=1704558035873;client-nonce=2542a019019051fab6c6c04ac5a63930;room-id=62300805;mod=0;turbo=0;emotes=;flags=;user-id=133862911;user-type=;id=840a0d0b-5c49-46be-8d11-ba07f5de6533;subscriber=0;display-name=123homo;badges=;tmi-sent-ts=1704558035709;first-msg=0;returning-chatter=0;historical=1;color=#FF0000 :123homo!123homo@123homo.tmi.twitch.tv PRIVMSG #nymn rat","@badges=;flags=;historical=1;user-id=810718356;id=19e82d4d-aaf0-4ef9-9180-3114497ad078;user-type=;turbo=0;returning-chatter=0;tmi-sent-ts=1704558035935;color=#FF0000;emotes=;display-name=holy4uck;client-nonce=2bda1986c136c5b04b70b794f5ff82fa;subscriber=0;first-msg=0;rm-received-ts=1704558036104;mod=0;badge-info=;room-id=62300805 :holy4uck!holy4uck@holy4uck.tmi.twitch.tv PRIVMSG #nymn :forsenParty 󠀀","@mod=0;rm-received-ts=1704558036731;color=#FFFF00;client-nonce=fac473d6de046648a2836c55565864f9;badges=subscriber/9,turbo/1;returning-chatter=0;tmi-sent-ts=1704558036484;user-type=;first-msg=0;subscriber=1;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106;user-id=40037186;flags=;display-name=Kotzblitz20;room-id=62300805;id=beaa183b-bd94-4f5b-9320-1f941b3e0e07;turbo=1;historical=1;badge-info=subscriber/9 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@tmi-sent-ts=1704558036623;first-msg=0;user-id=46199261;subscriber=0;color=#8A2BE2;client-nonce=8aa6b9908b0f9bea9d46e751fc272380;badge-info=;returning-chatter=0;flags=;rm-received-ts=1704558036803;mod=0;user-type=;display-name=Obiwun;badges=no_audio/1;turbo=0;room-id=62300805;historical=1;emotes=;id=2a04fc29-19d6-4f5b-bd57-63e567c2caae :obiwun!obiwun@obiwun.tmi.twitch.tv PRIVMSG #nymn :just like in real life","@subscriber=0;turbo=0;room-id=62300805;emotes=;mod=0;badge-info=;tmi-sent-ts=1704558036696;display-name=Patixxl;color=#FF0000;returning-chatter=0;flags=;badges=;first-msg=0;client-nonce=54a1e78e843230a1dfdb9ed8e311ea72;user-id=51967700;rm-received-ts=1704558036868;user-type=;id=84877796-0bad-410a-90a2-1cb433ad3ad0;historical=1 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@historical=1;mod=0;turbo=0;flags=;client-nonce=d3ac86dd052dd537377a0ef5ddfcd55e;color=#FF0000;emotes=;display-name=Patixxl;user-type=;first-msg=0;id=7955cdf6-66a1-4285-9859-aaae192b4ec5;badge-info=;subscriber=0;badges=;returning-chatter=0;room-id=62300805;rm-received-ts=1704558038009;tmi-sent-ts=1704558037830;user-id=51967700 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@returning-chatter=0;rm-received-ts=1704558038666;color=#63BD68;turbo=0;id=8eeda1c5-96cc-4898-ba0d-54d90b393779;display-name=jontEmillian;user-id=433352132;user-type=;badge-info=subscriber/38;subscriber=1;first-msg=0;emotes=;historical=1;mod=0;room-id=62300805;badges=subscriber/36,twitch-recap-2023/1;tmi-sent-ts=1704558038483;flags= :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn forsenParty","@room-id=62300805;subscriber=1;flags=;returning-chatter=0;badges=subscriber/9,turbo/1;turbo=1;id=826b4765-eec2-49f3-a49a-d55cb0cc4913;user-type=;badge-info=subscriber/9;user-id=40037186;first-msg=0;historical=1;client-nonce=8f10f9e81b153fa2467b3b7ed601b7ac;display-name=Kotzblitz20;tmi-sent-ts=1704558038586;mod=0;rm-received-ts=1704558038781;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170,176-186,192-202,208-218,224-234,240-250;color=#FFFF00 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@color=#376180;client-nonce=d3a4f64be02efe618d7e29b8ab5b3838;display-name=Storm;id=49656e4e-60a2-47fc-8801-1186d28fe8de;badge-info=;room-id=62300805;historical=1;rm-received-ts=1704558039139;first-msg=0;user-id=13107998;user-type=mod;emotes=;turbo=1;flags=0-12:A.5;badges=moderator/1,turbo/1;returning-chatter=0;subscriber=0;mod=1;tmi-sent-ts=1704558038907 :storm!storm@storm.tmi.twitch.tv PRIVMSG #nymn :you are a rat","@historical=1;color=#FF0000;user-id=51967700;subscriber=0;badges=;returning-chatter=0;mod=0;badge-info=;first-msg=0;display-name=Patixxl;flags=;client-nonce=c6d0ad584fdd8cf6d70308bed68ddbeb;tmi-sent-ts=1704558039096;room-id=62300805;id=05f1afe3-bf93-47d1-bc58-aac61ef512fb;emotes=;user-type=;turbo=0;rm-received-ts=1704558039268 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@user-type=;room-id=62300805;mod=0;vip=1;display-name=Joshlad;id=6c87fd57-28fe-436a-806f-037bedb46585;subscriber=1;turbo=0;rm-received-ts=1704558039316;badge-info=subscriber/77;tmi-sent-ts=1704558039131;first-msg=0;flags=;color=#D52AFF;historical=1;badges=vip/1,subscriber/72,rplace-2023/1;user-id=87120320;emotes=;returning-chatter=0 :joshlad!joshlad@joshlad.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@flags=;user-id=40037186;display-name=Kotzblitz20;room-id=62300805;first-msg=0;id=ddfe4717-e97f-446b-a793-eec573792f46;client-nonce=8254c7a1008bce87fbd0e92ee7fa6818;badges=subscriber/9,turbo/1;color=#FFFF00;turbo=1;rm-received-ts=1704558040553;historical=1;subscriber=1;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42;returning-chatter=0;user-type=;tmi-sent-ts=1704558040345;mod=0;badge-info=subscriber/9 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM","@client-nonce=6f00b4100d0bed9042c1419eea2f8de5;historical=1;flags=;user-id=51967700;returning-chatter=0;display-name=Patixxl;rm-received-ts=1704558040781;badge-info=;color=#FF0000;subscriber=0;tmi-sent-ts=1704558040601;first-msg=0;id=c1c13e15-b366-4377-871d-17acea6e41dc;room-id=62300805;mod=0;emotes=;turbo=0;user-type=;badges= :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@color=#00FF7F;emotes=;returning-chatter=0;historical=1;id=ada4e539-b480-4877-bb98-fee7ff7a67d0;first-msg=0;flags=;rm-received-ts=1704558041596;mod=0;client-nonce=5c2276be7eb33eb6f6e60126d3345fc8;badges=turbo/1;badge-info=;tmi-sent-ts=1704558041430;user-type=;room-id=62300805;display-name=FollowProtoBuddy;subscriber=0;turbo=1;user-id=216144449 :followprotobuddy!followprotobuddy@followprotobuddy.tmi.twitch.tv PRIVMSG #nymn :nimeDance 󠀀","@rm-received-ts=1704558042012;tmi-sent-ts=1704558041839;first-msg=0;display-name=DontCagePlebs;user-id=85837900;flags=;turbo=0;color=#DAA520;subscriber=1;client-nonce=4eda69aaf85b4c415b890cc68d896302;badges=subscriber/36,no_audio/1;returning-chatter=0;historical=1;emotes=emotesv2_2ce848d8d4cb42cbb94ba47b9dd8183e:12-18,32-38,52-58,72-78,92-98,112-118;id=de9c97ea-fe87-4659-8592-f53b113f0e7b;room-id=62300805;mod=0;user-type=;badge-info=subscriber/37 :dontcageplebs!dontcageplebs@dontcageplebs.tmi.twitch.tv PRIVMSG #nymn :forsenParty nymnEDM forsenParty nymnEDM forsenParty nymnEDM forsenParty nymnEDM forsenParty nymnEDM forsenParty nymnEDM","@badge-info=subscriber/49;client-nonce=fce907e25324c4abe6e750b6afe26f03;id=6834e77b-7bd5-4249-9ca2-571730669006;tmi-sent-ts=1704558042155;room-id=62300805;subscriber=1;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10;mod=0;returning-chatter=0;display-name=ME_ME;turbo=0;historical=1;color=#FF2424;user-type=;rm-received-ts=1704558042357;user-id=159210800;flags=;first-msg=0;badges=subscriber/48,bits/25000 :me_me!me_me@me_me.tmi.twitch.tv PRIVMSG #nymn :forsenParty ASYLUM RAVE","@tmi-sent-ts=1704558042544;room-id=62300805;subscriber=1;badge-info=subscriber/9;user-type=;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170,176-186,192-202,208-218,224-234,240-250,256-266,272-282,288-298;client-nonce=bec9a2ed28fe16915adbc4539c888c93;turbo=1;color=#FFFF00;badges=subscriber/9,turbo/1;flags=;rm-received-ts=1704558042804;returning-chatter=0;user-id=40037186;display-name=Kotzblitz20;mod=0;historical=1;id=76fc5fa6-9300-468d-b1e8-69908a0366e6;first-msg=0 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@mod=0;client-nonce=c5962853e04e915bf950fe15df440729;historical=1;display-name=imav1ctor;user-id=120451539;rm-received-ts=1704558042930;returning-chatter=0;tmi-sent-ts=1704558042722;flags=;badges=premium/1;subscriber=0;id=74f0ebce-8680-41cb-bda0-225903ae0615;first-msg=0;room-id=62300805;emotes=;color=#7B00FF;turbo=0;user-type=;badge-info= :imav1ctor!imav1ctor@imav1ctor.tmi.twitch.tv PRIVMSG #nymn :EDM Ratge RaveTime EDM Ratge RaveTime EDM Ratge RaveTime EDM Ratge RaveTime EDM Ratge RaveTime EDM Ratge RaveTime EDM Ratge RaveTime EDM Ratge RaveTime 󠀀","@id=d2d3243c-2dda-4679-b8b6-d9235ca989c4;flags=0-3:P.3;badge-info=;emotes=;first-msg=0;badges=;mod=0;color=#FF0000;subscriber=0;rm-received-ts=1704558042936;returning-chatter=0;client-nonce=67523fbb2f966959dc56573303911350;tmi-sent-ts=1704558042711;room-id=62300805;historical=1;user-id=25483756;turbo=0;display-name=ZpLit;user-type= :zplit!zplit@zplit.tmi.twitch.tv PRIVMSG #nymn ::tf: dev guy","@mod=0;display-name=terning;id=bc37a6bd-41ea-49b7-8889-2717c3e684e8;badge-info=subscriber/11;subscriber=1;client-nonce=36da8b4b0e75cfecc696db939ee268d7;historical=1;badges=subscriber/9,rplace-2023/1;color=#008000;turbo=0;tmi-sent-ts=1704558042816;user-type=;flags=;user-id=135571016;first-msg=0;returning-chatter=0;room-id=62300805;rm-received-ts=1704558043034;emotes= :terning!terning@terning.tmi.twitch.tv PRIVMSG #nymn :BOOMIES EDM","@turbo=0;room-id=62300805;badge-info=subscriber/38;mod=0;rm-received-ts=1704558043429;returning-chatter=0;historical=1;user-type=;flags=;color=#63BD68;subscriber=1;first-msg=0;tmi-sent-ts=1704558043263;display-name=jontEmillian;user-id=433352132;emotes=;id=2fdb47b6-ff5f-49c1-861f-57b4f88b1ddd;badges=subscriber/36,twitch-recap-2023/1 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn :forsenParty 󠀀","@turbo=0;color=#8A2BE2;returning-chatter=0;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170,176-186,192-202;subscriber=0;tmi-sent-ts=1704558044209;mod=0;id=42151a86-72e6-472b-9a45-0c11ad485e0e;user-type=;badge-info=;room-id=62300805;client-nonce=8830db21f24381399e6a0d6e1f045463;display-name=Purple_Geco;user-id=253596827;rm-received-ts=1704558044408;first-msg=0;historical=1;flags=;badges=no_audio/1 :purple_geco!purple_geco@purple_geco.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@emotes=;display-name=Patixxl;historical=1;tmi-sent-ts=1704558044283;client-nonce=5dd7bed2778d03dbe788e793d8780d38;rm-received-ts=1704558044462;color=#FF0000;badge-info=;room-id=62300805;flags=;user-id=51967700;subscriber=0;first-msg=0;id=7e110244-0bf1-4f53-86a5-1ffa1f7c43fe;mod=0;turbo=0;badges=;returning-chatter=0;user-type= :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@flags=;first-msg=0;rm-received-ts=1704558045327;tmi-sent-ts=1704558045154;turbo=0;subscriber=0;color=#DAA520;id=72d072c5-c2ca-49ab-9af5-e618346ef5db;display-name=3amo_magdy;emotes=;returning-chatter=0;badges=;room-id=62300805;mod=0;user-type=;badge-info=;user-id=428888588;client-nonce=35d8e567d4b97b4bcbb039f5e39457de;historical=1 :3amo_magdy!3amo_magdy@3amo_magdy.tmi.twitch.tv PRIVMSG #nymn :EDM Ratge RaveTime EDM Ratge RaveTime EDM Ratge RaveTime EDM Ratge RaveTime EDM Ratge RaveTime EDM Ratge RaveTime EDM Ratge RaveTime 󠀀","@mod=0;display-name=Purple_Geco;id=2ce2ef15-a27c-46fa-99ae-e404ec5ebc3c;flags=;first-msg=0;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170,176-186,192-202;color=#8A2BE2;badge-info=;returning-chatter=0;subscriber=0;client-nonce=acb78da756800fc9cc9276eb40a20b58;room-id=62300805;badges=no_audio/1;user-id=253596827;tmi-sent-ts=1704558045837;rm-received-ts=1704558046030;turbo=0;user-type=;historical=1 :purple_geco!purple_geco@purple_geco.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty","@subscriber=1;turbo=0;first-msg=0;room-id=62300805;flags=;rm-received-ts=1704558046087;user-type=;color=#23CE3F;user-id=190828982;returning-chatter=0;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10/emotesv2_2ce848d8d4cb42cbb94ba47b9dd8183e:12-18;id=42d51753-8cd8-4d3c-a80b-ab69eca02def;tmi-sent-ts=1704558045902;badges=subscriber/18,twitch-recap-2023/1;display-name=Near____________;client-nonce=1b2bd641c727c7c302e54c1fa692b610;badge-info=subscriber/20;historical=1;mod=0 :near____________!near____________@near____________.tmi.twitch.tv PRIVMSG #nymn :forsenParty nymnEDM 󠀀","@display-name=Patixxl;historical=1;badges=;client-nonce=2ecc4c6714f15b01a199a5c8351aae08;user-id=51967700;badge-info=;color=#FF0000;tmi-sent-ts=1704558046655;room-id=62300805;turbo=0;rm-received-ts=1704558046870;mod=0;user-type=;subscriber=0;emotes=;returning-chatter=0;id=16711817-e1c4-4af8-b998-e0de102995db;first-msg=0;flags= :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@badges=subscriber/36,twitch-recap-2023/1;user-type=;tmi-sent-ts=1704558047715;turbo=0;badge-info=subscriber/38;returning-chatter=0;emotes=;id=5256ced8-298c-4751-bf55-25d28a1f08ad;rm-received-ts=1704558047897;flags=;user-id=433352132;mod=0;display-name=jontEmillian;subscriber=1;historical=1;color=#63BD68;room-id=62300805;first-msg=0 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn forsenParty","@display-name=DontCagePlebs;badges=subscriber/36,no_audio/1;user-id=85837900;first-msg=0;client-nonce=d8bbbd864f8f1a9dcb29d3fc2bf62a34;tmi-sent-ts=1704558047941;badge-info=subscriber/37;rm-received-ts=1704558048140;room-id=62300805;emotes=;historical=1;turbo=0;mod=0;subscriber=1;returning-chatter=0;color=#DAA520;id=cabe23fe-481d-4d51-ac7a-0d863d34c6c2;flags=;user-type= :dontcageplebs!dontcageplebs@dontcageplebs.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@first-msg=0;badges=subscriber/9,chatter-cs-go-2022/1;rm-received-ts=1704558048947;user-type=;mod=0;turbo=0;room-id=62300805;returning-chatter=0;tmi-sent-ts=1704558048712;display-name=Phant0mBlades;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10;user-id=278896263;id=16a6448d-7c22-4377-a1ce-f2e7615ecb98;badge-info=subscriber/9;historical=1;color=#008000;subscriber=1;flags= :phant0mblades!phant0mblades@phant0mblades.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM","@flags=;rm-received-ts=1704558049305;client-nonce=b76159c23971e3a7d576837c006e32fc;turbo=1;mod=0;returning-chatter=0;color=#FFFF00;historical=1;badges=subscriber/9,turbo/1;first-msg=0;emote-only=1;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10;badge-info=subscriber/9;room-id=62300805;user-type=;user-id=40037186;id=2b24078e-ded9-4b99-9ab2-794ec1180c72;subscriber=1;display-name=Kotzblitz20;tmi-sent-ts=1704558049108 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn forsenParty","@turbo=0;badge-info=subscriber/47;emote-only=1;user-id=92529125;mod=0;tmi-sent-ts=1704558049430;emotes=emotesv2_2ce848d8d4cb42cbb94ba47b9dd8183e:12-18/emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10;historical=1;badges=subscriber/42,twitch-recap-2023/1;user-type=;room-id=62300805;id=e6b652ca-639c-4b3a-9a26-527c6a9ee73e;flags=;color=#FF0000;returning-chatter=0;display-name=Mawsonator;rm-received-ts=1704558049652;client-nonce=5c86ea25ef84080845c6cd59877d0429;first-msg=0;subscriber=1 :mawsonator!mawsonator@mawsonator.tmi.twitch.tv PRIVMSG #nymn :forsenParty nymnEDM","@historical=1;user-type=;badge-info=subscriber/38;badges=subscriber/36,twitch-recap-2023/1;flags=;tmi-sent-ts=1704558051734;subscriber=1;user-id=433352132;display-name=jontEmillian;emotes=;returning-chatter=0;mod=0;color=#63BD68;room-id=62300805;id=3111c620-c729-4ffd-81a3-701ff071387b;rm-received-ts=1704558051902;turbo=0;first-msg=0 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn :forsenParty 󠀀","@user-type=;historical=1;badges=;display-name=Patixxl;turbo=0;room-id=62300805;color=#FF0000;subscriber=0;emotes=;flags=;returning-chatter=0;user-id=51967700;badge-info=;tmi-sent-ts=1704558051732;id=76bdc9f3-bb29-414d-9452-ec57de8e7cbd;first-msg=0;client-nonce=0f02f812dfef6cd685d9a637266f8b77;mod=0;rm-received-ts=1704558051906 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@color=#D52AFF;tmi-sent-ts=1704558051841;rm-received-ts=1704558052021;id=d4b41416-e12a-45fe-ba78-06e33e5b3087;turbo=0;subscriber=1;historical=1;badge-info=subscriber/77;badges=vip/1,subscriber/72,rplace-2023/1;flags=;returning-chatter=0;user-id=87120320;display-name=Joshlad;emotes=;user-type=;room-id=62300805;first-msg=0;vip=1;mod=0 :joshlad!joshlad@joshlad.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@user-id=38936358;first-msg=0;id=a99b6cf2-fa40-420f-bb38-10c5c7709725;turbo=0;rm-received-ts=1704558052979;emotes=;display-name=havelsring;mod=0;badge-info=subscriber/45;room-id=62300805;flags=;color=#FFFFFF;badges=subscriber/42,bits/100;tmi-sent-ts=1704558052785;user-type=;client-nonce=a59f75ed1a639ff38efad356f072e3bb;subscriber=1;returning-chatter=0;historical=1 :havelsring!havelsring@havelsring.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@room-id=62300805;subscriber=1;user-type=mod;first-msg=0;badges=moderator/1,subscriber/60,rplace-2023/1;historical=1;flags=;tmi-sent-ts=1704558052992;color=#9146FF;id=08c20b55-4c44-478c-bc23-5a2ffc5afdc8;mod=1;user-id=41157245;emotes=;display-name=Mr0lle;turbo=0;badge-info=subscriber/67;rm-received-ts=1704558053171;returning-chatter=0 :mr0lle!mr0lle@mr0lle.tmi.twitch.tv PRIVMSG #nymn forsenParty","@tmi-sent-ts=1704558053610;mod=0;returning-chatter=0;subscriber=1;user-id=85837900;badge-info=subscriber/37;client-nonce=f81d48ff08f0f120f3d76237f12dcf53;first-msg=0;display-name=DontCagePlebs;id=481c3bde-0d6e-4813-9356-7780ce3e8def;badges=subscriber/36,no_audio/1;historical=1;rm-received-ts=1704558053872;emotes=;color=#DAA520;turbo=0;user-type=;flags=;room-id=62300805 :dontcageplebs!dontcageplebs@dontcageplebs.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@user-type=;display-name=FollowProtoBuddy;color=#00FF7F;room-id=62300805;rm-received-ts=1704558054270;historical=1;subscriber=0;tmi-sent-ts=1704558054032;first-msg=0;badges=turbo/1;emotes=;client-nonce=f109345b5890e3da16c5df0aca244b85;user-id=216144449;id=f77fa4a4-782d-4143-acf1-5c4f5140eb02;mod=0;returning-chatter=0;badge-info=;turbo=1;flags= :followprotobuddy!followprotobuddy@followprotobuddy.tmi.twitch.tv PRIVMSG #nymn :nimeDance forsen can i dance with you?","@rm-received-ts=1704558054600;first-msg=0;returning-chatter=0;user-id=433352132;room-id=62300805;mod=0;badges=subscriber/36,twitch-recap-2023/1;turbo=0;user-type=;subscriber=1;emotes=;historical=1;id=13783e35-2015-45aa-8be2-7a0c69ca6aae;flags=;badge-info=subscriber/38;color=#63BD68;display-name=jontEmillian;tmi-sent-ts=1704558054424 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn forsenParty","@rm-received-ts=1704558054638;id=57c063b5-1525-4240-b4e7-1937ad19c26c;badges=subscriber/3,no_audio/1;user-type=;subscriber=1;tmi-sent-ts=1704558054454;turbo=0;display-name=Sudnim;mod=0;color=#FFC000;user-id=49365214;flags=;badge-info=subscriber/4;first-msg=0;returning-chatter=0;historical=1;client-nonce=56bb43055e74ebbeb18009bcc7cf9a01;room-id=62300805;emotes= :sudnim!sudnim@sudnim.tmi.twitch.tv PRIVMSG #nymn monkaGIGA","@emotes=;color=#FF0000;room-id=62300805;subscriber=0;id=1e662854-37ef-4218-b4ee-654a300572bf;first-msg=0;historical=1;tmi-sent-ts=1704558054467;badge-info=;user-type=;client-nonce=640b0b3d52006a1d649eb4fa2e330b39;turbo=0;badges=;returning-chatter=0;user-id=51967700;rm-received-ts=1704558054650;display-name=Patixxl;flags=;mod=0 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@first-msg=0;color=#FFFF00;subscriber=1;room-id=62300805;tmi-sent-ts=1704558054987;emote-only=1;badge-info=subscriber/9;id=0d695970-3f50-40b9-a142-b46b55c93bda;badges=subscriber/9,turbo/1;user-id=40037186;historical=1;display-name=Kotzblitz20;flags=;rm-received-ts=1704558055186;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,12-22;turbo=1;user-type=;client-nonce=02780d0a5b8f7d67bb6a65e5a2a04e4b;mod=0;returning-chatter=0 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty forsenParty","@emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170,176-186,192-202;client-nonce=be0c9e5a3ecea4d19917c9bb26b9712b;first-msg=0;rm-received-ts=1704558055619;display-name=Purple_Geco;tmi-sent-ts=1704558055434;room-id=62300805;flags=;turbo=0;color=#8A2BE2;subscriber=0;mod=0;user-id=253596827;user-type=;historical=1;id=061e37dd-b6ed-4929-990d-3d3c862e6f71;badges=no_audio/1;badge-info=;returning-chatter=0 :purple_geco!purple_geco@purple_geco.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@returning-chatter=0;display-name=Joshlad;turbo=0;emotes=;tmi-sent-ts=1704558056679;rm-received-ts=1704558056870;flags=;historical=1;user-type=;subscriber=1;vip=1;badge-info=subscriber/77;mod=0;user-id=87120320;id=4f07504c-b176-4458-8738-94c5f5fdef7a;badges=vip/1,subscriber/72,rplace-2023/1;color=#D52AFF;room-id=62300805;first-msg=0 :joshlad!joshlad@joshlad.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@client-nonce=ccfbadd29da250bc58a687ced26b1b60;first-msg=0;room-id=62300805;flags=;turbo=0;id=a1dc219d-e21a-4fa1-a9fc-8e25a1a04569;rm-received-ts=1704558057337;emotes=;tmi-sent-ts=1704558057164;returning-chatter=0;badges=;badge-info=;display-name=Patixxl;user-type=;color=#FF0000;user-id=51967700;subscriber=0;historical=1;mod=0 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@user-type=;rm-received-ts=1704558058585;subscriber=1;mod=0;tmi-sent-ts=1704558058380;turbo=0;room-id=62300805;returning-chatter=0;color=#63BD68;id=74884179-68bd-40df-8b40-937c938e3114;flags=;first-msg=0;badge-info=subscriber/38;emotes=;historical=1;badges=subscriber/36,twitch-recap-2023/1;user-id=433352132;display-name=jontEmillian :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn :forsenParty 󠀀","@user-type=;badge-info=subscriber/9;mod=0;display-name=Kotzblitz20;color=#FFFF00;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106;user-id=40037186;room-id=62300805;tmi-sent-ts=1704558058875;id=7f23923e-079a-4e42-903f-2d5b3ae7808e;turbo=1;client-nonce=b930daedc0b80a0c275a792f284d6d05;historical=1;badges=subscriber/9,turbo/1;flags=;rm-received-ts=1704558059100;returning-chatter=0;subscriber=1;first-msg=0 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@id=66b040ed-9c85-4edd-8012-0b8d609e2274;room-id=62300805;returning-chatter=0;badges=turbo/1;client-nonce=afd4cb11fca458ad8e38ea02edd1c6d0;historical=1;first-msg=0;user-type=;badge-info=;subscriber=0;flags=;user-id=216144449;emotes=;turbo=1;color=#00FF7F;tmi-sent-ts=1704558059241;rm-received-ts=1704558059411;mod=0;display-name=FollowProtoBuddy :followprotobuddy!followprotobuddy@followprotobuddy.tmi.twitch.tv PRIVMSG #nymn :nimeDance forsen can i dance with you? 󠀀","@subscriber=1;user-type=;historical=1;user-id=159210800;returning-chatter=0;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10;id=bf6bec95-d5c3-4c29-a289-f638700ee0d2;badge-info=subscriber/49;flags=;badges=subscriber/48,bits/25000;tmi-sent-ts=1704558059545;room-id=62300805;turbo=0;color=#FF2424;client-nonce=27fae86f793027efdbfee1b84d1f8358;emote-only=1;display-name=ME_ME;rm-received-ts=1704558059758;first-msg=0;mod=0 :me_me!me_me@me_me.tmi.twitch.tv PRIVMSG #nymn forsenParty","@flags=;turbo=1;id=07b158ac-5794-4393-abd0-d678c08a0eb3;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170,176-186,192-202,208-218,224-234;mod=0;first-msg=0;returning-chatter=0;badges=subscriber/9,turbo/1;user-id=40037186;display-name=Kotzblitz20;room-id=62300805;subscriber=1;historical=1;user-type=;client-nonce=876541ffbccf3345a402e8e573ca4bc4;tmi-sent-ts=1704558060658;color=#FFFF00;rm-received-ts=1704558060848;badge-info=subscriber/9 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@user-id=51967700;first-msg=0;room-id=62300805;flags=;subscriber=0;returning-chatter=0;user-type=;badge-info=;color=#FF0000;display-name=Patixxl;client-nonce=9b0d610b8cb5948e1554c05d83005ed4;tmi-sent-ts=1704558060677;mod=0;emotes=;badges=;id=f62e9bc3-bbad-4bc9-893f-02cd3de660ba;historical=1;rm-received-ts=1704558060858;turbo=0 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@mod=0;user-type=;subscriber=1;historical=1;room-id=62300805;display-name=h_h410;badges=subscriber/54,chatter-cs-go-2022/1;rm-received-ts=1704558061149;returning-chatter=0;flags=;first-msg=0;id=53d45865-2575-4fd2-8071-a5b3af069095;tmi-sent-ts=1704558060957;color=#00FF7F;turbo=0;emotes=emotesv2_10304fc8867a4d3586aadf2c409b153a:0-14;badge-info=subscriber/54;user-id=117088592;emote-only=1 :h_h410!h_h410@h_h410.tmi.twitch.tv PRIVMSG #nymn forsenPossessed","@subscriber=1;historical=1;turbo=0;color=#D52AFF;room-id=62300805;mod=0;rm-received-ts=1704558062698;user-type=;tmi-sent-ts=1704558062517;user-id=87120320;id=f0090a00-3ecf-42e5-9771-80ebbf31fff1;display-name=Joshlad;flags=;badges=vip/1,subscriber/72,rplace-2023/1;badge-info=subscriber/77;vip=1;returning-chatter=0;emotes=;first-msg=0 :joshlad!joshlad@joshlad.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@user-id=51809775;flags=;tmi-sent-ts=1704558062973;first-msg=0;user-type=;subscriber=0;mod=0;client-nonce=c62c0d211e90a292300ab39d1e7f2067;display-name=Yrmyli;color=#FF1493;badges=;turbo=0;historical=1;rm-received-ts=1704558063150;badge-info=;returning-chatter=0;id=b4080521-87e2-49cd-82d2-99139f9468a7;emotes=;room-id=62300805 :yrmyli!yrmyli@yrmyli.tmi.twitch.tv PRIVMSG #nymn :nymn what is this game about DOCING","@badge-info=subscriber/9;display-name=Kotzblitz20;user-type=;turbo=1;id=1ecd6998-dadf-46f8-b2cf-5fbbf0c92edf;subscriber=1;user-id=40037186;badges=subscriber/9,turbo/1;client-nonce=d060ee2211e85e604b9d855ab403108d;mod=0;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170,176-186;room-id=62300805;historical=1;returning-chatter=0;flags=;rm-received-ts=1704558063248;first-msg=0;color=#FFFF00;tmi-sent-ts=1704558063067 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@returning-chatter=0;tmi-sent-ts=1704558063489;emotes=;client-nonce=e86b9dd5ae88bd34534323f237cae559;display-name=SecretCarrot;first-msg=0;subscriber=1;turbo=0;badges=subscriber/54,bits/1000;id=5b8bb19a-9943-409a-899c-7f1243e949e8;flags=;user-id=103592036;room-id=62300805;badge-info=subscriber/55;user-type=;mod=0;historical=1;rm-received-ts=1704558063683;color=#00615C :secretcarrot!secretcarrot@secretcarrot.tmi.twitch.tv PRIVMSG #nymn forsenParty","@badges=;flags=;id=d2da9cb3-81a4-47ae-8cd6-cedd4911c9b3;client-nonce=04185bca630f418cbc70d1d5ac0c68eb;emotes=;returning-chatter=0;historical=1;rm-received-ts=1704558063704;user-id=51967700;display-name=Patixxl;color=#FF0000;user-type=;tmi-sent-ts=1704558063524;badge-info=;room-id=62300805;mod=0;first-msg=0;subscriber=0;turbo=0 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@badges=subscriber/36,twitch-recap-2023/1;tmi-sent-ts=1704558063790;first-msg=0;turbo=0;badge-info=subscriber/38;rm-received-ts=1704558063975;color=#63BD68;historical=1;user-type=;room-id=62300805;flags=;user-id=433352132;id=0905f6d0-7609-4dab-94b1-3dd38d22e0ca;emotes=;mod=0;returning-chatter=0;display-name=jontEmillian;subscriber=1 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn forsenParty","@first-msg=0;turbo=0;room-id=62300805;id=2263d9da-83e8-45fe-8e19-9e29f9417e32;subscriber=0;returning-chatter=0;user-type=;display-name=Purple_Geco;flags=;badge-info=;tmi-sent-ts=1704558063826;rm-received-ts=1704558064008;client-nonce=4b2fd0f237f67e94416a0d75959b1025;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170,176-186,192-202;color=#8A2BE2;user-id=253596827;badges=no_audio/1;mod=0;historical=1 :purple_geco!purple_geco@purple_geco.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty","@rm-received-ts=1704558064517;emote-only=1;client-nonce=3518f4dff12eaa39feb13bfc36116a00;flags=;user-type=;first-msg=0;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10/emotesv2_2ce848d8d4cb42cbb94ba47b9dd8183e:12-18;mod=0;historical=1;badge-info=subscriber/20;turbo=0;subscriber=1;badges=subscriber/18,twitch-recap-2023/1;tmi-sent-ts=1704558064293;id=9777e02d-cdb6-4172-bbd4-2500b06ee490;room-id=62300805;returning-chatter=0;user-id=190828982;color=#23CE3F;display-name=Near____________ :near____________!near____________@near____________.tmi.twitch.tv PRIVMSG #nymn :forsenParty nymnEDM","@badge-info=subscriber/5;subscriber=1;color=#FF0000;rm-received-ts=1704558065295;first-msg=0;turbo=0;user-type=;historical=1;client-nonce=0f210a728db7218b298c8c85b5528af5;room-id=62300805;emotes=;user-id=75144877;mod=0;badges=subscriber/3,no_video/1;flags=;tmi-sent-ts=1704558065124;returning-chatter=0;display-name=buong1;id=5820d645-a8e7-45ff-ac47-e5db276f5363 :buong1!buong1@buong1.tmi.twitch.tv PRIVMSG #nymn WAYTOODANK","@historical=1;subscriber=1;badge-info=subscriber/67;id=dad4e1e6-736d-48e0-8f29-35c329346e14;flags=;badges=moderator/1,subscriber/60,rplace-2023/1;user-id=41157245;mod=1;room-id=62300805;returning-chatter=0;first-msg=0;tmi-sent-ts=1704558065152;emotes=;turbo=0;user-type=mod;display-name=Mr0lle;color=#9146FF;rm-received-ts=1704558065328 :mr0lle!mr0lle@mr0lle.tmi.twitch.tv PRIVMSG #nymn forsenParty","@historical=1;user-id=103665668;client-nonce=7bd6756ef7e7252c576e53b2a6398980;rm-received-ts=1704558065562;mod=0;user-type=;subscriber=0;room-id=62300805;badges=bits-charity/1;flags=;id=44bc83ce-473a-4c5e-a420-f092d865082e;returning-chatter=0;badge-info=;turbo=0;display-name=Intel_power;emotes=;tmi-sent-ts=1704558065383;first-msg=0;color=#0000FF :intel_power!intel_power@intel_power.tmi.twitch.tv PRIVMSG #nymn forseninsane","@rm-received-ts=1704558065835;display-name=ME_ME;first-msg=0;turbo=0;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10;client-nonce=a1f8c2b2ee9e81ef36a1a69654e011f7;id=1de83ebb-f26a-4090-850b-0db29b62c7c1;color=#FF2424;room-id=62300805;badge-info=subscriber/49;user-id=159210800;returning-chatter=0;mod=0;badges=subscriber/48,bits/25000;user-type=;tmi-sent-ts=1704558065671;historical=1;flags=;subscriber=1 :me_me!me_me@me_me.tmi.twitch.tv PRIVMSG #nymn :forsenParty 󠀀","@user-type=;mod=0;historical=1;returning-chatter=0;badges=subscriber/9,turbo/1;color=#FFFF00;tmi-sent-ts=1704558066054;turbo=1;rm-received-ts=1704558066246;first-msg=0;subscriber=1;display-name=Kotzblitz20;room-id=62300805;client-nonce=9bdd2f161f8ee8e0adf191964e9198de;flags=;id=cfb64e34-10f0-43d0-b563-69eb8382f018;user-id=40037186;badge-info=subscriber/9;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170,176-186,192-202,208-218,224-234,240-250,256-266,272-282 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@flags=;turbo=0;subscriber=0;mod=0;room-id=62300805;emotes=;user-id=51967700;id=26da472f-bd3a-4d6a-abab-da56d49ef425;returning-chatter=0;display-name=Patixxl;client-nonce=8f992d6c1a4e04348ede81c015ac5cc1;rm-received-ts=1704558066348;user-type=;badges=;first-msg=0;badge-info=;color=#FF0000;tmi-sent-ts=1704558066143;historical=1 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@tmi-sent-ts=1704558066393;subscriber=0;display-name=sehtt_;badge-info=;historical=1;turbo=0;returning-chatter=0;room-id=62300805;user-id=133344079;flags=;color=#5F9EA0;user-type=;rm-received-ts=1704558066584;first-msg=0;mod=0;emotes=;badges=no_audio/1;id=aca54e25-e5b8-4e08-b547-eff4bbfb4b5c;client-nonce=4fc5e82de2d25405ddd6374a19a33ded :sehtt_!sehtt_@sehtt_.tmi.twitch.tv PRIVMSG #nymn forsenPossessed","@returning-chatter=0;badges=no_audio/1;user-id=579006454;room-id=62300805;mod=0;flags=;rm-received-ts=1704558067173;client-nonce=264f46e99efbf58e7626e929446e8d6a;display-name=jezeroc;subscriber=0;historical=1;first-msg=0;color=#FF0000;tmi-sent-ts=1704558066974;turbo=0;user-type=;badge-info=;id=349ad7cb-19d4-4d67-94fd-8c157ce4b47d;emotes= :jezeroc!jezeroc@jezeroc.tmi.twitch.tv PRIVMSG #nymn :haHAA NOT RATATOUILLE","@tmi-sent-ts=1704558067336;emotes=;id=2ec9d9c9-9c2b-48dc-b9b8-03431591e726;display-name=Obiwun;mod=0;rm-received-ts=1704558067533;client-nonce=2d49a931dced3f90e3945dd161638453;first-msg=0;badges=no_audio/1;room-id=62300805;color=#8A2BE2;returning-chatter=0;badge-info=;user-id=46199261;turbo=0;flags=;historical=1;user-type=;subscriber=0 :obiwun!obiwun@obiwun.tmi.twitch.tv PRIVMSG #nymn :doctorWTF what is this game about???","@subscriber=0;tmi-sent-ts=1704558067604;rm-received-ts=1704558067777;id=7c20130e-af2d-444a-95f0-dc29d29472b2;returning-chatter=0;room-id=62300805;emotes=;flags=;client-nonce=aa1162a8b82e3a207c60b30f4d6fd802;color=#FF0000;user-type=;first-msg=0;badge-info=;user-id=810718356;historical=1;display-name=holy4uck;badges=;turbo=0;mod=0 :holy4uck!holy4uck@holy4uck.tmi.twitch.tv PRIVMSG #nymn forsenParty","@display-name=DontCagePlebs;tmi-sent-ts=1704558068291;user-type=;rm-received-ts=1704558068460;mod=0;badges=subscriber/36,no_audio/1;flags=;emotes=;user-id=85837900;turbo=0;first-msg=0;id=a7ef22af-b446-4c59-9d6c-809264a91c7f;badge-info=subscriber/37;client-nonce=98aca8b387661f0cd1d3df60df7e1d38;historical=1;subscriber=1;room-id=62300805;color=#DAA520;returning-chatter=0 :dontcageplebs!dontcageplebs@dontcageplebs.tmi.twitch.tv PRIVMSG #nymn AlienPls","@tmi-sent-ts=1704558068636;user-id=51967700;room-id=62300805;subscriber=0;display-name=Patixxl;color=#FF0000;mod=0;rm-received-ts=1704558068812;id=91ffdfca-d356-413a-bbce-614e4a90379f;emotes=;user-type=;flags=;turbo=0;badge-info=;client-nonce=432693930b82d241e331bbfaecb23495;first-msg=0;badges=;returning-chatter=0;historical=1 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@id=356436dc-07f8-4bf9-8e78-723cf3beed96;flags=;room-id=62300805;rm-received-ts=1704558069692;display-name=Phant0mBlades;returning-chatter=0;badge-info=subscriber/9;tmi-sent-ts=1704558069520;subscriber=1;turbo=0;user-type=;user-id=278896263;first-msg=0;badges=subscriber/9,chatter-cs-go-2022/1;mod=0;color=#008000;historical=1;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10 :phant0mblades!phant0mblades@phant0mblades.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM","@tmi-sent-ts=1704558070675;client-nonce=e5ac6342f739600daed3a41552a85b31;id=5ea8f615-0025-4ef0-bce6-a835ac40cd42;returning-chatter=0;display-name=ME_ME;mod=0;user-type=;turbo=0;subscriber=1;user-id=159210800;color=#FF2424;historical=1;rm-received-ts=1704558070852;badges=subscriber/48,bits/25000;emotes=;room-id=62300805;badge-info=subscriber/49;first-msg=0;flags= :me_me!me_me@me_me.tmi.twitch.tv PRIVMSG #nymn WAYTOODANK","@client-nonce=d50ff0ab6747ea7b53ad1435feb253f3;flags=;user-type=;tmi-sent-ts=1704558070946;emotes=;display-name=Patixxl;mod=0;id=8e29dbc3-a6b7-40d7-b7e6-d0302c78662c;rm-received-ts=1704558071131;turbo=0;historical=1;returning-chatter=0;badge-info=;first-msg=0;badges=;user-id=51967700;room-id=62300805;color=#FF0000;subscriber=0 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@turbo=0;flags=;id=5a9fb2c0-4980-4fd5-8725-4dab7b2ef20c;color=#9146FF;badges=moderator/1,subscriber/60,rplace-2023/1;first-msg=0;user-type=mod;subscriber=1;emotes=;historical=1;rm-received-ts=1704558071809;tmi-sent-ts=1704558071581;display-name=Mr0lle;user-id=41157245;room-id=62300805;badge-info=subscriber/67;returning-chatter=0;mod=1 :mr0lle!mr0lle@mr0lle.tmi.twitch.tv PRIVMSG #nymn forsenParty","@emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170,176-186,192-202;badge-info=;color=#8A2BE2;tmi-sent-ts=1704558072023;turbo=0;subscriber=0;display-name=Purple_Geco;first-msg=0;id=e5eb4695-7136-4e14-b49e-7e292f9456af;mod=0;historical=1;badges=no_audio/1;user-id=253596827;rm-received-ts=1704558072207;client-nonce=931ecc7bd7b6e3e67e650415ebddb8c4;flags=;returning-chatter=0;room-id=62300805;user-type= :purple_geco!purple_geco@purple_geco.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@emotes=;subscriber=0;turbo=0;flags=;tmi-sent-ts=1704558074219;display-name=Patixxl;first-msg=0;badges=;id=29adcd97-2588-4924-b04e-32e9f0590821;room-id=62300805;mod=0;user-id=51967700;color=#FF0000;client-nonce=4a0c6f2d448f35e008475441ed1c10a9;user-type=;returning-chatter=0;rm-received-ts=1704558074385;badge-info=;historical=1 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@turbo=0;room-id=62300805;flags=;emotes=;returning-chatter=0;first-msg=0;badge-info=subscriber/5;rm-received-ts=1704558075785;subscriber=1;badges=subscriber/3,no_video/1;color=#FF0000;id=717e2ccd-bf01-4198-a0a4-846463b3d673;display-name=buong1;client-nonce=6ec2660e6b572b546e89431f6db633c0;user-type=;user-id=75144877;historical=1;tmi-sent-ts=1704558075597;mod=0 :buong1!buong1@buong1.tmi.twitch.tv PRIVMSG #nymn FEELSWAYTOODONKMAN","@tmi-sent-ts=1704558078901;returning-chatter=0;room-id=62300805;user-id=433352132;display-name=jontEmillian;badge-info=subscriber/38;emotes=;id=f41fe58c-c230-4fad-956e-7e7a83470e66;badges=subscriber/36,twitch-recap-2023/1;rm-received-ts=1704558079077;historical=1;mod=0;flags=;user-type=;first-msg=0;subscriber=1;turbo=0;color=#63BD68 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn :forsenParty 󠀀","@user-type=;display-name=Patixxl;color=#FF0000;user-id=51967700;tmi-sent-ts=1704558079301;id=3408f243-0d2c-47d4-8c0f-cc9137a57ae0;mod=0;historical=1;subscriber=0;badges=;client-nonce=77d1e635e796091ea351671916dad357;room-id=62300805;emotes=;turbo=0;rm-received-ts=1704558079500;first-msg=0;badge-info=;flags=;returning-chatter=0 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@flags=;id=a34acc3a-5915-4fe4-a560-501a1c0cce3f;badges=vip/1,subscriber/72,rplace-2023/1;returning-chatter=0;color=#D52AFF;subscriber=1;rm-received-ts=1704558080880;mod=0;user-id=87120320;room-id=62300805;user-type=;display-name=Joshlad;tmi-sent-ts=1704558080665;vip=1;turbo=0;badge-info=subscriber/77;emotes=;historical=1;first-msg=0 :joshlad!joshlad@joshlad.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@badge-info=;user-id=216144449;room-id=62300805;historical=1;subscriber=0;flags=;rm-received-ts=1704558084195;color=#00FF7F;client-nonce=ce7fde98cf9cdfa0abe9c2099c503d23;display-name=FollowProtoBuddy;mod=0;returning-chatter=0;first-msg=0;badges=turbo/1;emotes=;id=dfc2b883-94dc-475b-a811-7016eb0e3b0a;user-type=;tmi-sent-ts=1704558083997;turbo=1 :followprotobuddy!followprotobuddy@followprotobuddy.tmi.twitch.tv PRIVMSG #nymn :nimeDance forsen can i dance with you?","@mod=0;color=#DAA520;room-id=62300805;first-msg=0;badges=subscriber/36,no_audio/1;emotes=;returning-chatter=0;flags=;historical=1;id=76a4b633-d826-421b-8116-aa18aa7562e1;user-type=;user-id=85837900;subscriber=1;badge-info=subscriber/37;tmi-sent-ts=1704558089747;rm-received-ts=1704558089924;display-name=DontCagePlebs;client-nonce=5c2bbbed12d726e8b87c09e040711bf4;turbo=0 :dontcageplebs!dontcageplebs@dontcageplebs.tmi.twitch.tv PRIVMSG #nymn :Alien360 AlienPls","@first-msg=0;emotes=;badges=moderator/1,subscriber/60,rplace-2023/1;returning-chatter=0;user-id=41157245;badge-info=subscriber/67;color=#9146FF;id=b1bfc39e-e2d8-4110-b508-e3613892d091;room-id=62300805;display-name=Mr0lle;mod=1;tmi-sent-ts=1704558089791;user-type=mod;turbo=0;historical=1;subscriber=1;rm-received-ts=1704558089960;flags= :mr0lle!mr0lle@mr0lle.tmi.twitch.tv PRIVMSG #nymn forsenParty","@badge-info=;display-name=Yrmyli;room-id=62300805;user-id=51809775;color=#FF1493;turbo=0;badges=;flags=;id=c6239761-2d38-4e08-93f7-c9ed66199a9a;emotes=;user-type=;returning-chatter=0;historical=1;client-nonce=e9e19adfe26b6058df829d30846b3eb9;rm-received-ts=1704558092866;first-msg=0;subscriber=0;tmi-sent-ts=1704558092695;mod=0 :yrmyli!yrmyli@yrmyli.tmi.twitch.tv PRIVMSG #nymn :NymN what is this game about DOCING","@room-id=62300805;badge-info=subscriber/38;mod=0;historical=1;subscriber=1;returning-chatter=0;tmi-sent-ts=1704558092963;color=#63BD68;user-type=;flags=;first-msg=0;user-id=433352132;id=fe0d3af0-2f2f-46dd-9191-f2a79c1acdaf;badges=subscriber/36,twitch-recap-2023/1;rm-received-ts=1704558093158;display-name=jontEmillian;emotes=;turbo=0 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn forsenParty","@turbo=0;id=bbea25c8-0c21-46ff-ac08-c727ee27ba94;display-name=Purple_Geco;subscriber=0;returning-chatter=0;client-nonce=1bd1af296edd046efba9d68c2c963fd0;user-type=;first-msg=0;flags=;rm-received-ts=1704558093209;mod=0;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170,176-186,192-202;color=#8A2BE2;badges=no_audio/1;room-id=62300805;tmi-sent-ts=1704558093022;user-id=253596827;historical=1;badge-info= :purple_geco!purple_geco@purple_geco.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty","@tmi-sent-ts=1704558095417;subscriber=1;color=#FF2424;turbo=0;id=71d335f9-06d8-4a1f-a686-26954be8c282;flags=;rm-received-ts=1704558095595;badges=subscriber/48,bits/25000;first-msg=0;emotes=;user-type=;historical=1;mod=0;user-id=159210800;room-id=62300805;returning-chatter=0;client-nonce=41b2ecb48d1d6f49dac62e83e77c31b2;badge-info=subscriber/49;display-name=ME_ME :me_me!me_me@me_me.tmi.twitch.tv PRIVMSG #nymn :WAYTOODANK 󠀀","@tmi-sent-ts=1704558096173;returning-chatter=0;turbo=0;flags=;emotes=;badges=subscriber/54,chatter-cs-go-2022/1;display-name=h_h410;room-id=62300805;historical=1;user-type=;badge-info=subscriber/54;color=#00FF7F;mod=0;id=b82e6297-8e23-4624-bc41-d147ecdfa11a;subscriber=1;user-id=117088592;rm-received-ts=1704558096362;first-msg=0 :h_h410!h_h410@h_h410.tmi.twitch.tv PRIVMSG #nymn forseninsane","@historical=1;rm-received-ts=1704558097913;emotes=;room-id=62300805;first-msg=0;user-type=;subscriber=1;id=10d6d300-5d94-4023-aa96-63e4d979afe7;color=#008000;badge-info=subscriber/9;tmi-sent-ts=1704558097719;display-name=Phant0mBlades;mod=0;flags=;returning-chatter=0;turbo=0;badges=subscriber/9,chatter-cs-go-2022/1;user-id=278896263 :phant0mblades!phant0mblades@phant0mblades.tmi.twitch.tv PRIVMSG #nymn :EASIEST DIFFICULTY LULE","@historical=1;subscriber=1;turbo=1;client-nonce=aa81bb0d65cccf15ab8b6d21b88334ac;badges=subscriber/9,turbo/1;user-type=;display-name=Kotzblitz20;mod=0;flags=;rm-received-ts=1704558099116;color=#FFFF00;user-id=40037186;tmi-sent-ts=1704558098935;badge-info=subscriber/9;emotes=emotesv2_b7482780923442c499ae7b4706040695:0-11;emote-only=1;room-id=62300805;id=478cdd66-8ae3-4d18-98c3-cbf74f88b324;first-msg=0;returning-chatter=0 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn forsenInsane","@badge-info=;badges=bits-charity/1;mod=0;id=e7f57f9c-95d3-4989-b67e-33166b520b6a;user-type=;tmi-sent-ts=1704558100913;returning-chatter=0;client-nonce=933ec0b4089df2788a52f6f4592fd705;first-msg=0;emotes=;historical=1;color=#0000FF;display-name=Intel_power;subscriber=0;room-id=62300805;rm-received-ts=1704558101080;flags=;turbo=0;user-id=103665668 :intel_power!intel_power@intel_power.tmi.twitch.tv PRIVMSG #nymn forseninsane","@user-type=;badge-info=subscriber/49;historical=1;user-id=159210800;flags=;tmi-sent-ts=1704558101636;mod=0;rm-received-ts=1704558101845;client-nonce=530927fea61f7acb071b230d7ceec398;room-id=62300805;emotes=;turbo=0;color=#FF2424;badges=subscriber/48,bits/25000;id=02df18ce-fe5f-427c-af2b-06c7b8bc0716;returning-chatter=0;first-msg=0;subscriber=1;display-name=ME_ME :me_me!me_me@me_me.tmi.twitch.tv PRIVMSG #nymn :@Phant0mBlades OkeyL","@room-id=62300805;subscriber=1;first-msg=0;rm-received-ts=1704558102758;display-name=orange_bean;id=fafc3d5b-edb3-402d-809d-146a6110198f;emotes=;returning-chatter=0;mod=0;user-type=;badges=subscriber/48;user-id=29649547;turbo=0;color=#FF7F50;tmi-sent-ts=1704558102580;flags=;historical=1;badge-info=subscriber/53 :orange_bean!orange_bean@orange_bean.tmi.twitch.tv PRIVMSG #nymn :Ratge RaveTime","@first-msg=0;returning-chatter=0;rm-received-ts=1704558103260;user-type=;subscriber=1;emotes=;badge-info=subscriber/9;room-id=62300805;flags=;mod=0;badges=subscriber/9,turbo/1;color=#FFFF00;turbo=1;id=a3cbf83a-e2af-4daa-8b8b-63feb024c3fa;client-nonce=76d8b591141374cfd23a66eedc6af5b8;historical=1;tmi-sent-ts=1704558103084;display-name=Kotzblitz20;user-id=40037186 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn AlienTechno","@user-id=51967700;mod=0;color=#FF0000;id=df8888b0-05b0-49e4-8a0f-2d2080bbdfcc;subscriber=0;emotes=;first-msg=0;historical=1;returning-chatter=0;rm-received-ts=1704558104943;user-type=;room-id=62300805;badges=;badge-info=;client-nonce=4b8d9fa638d86eb58e33bf553008d257;flags=;turbo=0;display-name=Patixxl;tmi-sent-ts=1704558104749 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@user-type=;room-id=62300805;turbo=0;first-msg=0;rm-received-ts=1704558105052;subscriber=1;badge-info=subscriber/38;returning-chatter=0;id=9f7462d5-3a41-470c-92d9-da8a68e230e7;display-name=jontEmillian;emotes=;badges=subscriber/36,twitch-recap-2023/1;flags=;user-id=433352132;color=#63BD68;mod=0;tmi-sent-ts=1704558104870;historical=1 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn :forsenParty 󠀀","@first-msg=0;subscriber=1;user-type=;tmi-sent-ts=1704558106145;badges=subscriber/42,bits/100;badge-info=subscriber/45;turbo=0;color=#FFFFFF;room-id=62300805;mod=0;emotes=;historical=1;user-id=38936358;id=3e2e6027-28dd-4d66-b233-06427ee9bbdf;returning-chatter=0;client-nonce=d9f7659de8c660422d19370372c8dcf0;rm-received-ts=1704558106335;display-name=havelsring;flags= :havelsring!havelsring@havelsring.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@display-name=Mr0lle;user-type=mod;subscriber=1;rm-received-ts=1704558106631;room-id=62300805;returning-chatter=0;badge-info=subscriber/67;first-msg=0;user-id=41157245;tmi-sent-ts=1704558106459;historical=1;mod=1;badges=moderator/1,subscriber/60,rplace-2023/1;turbo=0;color=#9146FF;flags=;id=62162402-8ca9-47a8-bbc6-8bd2518ce4ea;emotes= :mr0lle!mr0lle@mr0lle.tmi.twitch.tv PRIVMSG #nymn forsenParty","@rm-received-ts=1704558106934;badge-info=subscriber/9;subscriber=1;returning-chatter=0;display-name=Phant0mBlades;flags=;historical=1;first-msg=0;user-type=;user-id=278896263;tmi-sent-ts=1704558106755;room-id=62300805;turbo=0;emotes=;id=07c7bc32-48ed-49c9-883e-929f344ff3cb;mod=0;badges=subscriber/9,chatter-cs-go-2022/1;color=#008000 :phant0mblades!phant0mblades@phant0mblades.tmi.twitch.tv PRIVMSG #nymn :@ME_ME dankHug","@subscriber=1;first-msg=0;badge-info=subscriber/37;returning-chatter=0;color=#DAA520;rm-received-ts=1704558108060;user-id=85837900;room-id=62300805;display-name=DontCagePlebs;turbo=0;mod=0;emotes=;id=fa6bcb9b-80c2-4ea3-be8b-212561e1961c;client-nonce=2040f9e5f6aa7f3e50f628eff46089b8;historical=1;user-type=;tmi-sent-ts=1704558107879;badges=subscriber/36,no_audio/1;flags= :dontcageplebs!dontcageplebs@dontcageplebs.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@rm-received-ts=1704558108101;tmi-sent-ts=1704558107914;subscriber=0;user-type=;first-msg=0;emotes=;room-id=62300805;user-id=51967700;badges=;client-nonce=f388628b820df921fdff23d3932c5b3f;mod=0;badge-info=;turbo=0;display-name=Patixxl;id=69b8945f-fee9-45d3-8a83-d2bdede8dbd7;flags=;color=#FF0000;returning-chatter=0;historical=1 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@display-name=Joshlad;flags=;user-id=87120320;color=#D52AFF;first-msg=0;emotes=;tmi-sent-ts=1704558108067;turbo=0;mod=0;historical=1;rm-received-ts=1704558108240;badge-info=subscriber/77;room-id=62300805;returning-chatter=0;user-type=;id=9fcc43d8-19e9-4ba4-9131-0c447ddc5121;badges=vip/1,subscriber/72,rplace-2023/1;subscriber=1;vip=1 :joshlad!joshlad@joshlad.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@client-nonce=1dd74c3a13c350b2fc76d1588ade7cac;display-name=crazyjuni0r_;historical=1;user-id=222340799;user-type=;id=20ec0428-8f47-4792-ac3d-6d8b99af1b2e;badges=subscriber/6,chatter-cs-go-2022/1;turbo=0;color=#B22222;emote-only=1;first-msg=0;subscriber=1;flags=;rm-received-ts=1704558109005;returning-chatter=0;room-id=62300805;tmi-sent-ts=1704558108791;emotes=emotesv2_d6be8456d996420c81d1fb0791cf9d20:0-8;mod=0;badge-info=subscriber/7 :crazyjuni0r_!crazyjuni0r_@crazyjuni0r_.tmi.twitch.tv PRIVMSG #nymn pspGAGAGA","@room-id=62300805;id=6c9d74be-278e-45d1-a02f-f4e0198a0cf7;user-type=;returning-chatter=0;mod=0;flags=;display-name=holy4uck;historical=1;rm-received-ts=1704558109145;emotes=;client-nonce=f6ef7e9c4c0d08e5b5a9e307a6a9dc90;subscriber=0;turbo=0;color=#FF0000;tmi-sent-ts=1704558108993;badges=;user-id=810718356;first-msg=0;badge-info= :holy4uck!holy4uck@holy4uck.tmi.twitch.tv PRIVMSG #nymn :forsenParty 󠀀","@id=1f8ac249-12f0-4830-8674-83c285f6d691;room-id=62300805;turbo=0;color=#23CE3F;user-id=190828982;badge-info=subscriber/20;mod=0;historical=1;first-msg=0;client-nonce=d58ae9a7bf695bc6cb2410a39473a205;returning-chatter=0;display-name=Near____________;flags=;badges=subscriber/18,twitch-recap-2023/1;tmi-sent-ts=1704558109027;rm-received-ts=1704558109208;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10/emotesv2_2ce848d8d4cb42cbb94ba47b9dd8183e:12-18;user-type=;subscriber=1 :near____________!near____________@near____________.tmi.twitch.tv PRIVMSG #nymn :forsenParty nymnEDM 󠀀","@first-msg=0;badges=subscriber/9,turbo/1;mod=0;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170,176-186,192-202;returning-chatter=0;id=f2884bb9-aede-420d-8014-6e5b9e8a882f;user-id=40037186;historical=1;turbo=1;badge-info=subscriber/9;subscriber=1;user-type=;display-name=Kotzblitz20;rm-received-ts=1704558110532;room-id=62300805;color=#FFFF00;tmi-sent-ts=1704558110342;flags=;client-nonce=dcb8db1dc43eb2c1ed7eda2eccfbd668 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@first-msg=0;id=b672890e-ecce-4bbc-ba7d-106b2e653337;client-nonce=be33080321bb65eb3d501c4257e831bc;historical=1;user-type=;mod=0;rm-received-ts=1704558112028;color=#FF2424;turbo=0;flags=;tmi-sent-ts=1704558111835;subscriber=1;emotes=;badges=subscriber/48,bits/25000;display-name=ME_ME;badge-info=subscriber/49;room-id=62300805;user-id=159210800;returning-chatter=0 :me_me!me_me@me_me.tmi.twitch.tv PRIVMSG #nymn DOCING","@id=1cd4847c-c6da-4481-8067-d9fbf886b00e;subscriber=0;display-name=KelemvorUber;mod=0;client-nonce=1ff64bcdbc10f1e42446b57629301a59;returning-chatter=0;user-type=;historical=1;turbo=0;tmi-sent-ts=1704558112134;room-id=62300805;user-id=38635616;first-msg=0;badges=twitch-recap-2023/1;flags=;badge-info=;emotes=;rm-received-ts=1704558112311;color=#F7FF00 :kelemvoruber!kelemvoruber@kelemvoruber.tmi.twitch.tv PRIVMSG #nymn OMEGALUOL","@subscriber=1;color=#FFFF00;room-id=62300805;display-name=Kotzblitz20;historical=1;first-msg=0;tmi-sent-ts=1704558112919;flags=;rm-received-ts=1704558113117;id=869622c8-24d3-4045-803f-bab8ec374ac9;badge-info=subscriber/9;turbo=1;user-id=40037186;returning-chatter=0;client-nonce=5d8d0e1100a601806753709196af91ca;mod=0;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122;user-type=;badges=subscriber/9,turbo/1 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@client-nonce=8f154789fffb86cef0845261984a02d3;color=#0000FF;id=4e00f786-c01f-4f84-a9d6-79bb688b9a70;subscriber=1;display-name=SnuggleUncle;mod=0;first-msg=0;turbo=0;room-id=62300805;badges=subscriber/12,twitch-recap-2023/1;user-id=69072013;rm-received-ts=1704558113467;emotes=;tmi-sent-ts=1704558113280;badge-info=subscriber/17;flags=;historical=1;returning-chatter=0;user-type= :snuggleuncle!snuggleuncle@snuggleuncle.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@user-type=;rm-received-ts=1704558113972;id=fffbdd14-0932-4b3e-b5a8-9feecfca5b00;badge-info=;user-id=92830211;room-id=62300805;client-nonce=4ff9af2ca6ca3ace7916c46b5d76aa0a;mod=0;tmi-sent-ts=1704558113791;returning-chatter=0;emotes=;color=#71A1E6;subscriber=0;turbo=0;historical=1;first-msg=0;badges=glhf-pledge/1;flags=;display-name=soran2202 :soran2202!soran2202@soran2202.tmi.twitch.tv PRIVMSG #nymn :FeelsDankMan 👋 helo","@rm-received-ts=1704558114227;mod=0;id=3a646610-8601-4347-aad0-2e52075a5c72;tmi-sent-ts=1704558114068;emotes=;user-id=51967700;turbo=0;color=#FF0000;display-name=Patixxl;client-nonce=942344bcb85638fd942b3d34d69481f9;flags=;historical=1;returning-chatter=0;first-msg=0;room-id=62300805;user-type=;subscriber=0;badges=;badge-info= :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@flags=;subscriber=1;emotes=;turbo=0;color=#63BD68;returning-chatter=0;badges=subscriber/36,twitch-recap-2023/1;badge-info=subscriber/38;first-msg=0;mod=0;room-id=62300805;user-id=433352132;user-type=;tmi-sent-ts=1704558114040;id=d8ada0fb-57b5-4f1e-b0cf-c4ba723444ba;historical=1;display-name=jontEmillian;rm-received-ts=1704558114231 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn forsenParty","@rm-received-ts=1704558115440;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10;subscriber=0;tmi-sent-ts=1704558115231;badges=;room-id=62300805;user-id=151423066;mod=0;user-type=;first-msg=0;id=4042d549-74dd-478f-9e7e-50348ff731f1;badge-info=;returning-chatter=0;turbo=0;flags=;emote-only=1;color=#FF69B4;historical=1;display-name=forsenkkona_ :forsenkkona_!forsenkkona_@forsenkkona_.tmi.twitch.tv PRIVMSG #nymn forsenParty","@badge-info=subscriber/9;mod=0;user-type=;turbo=1;display-name=Kotzblitz20;client-nonce=0612841dc2ae04f4ef6c314874ae4f5c;historical=1;flags=;tmi-sent-ts=1704558115773;rm-received-ts=1704558115968;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170,176-186,192-202;subscriber=1;room-id=62300805;id=beec82dd-3a40-4ddb-a7ea-5e41dfe21a4b;user-id=40037186;first-msg=0;returning-chatter=0;badges=subscriber/9,turbo/1;color=#FFFF00 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@user-type=;tmi-sent-ts=1704558116560;turbo=0;color=#1E90FF;flags=;returning-chatter=0;badges=;historical=1;id=921877aa-e334-4ac2-afcb-d740bcbc9cb8;subscriber=0;client-nonce=7009d4d14b1369080a2920763da2f62a;display-name=zzlint;badge-info=;room-id=62300805;user-id=29764188;rm-received-ts=1704558116727;emotes=;first-msg=0;mod=0 :zzlint!zzlint@zzlint.tmi.twitch.tv PRIVMSG #nymn :a rave in a sewer","@color=#9146FF;returning-chatter=0;emotes=;mod=1;display-name=Mr0lle;historical=1;flags=;room-id=62300805;user-id=41157245;turbo=0;subscriber=1;first-msg=0;badges=moderator/1,subscriber/60,rplace-2023/1;badge-info=subscriber/67;id=e4c0b050-a28c-40a8-a467-a41a44dc76c6;tmi-sent-ts=1704558116591;user-type=mod;rm-received-ts=1704558116766 :mr0lle!mr0lle@mr0lle.tmi.twitch.tv PRIVMSG #nymn forsenParty","@badge-info=;historical=1;tmi-sent-ts=1704558117029;emotes=;first-msg=0;client-nonce=f11d8a2495d2d74f7c7a71653825cb8d;color=#FF0000;room-id=62300805;display-name=Patixxl;badges=;rm-received-ts=1704558117219;turbo=0;user-id=51967700;user-type=;subscriber=0;id=567c72af-0add-4cba-9698-7cedf86bf10f;returning-chatter=0;mod=0;flags= :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@room-id=62300805;client-nonce=4a4f92047f7193581f8a335964b97731;mod=1;first-msg=0;returning-chatter=0;display-name=Storm;user-type=mod;tmi-sent-ts=1704558117901;rm-received-ts=1704558118091;flags=;badge-info=;historical=1;turbo=1;user-id=13107998;badges=moderator/1,turbo/1;id=123d26df-aa30-4e43-b30d-7df96fb54b92;color=#376180;subscriber=0;emotes=90076:6-17 :storm!storm@storm.tmi.twitch.tv PRIVMSG #nymn :Ratge StinkyCheese","@subscriber=1;display-name=Phant0mBlades;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10;user-id=278896263;rm-received-ts=1704558118197;turbo=0;user-type=;badges=subscriber/9,chatter-cs-go-2022/1;flags=;badge-info=subscriber/9;first-msg=0;tmi-sent-ts=1704558118021;room-id=62300805;historical=1;mod=0;id=2146a4d9-d9a3-4c11-95ba-198755ec9074;color=#008000;returning-chatter=0 :phant0mblades!phant0mblades@phant0mblades.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM","@user-type=;returning-chatter=0;user-id=216144449;historical=1;display-name=FollowProtoBuddy;color=#00FF7F;id=d14b661e-a9fa-4d23-b911-9b26332c43bc;turbo=1;first-msg=0;client-nonce=6dea2760e96d24918be2fdd4c28f49d0;subscriber=0;badge-info=;room-id=62300805;badges=turbo/1;flags=;emotes=;tmi-sent-ts=1704558118561;rm-received-ts=1704558118750;mod=0 :followprotobuddy!followprotobuddy@followprotobuddy.tmi.twitch.tv PRIVMSG #nymn nimeDance","@first-msg=0;user-type=;returning-chatter=0;emotes=;room-id=62300805;subscriber=1;display-name=jontEmillian;user-id=433352132;badge-info=subscriber/38;color=#63BD68;id=98d8adef-151e-4ef1-8f40-fa8152f23dac;flags=;mod=0;rm-received-ts=1704558123091;badges=subscriber/36,twitch-recap-2023/1;tmi-sent-ts=1704558122930;turbo=0;historical=1 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn :forsenParty 󠀀","@emotes=;display-name=Kotzblitz20;color=#FFFF00;client-nonce=f873fc68fe513770ce0985f634769824;turbo=1;subscriber=1;first-msg=0;flags=;user-type=;user-id=40037186;returning-chatter=0;tmi-sent-ts=1704558123415;badge-info=subscriber/9;rm-received-ts=1704558123591;id=f8080f7b-972a-4212-87ca-b42c472df804;mod=0;badges=subscriber/9,turbo/1;room-id=62300805;historical=1 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn ClueLookingAtYou","@first-msg=0;user-id=133862911;reply-parent-msg-id=fffbdd14-0932-4b3e-b5a8-9feecfca5b00;reply-thread-parent-msg-id=fffbdd14-0932-4b3e-b5a8-9feecfca5b00;returning-chatter=0;tmi-sent-ts=1704558128218;user-type=;reply-thread-parent-user-id=92830211;reply-thread-parent-display-name=soran2202;reply-thread-parent-user-login=soran2202;badges=;reply-parent-user-login=soran2202;historical=1;rm-received-ts=1704558128399;id=be3a2cb7-25c0-4ac6-b2b7-475a19ca71ef;color=#FF0000;display-name=123homo;reply-parent-display-name=soran2202;reply-parent-msg-body=FeelsDankMan\\s👋\\shelo;client-nonce=f14fd90ead77c4c5679338e9aaec09cf;emotes=;flags=;turbo=0;subscriber=0;room-id=62300805;mod=0;badge-info=;reply-parent-user-id=92830211 :123homo!123homo@123homo.tmi.twitch.tv PRIVMSG #nymn :@soran2202 sup dude how are ya","@user-id=159210800;returning-chatter=0;emotes=;historical=1;first-msg=0;badges=subscriber/48,bits/25000;client-nonce=0717802bff8668a612aa1810edd143ea;color=#FF2424;id=849e739f-ca10-4982-8b70-5bb4c53ff494;badge-info=subscriber/49;subscriber=1;flags=;rm-received-ts=1704558128625;room-id=62300805;user-type=;display-name=ME_ME;mod=0;turbo=0;tmi-sent-ts=1704558128427 :me_me!me_me@me_me.tmi.twitch.tv PRIVMSG #nymn forsenUnpleased","@emotes=;id=6236b23e-cb57-4cb0-adc9-72e737641a88;color=#0000FF;first-msg=0;turbo=0;flags=;client-nonce=d72d284b9e47c73f597cb8dcb527d344;badges=subscriber/12,twitch-recap-2023/1;subscriber=1;badge-info=subscriber/17;historical=1;user-type=;tmi-sent-ts=1704558129219;display-name=SnuggleUncle;returning-chatter=0;rm-received-ts=1704558129405;mod=0;user-id=69072013;room-id=62300805 :snuggleuncle!snuggleuncle@snuggleuncle.tmi.twitch.tv PRIVMSG #nymn ClueLookingAtYou","@subscriber=0;id=456695c7-594f-4a6c-8d8e-c78cb652f5bc;returning-chatter=0;historical=1;tmi-sent-ts=1704558129719;room-id=62300805;flags=;color=#9ACD32;user-id=135853293;badge-info=;emotes=;badges=twitch-recap-2023/1;turbo=0;client-nonce=50c0e3264f5749dfa75ee22bde206fdc;display-name=theKiryu;rm-received-ts=1704558129892;user-type=;first-msg=0;mod=0 :thekiryu!thekiryu@thekiryu.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@returning-chatter=0;turbo=0;flags=;rm-received-ts=1704558129914;user-type=;first-msg=0;historical=1;subscriber=0;emotes=;mod=0;user-id=137332535;tmi-sent-ts=1704558129728;color=#1E90FF;client-nonce=2e7353fc46a525f34fd8fb0cab8e7bac;display-name=BlueAves;badge-info=;badges=;id=f99d8b6a-ae72-46a4-8c65-a92ca670abb0;room-id=62300805 :blueaves!blueaves@blueaves.tmi.twitch.tv PRIVMSG #nymn :nymn im giving you mufflo meat but its not coming","@display-name=DontCagePlebs;first-msg=0;returning-chatter=0;flags=;room-id=62300805;mod=0;historical=1;user-type=;user-id=85837900;badge-info=subscriber/37;subscriber=1;badges=subscriber/36,no_audio/1;turbo=0;emotes=;color=#DAA520;rm-received-ts=1704558131276;id=ac94948c-32e4-4e41-a152-d1afff1e2a62;client-nonce=774787c7e9b0580060eccf9f9fddc647;tmi-sent-ts=1704558131102 :dontcageplebs!dontcageplebs@dontcageplebs.tmi.twitch.tv PRIVMSG #nymn Listening","@subscriber=1;historical=1;display-name=Kotzblitz20;user-type=;emotes=;mod=0;color=#FFFF00;user-id=40037186;badge-info=subscriber/9;rm-received-ts=1704558131559;tmi-sent-ts=1704558131385;room-id=62300805;flags=;id=3321bb7c-4647-4247-95fd-1de4311400e3;turbo=1;client-nonce=4ce84653605b19585da296e666fc16c1;first-msg=0;badges=subscriber/9,turbo/1;returning-chatter=0 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :ClueLookingAtYou ClueLookingAtYou","@emotes=;user-type=mod;badges=moderator/1,subscriber/60,rplace-2023/1;rm-received-ts=1704558133957;tmi-sent-ts=1704558133729;badge-info=subscriber/67;display-name=Mr0lle;room-id=62300805;id=bf1b6466-f55c-455c-b575-87cb8c5c2c09;first-msg=0;historical=1;turbo=0;user-id=41157245;subscriber=1;mod=1;flags=;color=#9146FF;returning-chatter=0 :mr0lle!mr0lle@mr0lle.tmi.twitch.tv PRIVMSG #nymn Despair","@emotes=;user-type=;tmi-sent-ts=1704558133959;user-id=46691465;mod=0;rm-received-ts=1704558134180;badges=twitch-recap-2023/1;returning-chatter=0;flags=;subscriber=0;badge-info=;first-msg=0;turbo=0;historical=1;display-name=DeeBeeZenron;id=31ca603d-8630-4208-8231-51a47ec4cf44;color=#9ACD32;room-id=62300805 :deebeezenron!deebeezenron@deebeezenron.tmi.twitch.tv PRIVMSG #nymn forsenUnpleased","@user-type=;badge-info=subscriber/4;returning-chatter=0;mod=0;emotes=;room-id=62300805;color=#8A2BE2;badges=subscriber/3,no_audio/1;id=dbd0a1b3-b17c-496a-a2c8-d9c2b05b7308;tmi-sent-ts=1704558134055;subscriber=1;turbo=0;historical=1;first-msg=0;display-name=pleasekeepconnor6silly;user-id=137782780;flags=;client-nonce=f0669459051424941fe0945f80d2626b;rm-received-ts=1704558134275 :pleasekeepconnor6silly!pleasekeepconnor6silly@pleasekeepconnor6silly.tmi.twitch.tv PRIVMSG #nymn forsenUnpleased","@user-id=103592036;subscriber=1;rm-received-ts=1704558135243;first-msg=0;emotes=;tmi-sent-ts=1704558135041;returning-chatter=0;turbo=0;badges=subscriber/54,bits/1000;historical=1;id=d106af6b-bdfe-4050-8a68-4b1529637c64;client-nonce=a786cae81197eb879d978fe02e9c4ddc;mod=0;flags=;badge-info=subscriber/55;display-name=SecretCarrot;color=#00615C;room-id=62300805;user-type= :secretcarrot!secretcarrot@secretcarrot.tmi.twitch.tv PRIVMSG #nymn :Okayeg 💢 ❓","@rm-received-ts=1704558136142;id=67a07fca-5f86-4560-96c4-f505812abba9;first-msg=0;subscriber=1;badge-info=subscriber/101;emotes=;mod=1;user-type=mod;color=#FF69B4;flags=;tmi-sent-ts=1704558135943;turbo=0;returning-chatter=0;display-name=botnextdoor;badges=moderator/1,subscriber/72;user-id=97661864;room-id=62300805;historical=1 :botnextdoor!botnextdoor@botnextdoor.tmi.twitch.tv PRIVMSG #nymn :\u0001ACTION Make sure to subscribe to NymN's YouTube channels: Stream channel - youtube.com/nymnion | Music channel - youtube.com/nymnhs | Clips channel - https://www.youtube.com/c/dailydoseofnymnion\u0001","@client-nonce=164bd91e537dc2c2213751e683638c86;mod=0;first-msg=0;room-id=62300805;rm-received-ts=1704558137343;flags=;badge-info=subscriber/9;badges=subscriber/9,turbo/1;user-id=40037186;historical=1;user-type=;color=#FFFF00;emotes=;display-name=Kotzblitz20;subscriber=1;tmi-sent-ts=1704558137149;returning-chatter=0;turbo=1;id=b0972df5-8409-4dff-a353-005426f16a56 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn Listening","@mod=0;badge-info=subscriber/49;turbo=0;tmi-sent-ts=1704558138793;display-name=ME_ME;returning-chatter=0;badges=subscriber/48,bits/25000;first-msg=0;user-type=;subscriber=1;id=78834a53-610c-4e25-b8df-bbd3e872b0b3;historical=1;rm-received-ts=1704558138954;flags=;client-nonce=798e7ce7b1a53595156cd82d71b94492;emotes=;user-id=159210800;room-id=62300805;color=#FF2424 :me_me!me_me@me_me.tmi.twitch.tv PRIVMSG #nymn :forsenUnpleased 󠀀","@turbo=0;user-type=mod;display-name=Mr0lle;badge-info=subscriber/67;rm-received-ts=1704558139453;badges=moderator/1,subscriber/60,rplace-2023/1;returning-chatter=0;emotes=emotesv2_0d9a7f5d38e44c2ca8eb96ab0e3e380a:0-9;first-msg=0;tmi-sent-ts=1704558139264;color=#9146FF;mod=1;historical=1;flags=;id=ebec3d7c-f667-447e-8905-6a16ecb974ca;subscriber=1;room-id=62300805;user-id=41157245;emote-only=1 :mr0lle!mr0lle@mr0lle.tmi.twitch.tv PRIVMSG #nymn k4yDespair","@mod=0;returning-chatter=0;emotes=;historical=1;user-id=433352132;flags=;id=ae3999cf-116f-4c4a-b925-b3f6865d33cb;turbo=0;color=#63BD68;tmi-sent-ts=1704558143888;rm-received-ts=1704558144062;room-id=62300805;badges=subscriber/36,twitch-recap-2023/1;badge-info=subscriber/38;user-type=;display-name=jontEmillian;subscriber=1;first-msg=0 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn Life","@flags=;badge-info=subscriber/9;color=#FFFF00;tmi-sent-ts=1704558146666;user-id=40037186;room-id=62300805;historical=1;first-msg=0;client-nonce=92cd04d209150071f4934ca527be7080;rm-received-ts=1704558146849;badges=subscriber/9,turbo/1;display-name=Kotzblitz20;mod=0;returning-chatter=0;id=674bbe3c-d6b4-4edc-827f-329110fe1f03;subscriber=1;turbo=1;user-type=;emotes= :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn OOOO","@badges=subscriber/54,chatter-cs-go-2022/1;user-type=;first-msg=0;display-name=h_h410;returning-chatter=0;badge-info=subscriber/54;emotes=;historical=1;subscriber=1;rm-received-ts=1704558147145;flags=;id=e3d2928d-ef22-4334-9f3a-0232c46c525c;mod=0;turbo=0;user-id=117088592;room-id=62300805;tmi-sent-ts=1704558146971;color=#00FF7F :h_h410!h_h410@h_h410.tmi.twitch.tv PRIVMSG #nymn docNOWAY","@tmi-sent-ts=1704558147594;user-type=;client-nonce=d7fb0bba3cd460c0d7f8675e5292e4a5;badge-info=;display-name=ALotOfChickens;historical=1;subscriber=0;id=f28ad525-b97c-45df-8d01-c9be42762cb7;room-id=62300805;rm-received-ts=1704558147812;flags=;color=#10E2E2;first-msg=0;turbo=0;mod=0;badges=twitch-recap-2023/1;emotes=;returning-chatter=0;user-id=167633177 :alotofchickens!alotofchickens@alotofchickens.tmi.twitch.tv PRIVMSG #nymn PagMan","@display-name=SadRosh;historical=1;badge-info=;color=#B22222;room-id=62300805;mod=0;tmi-sent-ts=1704558147995;turbo=0;emotes=;user-id=184644555;id=42f7454e-a8f8-4a91-afca-af752bc76328;user-type=;first-msg=0;subscriber=0;rm-received-ts=1704558148178;badges=no_audio/1;returning-chatter=0;flags=;client-nonce=c2e8282f6fcebdddec8042d4dfcca95c :sadrosh!sadrosh@sadrosh.tmi.twitch.tv PRIVMSG #nymn huge","@tmi-sent-ts=1704558148029;room-id=62300805;subscriber=1;flags=;display-name=SecretCarrot;badge-info=subscriber/55;user-id=103592036;returning-chatter=0;client-nonce=af073bd1fd381f926cb180e49bdca822;turbo=0;rm-received-ts=1704558148214;color=#00615C;emotes=;first-msg=0;id=c505db6e-e3b1-45bc-a00f-0aca1c570f1e;mod=0;badges=subscriber/54,bits/1000;historical=1;user-type= :secretcarrot!secretcarrot@secretcarrot.tmi.twitch.tv PRIVMSG #nymn docOOOO","@display-name=Phant0mBlades;historical=1;flags=;tmi-sent-ts=1704558148552;returning-chatter=0;user-type=;badge-info=subscriber/9;color=#008000;user-id=278896263;badges=subscriber/9,chatter-cs-go-2022/1;id=a4c82c4d-1871-41b8-8cb3-8c3e1f8b1cf5;turbo=0;emotes=;first-msg=0;rm-received-ts=1704558148748;room-id=62300805;subscriber=1;mod=0 :phant0mblades!phant0mblades@phant0mblades.tmi.twitch.tv PRIVMSG #nymn forsenUnpleased","@emotes=;subscriber=1;historical=1;color=#DAA520;returning-chatter=0;mod=0;user-id=85837900;id=a72c5f44-fbbd-4a6d-9593-2bd8dbe0ccdb;user-type=;client-nonce=b90e7167c7c1bc168ed6673faf28133a;flags=;room-id=62300805;tmi-sent-ts=1704558148576;rm-received-ts=1704558148761;turbo=0;first-msg=0;badge-info=subscriber/37;display-name=DontCagePlebs;badges=subscriber/36,no_audio/1 :dontcageplebs!dontcageplebs@dontcageplebs.tmi.twitch.tv PRIVMSG #nymn OOOO","@flags=;id=d746ad18-260c-4bd1-93d1-2913a9c767ff;user-id=75144877;display-name=buong1;emotes=;client-nonce=bc28711f3e54c6136f069450ec262550;turbo=0;badge-info=subscriber/5;color=#FF0000;mod=0;tmi-sent-ts=1704558149119;rm-received-ts=1704558149297;first-msg=0;historical=1;subscriber=1;badges=subscriber/3,no_video/1;user-type=;room-id=62300805;returning-chatter=0 :buong1!buong1@buong1.tmi.twitch.tv PRIVMSG #nymn OOOO","@turbo=0;subscriber=1;badge-info=subscriber/17;mod=0;user-type=;user-id=69072013;id=b6cdf67a-cd09-45bf-bfe6-ee2fd9fee34d;rm-received-ts=1704558149654;first-msg=0;room-id=62300805;color=#0000FF;display-name=SnuggleUncle;badges=subscriber/12,twitch-recap-2023/1;emotes=;flags=;historical=1;tmi-sent-ts=1704558149411;client-nonce=12dc059fabb70f70176246b246a4e830;returning-chatter=0 :snuggleuncle!snuggleuncle@snuggleuncle.tmi.twitch.tv PRIVMSG #nymn OOOO","@mod=0;subscriber=1;first-msg=0;client-nonce=caeea8326e960d1f46d719c4d883bca9;color=#00FF7F;flags=;id=1c8d6fa8-2873-4a88-b70f-0cbde7f1565c;returning-chatter=0;tmi-sent-ts=1704558150299;user-id=80542722;room-id=62300805;turbo=0;badges=subscriber/0,no_video/1;emotes=;user-type=;rm-received-ts=1704558150499;display-name=jqxlol;historical=1;badge-info=subscriber/2 :jqxlol!jqxlol@jqxlol.tmi.twitch.tv PRIVMSG #nymn :Okayeg 💢 MUZIKA ? Okayeg 💢 MUZIKA ? Okayeg 💢 MUZIKA ? Okayeg 💢 MUZIKA ? Okayeg 💢 MUZIKA ? Okayeg 💢 MUZIKA ?","@historical=1;subscriber=0;color=#5F9EA0;flags=;user-type=;emotes=;client-nonce=7c4ab09671c546fbbe2da7bdb055cbbd;turbo=0;room-id=62300805;user-id=133344079;badges=no_audio/1;badge-info=;returning-chatter=0;rm-received-ts=1704558150898;display-name=sehtt_;mod=0;tmi-sent-ts=1704558150717;first-msg=0;id=cdc24373-8268-44d6-828c-d530d28bc990 :sehtt_!sehtt_@sehtt_.tmi.twitch.tv PRIVMSG #nymn docNOWAY","@display-name=Duchene;color=#000000;returning-chatter=0;historical=1;id=d33671cc-fd23-4a88-8599-11bb963c92b9;tmi-sent-ts=1704558151096;client-nonce=2745de0d32d3ec4c7475e8a76983c5eb;turbo=0;user-type=;emotes=;badge-info=;badges=;flags=;first-msg=0;subscriber=0;user-id=205837377;mod=0;room-id=62300805;rm-received-ts=1704558151306 :duchene!duchene@duchene.tmi.twitch.tv PRIVMSG #nymn OOOO","@emotes=;returning-chatter=0;display-name=mnqn18;historical=1;color=#1E90FF;client-nonce=f6c25ba5948cbdfde77aebb22b3e120b;subscriber=1;badges=subscriber/0,premium/1;tmi-sent-ts=1704558151975;room-id=62300805;badge-info=subscriber/2;user-id=474204887;first-msg=0;id=bbd4d3a6-e5a3-4de5-bc94-5605cab79779;flags=;turbo=0;user-type=;rm-received-ts=1704558152450;mod=0 :mnqn18!mnqn18@mnqn18.tmi.twitch.tv PRIVMSG #nymn WHAT","@rm-received-ts=1704558152841;tmi-sent-ts=1704558152670;flags=;badge-info=subscriber/49;room-id=62300805;emotes=;color=#FF2424;badges=subscriber/48,bits/25000;client-nonce=2c8d17466ae0eddbaa909c8b5ffa9b65;subscriber=1;turbo=0;returning-chatter=0;display-name=ME_ME;first-msg=0;user-type=;historical=1;user-id=159210800;id=0f1d74ee-9cd6-4351-8617-de6124d8a50d;mod=0 :me_me!me_me@me_me.tmi.twitch.tv PRIVMSG #nymn Ratge","@display-name=orange_bean;emotes=;tmi-sent-ts=1704558152884;first-msg=0;mod=0;historical=1;badge-info=subscriber/53;rm-received-ts=1704558153074;id=e9be9aee-2002-4b98-8126-ec989b53cfad;user-type=;turbo=0;color=#FF7F50;user-id=29649547;subscriber=1;badges=subscriber/48;flags=;room-id=62300805;returning-chatter=0 :orange_bean!orange_bean@orange_bean.tmi.twitch.tv PRIVMSG #nymn Ratge","@badge-info=subscriber/20;subscriber=1;turbo=0;flags=;color=#1E90FF;first-msg=0;client-nonce=ebf666d318a252d4be0f2d3229c3f4a4;rm-received-ts=1704558153864;mod=0;user-type=;user-id=80236449;display-name=any_plinkers;tmi-sent-ts=1704558153679;emotes=;room-id=62300805;id=c06cb711-fdc6-403d-93bd-ce8d651ebd1b;historical=1;returning-chatter=0;badges=subscriber/18 :any_plinkers!any_plinkers@any_plinkers.tmi.twitch.tv PRIVMSG #nymn Ratge","@flags=;subscriber=0;emotes=;client-nonce=ec0e4680c1d1a19a9250dd1a9195abbf;first-msg=0;color=#25E000;user-id=63372784;badge-info=;historical=1;turbo=0;tmi-sent-ts=1704558153676;badges=bits/100;returning-chatter=0;rm-received-ts=1704558153864;room-id=62300805;user-type=;mod=0;id=9ea5efa5-362c-4807-8489-ed7d921d8d5f;display-name=DM8917 :dm8917!dm8917@dm8917.tmi.twitch.tv PRIVMSG #nymn Ratge","@user-type=;badge-info=subscriber/9;emotes=;mod=0;historical=1;rm-deleted=1;room-id=62300805;flags=;returning-chatter=0;tmi-sent-ts=1704558153853;turbo=0;user-id=423574282;subscriber=1;first-msg=0;display-name=e7om;rm-received-ts=1704558154050;color=#D2FFD6;badges=subscriber/9,gold-pixel-heart/1;id=7b9dd1f1-4278-4d86-8c60-8cd333e6578f :e7om!e7om@e7om.tmi.twitch.tv PRIVMSG #nymn :⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠿⠿⠿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ ⣿⣿⣿⣿⣿⣿⡿⠋⠄⠄⠄⠄⠄⠄⠙⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ ⣿⣿⣿⣿⡿⠋⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⣿⣿⡏⢉⡙⢻⡉⢉⡏⢉⡙⢻⣿⣿ ⣿⣿⣿⡿⠁⠄⢀⣰⣶⣾⣿⣿⣿⣿⣦⠄⣿⣿⡇⢠⡀⢿⠇⠸⡇⢨⣥⣾⣿⣿ ⣿⣿⣿⣇⠄⠄⣾⣿⣿⠿⢿⣿⣿⠿⠿⠇⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ ⣿⣿⣿⣿⡄⠘⣿⣿⣐⣲⣤⣿⣧⣬⣥⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ ⣿⣿⣿⣿⣯⣢⣿⣿⣿⣿⣿⡿⢿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ ⣿⣿⣿⣿⣿⣿⢹⣿⣿⣿⡿⠊⠤⣈⢻⣿⣹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ ⣿⣿⣿⠿⠛⠛⣎⠛⠿⢿⣿⣿⣭⣽⠟⢃⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ ⠟⠉⠄⠄⡀⡀⢻⣧⣀⠄⠄⠉⠉⠉⡀⠙⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ ⠄⠂⠸⠫⢀⣄⠄⡻⣿⣷⣄⠄⢀⣼⡇⠄⠄⠈⠻⠿⠿⣿⣿⣿⣿⣿⣿⣿⣿⣿ ⠄⠄⠄⠉⢀⣀⢀⢐⣄⠙⠻⠿⠿⠿⠂⠄⠄⠄⠄⠄⠄⠄⠙⢿⣿⣿⣿⣿⣿⣿ ⠄⠄⠄⠄⠈⠄⠁⠂⠹⣷⡝⢷⣶⣶⣄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠙⣿⣿⣿⣿⣿ ⠄⠄⠄⠄⠄⠄⠄⠄⠄⠜⣿⣦⠻⣿⣿⣷⠄⣴⣠⠄⠄⠄⠄⠄⠄⠈⢿⣿⣿⣿ ⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⢘⣿⣷⡙⣿⣿⡆⣿⣿⡇⠄⠄⡀⠒⠄⠄⠘⢿⣿⣿","@rm-received-ts=1704558154679;tmi-sent-ts=1704558154585;ban-duration=180;room-id=62300805;target-user-id=423574282;historical=1 :tmi.twitch.tv CLEARCHAT #nymn e7om","@rm-received-ts=1704558162700;user-type=;badges=no_audio/1;tmi-sent-ts=1704558156548;room-id=62300805;badge-info=;returning-chatter=0;color=#0000FF;client-nonce=9f4888a1653f847ef531fa2287e42de8;historical=1;subscriber=0;turbo=0;id=9df92182-192c-4839-a3af-b360b9e4a09f;emotes=302972056:0-11;emote-only=1;mod=0;display-name=OfficialScrap;first-msg=0;flags=;user-id=85115603 :officialscrap!officialscrap@officialscrap.tmi.twitch.tv PRIVMSG #nymn ItsHappening","@display-name=Rattge;first-msg=0;user-type=;room-id=62300805;turbo=0;returning-chatter=0;flags=;tmi-sent-ts=1704558156630;historical=1;client-nonce=cf9c9c299e3e86c16b1c803db78a1c6d;emotes=;id=620bb23c-dff0-45b5-9078-5a9cd27845a4;rm-received-ts=1704558162701;color=#FF0000;badge-info=;badges=twitch-recap-2023/1;mod=0;subscriber=0;user-id=91515163 :rattge!rattge@rattge.tmi.twitch.tv PRIVMSG #nymn Ratge","@rm-received-ts=1704558162715;id=ee317454-5c4c-4e88-9096-f9d87e2201b0;badges=subscriber/36;badge-info=subscriber/41;turbo=0;first-msg=0;tmi-sent-ts=1704558157080;historical=1;user-id=154079285;client-nonce=7e2af4294366cda34a0c15394f2749bd;flags=;color=#00FF7F;subscriber=1;user-type=;room-id=62300805;returning-chatter=0;mod=0;display-name=boogkitty;emotes= :boogkitty!boogkitty@boogkitty.tmi.twitch.tv PRIVMSG #nymn :Ratge RaveTime","@user-id=232078107;emotes=;turbo=0;badges=no_audio/1;client-nonce=11e530e579624107fb09f83af24e080e;room-id=62300805;returning-chatter=0;first-msg=0;mod=0;color=#008000;display-name=BastunGuy1;subscriber=0;historical=1;flags=;user-type=;badge-info=;rm-received-ts=1704558162750;tmi-sent-ts=1704558158049;id=94c1e5e8-0c55-48ff-bfdc-782e8561c923 :bastunguy1!bastunguy1@bastunguy1.tmi.twitch.tv PRIVMSG #nymn forsen","@badges=subscriber/0,no_video/1;badge-info=subscriber/2;user-type=;turbo=0;emotes=;client-nonce=441ce45dc3e104c77442f9a2815ecc9b;room-id=62300805;display-name=jqxlol;id=2152c3b9-0bad-476c-9a15-8b9c3ff1b861;first-msg=0;flags=;color=#00FF7F;user-id=80542722;historical=1;returning-chatter=0;tmi-sent-ts=1704558159488;subscriber=1;mod=0;rm-received-ts=1704558162796 :jqxlol!jqxlol@jqxlol.tmi.twitch.tv PRIVMSG #nymn :Okayeg 💢 MUZIKA ? Okayeg 💢 MUZIKA ? Okayeg 💢 MUZIKA ? Okayeg 💢 MUZIKA ? Okayeg 💢 MUZIKA ? Okayeg 💢 MUZIKA ? 󠀀","@subscriber=1;returning-chatter=0;user-id=32852911;tmi-sent-ts=1704558160717;color=#FAD130;client-nonce=d797aa21c00a05d4d6f58cfd5a32b2af;first-msg=0;id=482c0dcb-3999-4e4d-a548-5dee3f756b85;badges=subscriber/12,sub-gifter/5;emotes=emotesv2_94c411cb0e2b467e8e844a01ab92489b:0-5;rm-received-ts=1704558162829;mod=0;display-name=WhideX;historical=1;turbo=0;emote-only=1;user-type=;badge-info=subscriber/15;flags=;room-id=62300805 :whidex!whidex@whidex.tmi.twitch.tv PRIVMSG #nymn cniSip","@first-msg=0;badge-info=;client-nonce=95be8cd2cd8f7b736f3a35952f562251;tmi-sent-ts=1704558161355;mod=0;id=7cf4ee00-7596-4bc3-923f-617bd9c22418;turbo=0;badges=rplace-2023/1;emotes=;color=#1E90FF;subscriber=0;historical=1;user-id=150759149;rm-received-ts=1704558162848;room-id=62300805;display-name=MxW02;returning-chatter=0;flags=;user-type= :mxw02!mxw02@mxw02.tmi.twitch.tv PRIVMSG #nymn :sounds like hotline miami","@subscriber=0;display-name=Patixxl;flags=;emotes=;first-msg=0;user-id=51967700;color=#FF0000;id=30e1f0d3-9fd8-41aa-9a1a-2499ee2931a9;historical=1;badges=;tmi-sent-ts=1704558162772;user-type=;turbo=0;badge-info=;returning-chatter=0;rm-received-ts=1704558162943;mod=0;client-nonce=69b3529630ea86206338fc0dd227f5c7;room-id=62300805 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn forsenUnpleased","@first-msg=0;client-nonce=4bd8911054a768b38ca7186371e31a93;user-id=265935517;emotes=;subscriber=0;id=4fcc0125-b108-4b3f-b2f3-335dced675e9;flags=;badges=;user-type=;tmi-sent-ts=1704558163358;badge-info=;display-name=TankingJo;room-id=62300805;turbo=0;color=#FF4500;historical=1;rm-received-ts=1704558163541;mod=0;returning-chatter=0 :tankingjo!tankingjo@tankingjo.tmi.twitch.tv PRIVMSG #nymn :is this what mitch jones sees every day?","@historical=1;badges=glhf-pledge/1;badge-info=;returning-chatter=0;first-msg=0;turbo=0;rm-deleted=1;id=46d7d7ae-de74-4ea5-84fd-a4f7df3e2aa7;user-type=;rm-received-ts=1704558164389;display-name=Abitbol;user-id=31034458;mod=0;room-id=62300805;emotes=;color=#9ACD32;subscriber=0;tmi-sent-ts=1704558164189;flags= :abitbol!abitbol@abitbol.tmi.twitch.tv PRIVMSG #nymn :I⣿⣿⡇⢀⣤⣤⣤⣤⣤⠹⠟⣩⣭⣤⣬⣍⡻⠁⣤⡄⢤⣤⣬⣉⠻⣿⣿⣿I I⣿⣿⡇⣈⢿⠄⠄⠤⠤⢠⣿⡿⣩⣤⣍⢻⡷⡀⣿⡇⠠⠬⢹⣿⠄⣿⣿⣿I I⣿⣿⡇⣷⣯⠺⠿⠿⠿⢸⣿⡇⣿⣿⣿⢈⣬⡃⣶⡇⣶⣶⣾⡟⢰⣿⣿⣿I I⣿⣿⡇⣿⣿⢸⣿⣿⣿⡌⢷⣶⣌⠛⢩⣼⡿⠁⠽⡇⢰⣦⠐⣉⠈⣿⣿⣿I I⣿⣿⣇⣃⣘⣸⣿⣿⣿⣿⣦⣍⣋⣐⣛⣋⣴⣀⣀⣀⣻⣿⣄⣋⣁⣿⣿⣿I I⣿⣿⡟⢩⣴⣶⢰⣶⣬⠁⣤⣦⣶⣶⣶⣶⢀⣶⣦⠹⣿⢰⣶⡆⣿⣿⣿⣿I I⣿⣿⡇⢺⣿⣐⣒⠘⠛⠂⠈⢻⢐⣒⡒⡒⢘⠊⠻⢇⠹⢨⣿⡇⣿⣿⣿⣿I I⣿⣿⡷⠆⠙⠫⠻⠿⡑⠄⣉⡣⠙⠛⠋⠂⢸⣽⡇⠹⣷⠘⣿⡇⣿⣿⣿⣿I I⣿⣿⡄⢻⡧⢌⠉⣀⠃⠂⠽⠇⠈⠉⡉⣉⢸⣿⡇⣦⠹⣷⡜⠂⣿⣿⣿⣿I I⣿⣿⣿⣦⣈⣀⣀⣀⣠⣆⣀⣀⣀⣈⣀⣀⣀⣀⣀⣿⣧⣀⣈⣀⣿⣿⣿⣿I I⣶⡆⣶⣶⡶⣦⠙⠟⣩⣴⣶⢰⣦⣍⡙⡐⣶⣦⢀⣶⡶⠄⣡⣶⠶⣶⣦⣍I I⣿⡇⣐⡒⣨⡾⠃⢰⣿⢋⣤⣦⡘⡿⠷⢰⠸⢇⡼⣿⢡⠄⣿⣇⣐⠂⠘⢛I I⡿⠁⢛⣛⠻⠶⠄⠸⠍⠸⣿⣿⡇⣰⣻⢰⠇⣾⡌⢀⣾⠷⠌⠙⡓⠛⠿⠤I I⢤⡅⡈⣉⣀⡈⠁⡀⢻⢧⢈⢋⣴⡴⠏⡌⣸⣾⠇⣼⣿⠄⢶⣦⠉⢉⡈⠄I I⣈⣁⣈⣀⣀⣤⣶⣿⣦⣤⣀⣈⣤⣴⣾⣄⣀⣠⣼⣿⣿⣿⣦⣭⣄⣈⣠⣴I","@historical=1;room-id=62300805;tmi-sent-ts=1704558164931;rm-received-ts=1704558165026;target-user-id=31034458;ban-duration=180 :tmi.twitch.tv CLEARCHAT #nymn abitbol","@historical=1;turbo=0;id=dd19eacb-9ede-4b28-840c-74a217845de9;user-id=85837900;user-type=;emotes=;rm-received-ts=1704558165579;first-msg=0;room-id=62300805;badge-info=subscriber/37;tmi-sent-ts=1704558165406;returning-chatter=0;client-nonce=1ca4609ecb55d4fc3e65da684e786d81;flags=;display-name=DontCagePlebs;subscriber=1;color=#DAA520;badges=subscriber/36,no_audio/1;mod=0 :dontcageplebs!dontcageplebs@dontcageplebs.tmi.twitch.tv PRIVMSG #nymn kek","@first-msg=0;room-id=62300805;rm-received-ts=1704558166123;turbo=0;flags=0-3:P.3;client-nonce=b2489fba5f0c52e14dcee40213e719ed;returning-chatter=0;color=#DAA520;badge-info=;badges=rplace-2023/1;tmi-sent-ts=1704558165954;user-type=;subscriber=0;id=4988da4c-9718-4633-97db-8dec40ca59c2;emotes=;user-id=501354315;mod=0;historical=1;display-name=ricardo_with_clothes :ricardo_with_clothes!ricardo_with_clothes@ricardo_with_clothes.tmi.twitch.tv PRIVMSG #nymn :lmao hollow knight reference","@id=096fcede-0b5b-404d-b2c1-573b603198c0;room-id=62300805;badge-info=subscriber/9;returning-chatter=0;user-type=;turbo=0;historical=1;mod=0;rm-received-ts=1704558166736;color=#008000;flags=;tmi-sent-ts=1704558166564;emotes=emotesv2_4c39207000564711868f3196cc0a8748:13-19;first-msg=0;subscriber=1;badges=subscriber/9,chatter-cs-go-2022/1;display-name=Phant0mBlades;user-id=278896263 :phant0mblades!phant0mblades@phant0mblades.tmi.twitch.tv PRIVMSG #nymn :forsen died? PoroSad","@flags=;returning-chatter=0;badges=subscriber/36,twitch-recap-2023/1;badge-info=subscriber/38;emotes=;rm-received-ts=1704558166904;subscriber=1;mod=0;user-type=;room-id=62300805;display-name=jontEmillian;first-msg=0;tmi-sent-ts=1704558166726;user-id=433352132;id=feeefdab-e645-4bef-b654-0965836cd0a7;turbo=0;color=#63BD68;historical=1 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn forsenParty","@client-nonce=e45b13d87ed27bc4c65462baa7c5ca7f;tmi-sent-ts=1704558166763;rm-received-ts=1704558166966;color=#008000;flags=;mod=0;subscriber=0;returning-chatter=0;display-name=AikawaCaiman;room-id=62300805;historical=1;id=b66a0f31-c3c1-4a18-b708-3c42b8c4a7bc;user-type=;emotes=;turbo=0;badges=no_audio/1;user-id=157747813;badge-info=;first-msg=0 :aikawacaiman!aikawacaiman@aikawacaiman.tmi.twitch.tv PRIVMSG #nymn :o7 FLY HIGH FORSEN 🕊️","@emotes=;historical=1;client-nonce=cbfe2d9b538627c6ef229b346717e737;display-name=Kotzblitz20;id=a8e59836-a137-4e5d-9a06-36a087cfc339;user-type=;room-id=62300805;badges=subscriber/9,turbo/1;user-id=40037186;color=#FFFF00;first-msg=0;turbo=1;mod=0;tmi-sent-ts=1704558167209;badge-info=subscriber/9;flags=;rm-received-ts=1704558167388;subscriber=1;returning-chatter=0 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn monkaOMEGA","@display-name=ME_ME;room-id=62300805;emote-only=1;historical=1;emotes=31097:0-9;rm-received-ts=1704558168193;first-msg=0;tmi-sent-ts=1704558168004;user-type=;mod=0;id=47bad94f-7ae0-496b-b11f-52691218f3c6;color=#FF2424;flags=;client-nonce=9709d35ec3849012a63290dc622cc8fa;badge-info=subscriber/49;subscriber=1;badges=subscriber/48,bits/25000;user-id=159210800;returning-chatter=0;turbo=0 :me_me!me_me@me_me.tmi.twitch.tv PRIVMSG #nymn forsenBoys","@color=#FF0000;badge-info=;user-type=;subscriber=0;mod=0;flags=;room-id=62300805;rm-received-ts=1704558168824;first-msg=0;historical=1;user-id=51967700;badges=;client-nonce=ac98ffeee20603b03966b3d24a2256b5;returning-chatter=0;id=5643a39b-ec81-4c58-8b80-1dde472f2185;tmi-sent-ts=1704558168655;turbo=0;emotes=;display-name=Patixxl :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenUnpleased 󠀀","@id=0721a398-812e-43cc-8308-729b4aeaaaa5;user-type=;flags=;turbo=0;tmi-sent-ts=1704558170495;subscriber=0;badges=;returning-chatter=0;badge-info=;emotes=;rm-received-ts=1704558170669;user-id=91920489;client-nonce=8b74febcb2643e8b861d5ff5730e37b9;historical=1;room-id=62300805;color=#FF4500;first-msg=0;mod=0;display-name=Franck_Saget :franck_saget!franck_saget@franck_saget.tmi.twitch.tv PRIVMSG #nymn forsenUnpleased","@user-type=;returning-chatter=0;badge-info=subscriber/38;display-name=jontEmillian;turbo=0;historical=1;user-id=433352132;flags=;room-id=62300805;subscriber=1;mod=0;first-msg=0;tmi-sent-ts=1704558170558;id=b7da7b46-346b-44a0-8eda-f7dc873a2bee;badges=subscriber/36,twitch-recap-2023/1;emotes=;color=#63BD68;rm-received-ts=1704558170719 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn :forsenParty 󠀀","@emotes=;badges=bits-charity/1;color=#0000FF;user-id=103665668;user-type=;client-nonce=88d94741ba24eec5c1aea4079c8022bd;subscriber=0;returning-chatter=0;id=bf46e7ae-1d63-4389-81d2-92d09a8ae003;display-name=Intel_power;turbo=0;tmi-sent-ts=1704558171866;mod=0;first-msg=0;room-id=62300805;flags=;historical=1;rm-received-ts=1704558172033;badge-info= :intel_power!intel_power@intel_power.tmi.twitch.tv PRIVMSG #nymn Alien360","@user-id=63372784;display-name=DM8917;id=fa29c6b2-bd07-47ef-a4f9-19a0834d20dd;color=#25E000;badge-info=;emotes=;room-id=62300805;historical=1;flags=;rm-received-ts=1704558173058;user-type=;badges=bits/100;client-nonce=800dbb697b7d58c832090bb8d29b0f29;first-msg=0;returning-chatter=0;mod=0;tmi-sent-ts=1704558172836;turbo=0;subscriber=0 :dm8917!dm8917@dm8917.tmi.twitch.tv PRIVMSG #nymn monkaE","@badge-info=subscriber/49;color=#FF2424;client-nonce=4d3e3c84a31938c03ed2b39102324c56;display-name=ME_ME;flags=;returning-chatter=0;subscriber=1;mod=0;rm-received-ts=1704558173142;badges=subscriber/48,bits/25000;id=9e63d588-3d69-48c4-9258-001a548aa153;turbo=0;first-msg=0;historical=1;emotes=;room-id=62300805;user-id=159210800;tmi-sent-ts=1704558172929;user-type= :me_me!me_me@me_me.tmi.twitch.tv PRIVMSG #nymn PepeS","@flags=;user-type=;client-nonce=0eb503d0f331d9c2697ff76d8ba7e9f2;user-id=85837900;badge-info=subscriber/37;tmi-sent-ts=1704558172999;turbo=0;display-name=DontCagePlebs;first-msg=0;emotes=;returning-chatter=0;color=#DAA520;mod=0;room-id=62300805;subscriber=1;id=addcaaf1-521c-4f78-806d-4d49017974ef;historical=1;badges=subscriber/36,no_audio/1;rm-received-ts=1704558173191 :dontcageplebs!dontcageplebs@dontcageplebs.tmi.twitch.tv PRIVMSG #nymn monkaS","@badge-info=;first-msg=0;user-type=;turbo=0;room-id=62300805;emotes=;id=b5975931-4724-4a7d-8038-3ea64698792c;flags=0-5:A.6/I.6;returning-chatter=0;tmi-sent-ts=1704558173072;client-nonce=59e49395644e34027bf315f857ee9a2c;display-name=Obiwun;badges=no_audio/1;user-id=46199261;mod=0;historical=1;color=#8A2BE2;subscriber=0;rm-received-ts=1704558173247 :obiwun!obiwun@obiwun.tmi.twitch.tv PRIVMSG #nymn SCHIZO","@subscriber=1;emotes=;rm-received-ts=1704558173558;user-id=278896263;historical=1;badge-info=subscriber/9;returning-chatter=0;first-msg=0;display-name=Phant0mBlades;color=#008000;mod=0;room-id=62300805;user-type=;turbo=0;id=dd3fca53-4412-4d7f-b545-2d51a6c48520;tmi-sent-ts=1704558173394;badges=subscriber/9,chatter-cs-go-2022/1;flags= :phant0mblades!phant0mblades@phant0mblades.tmi.twitch.tv PRIVMSG #nymn forsenUnpleased","@first-msg=0;returning-chatter=0;subscriber=1;flags=;color=#1E90FF;id=02ff21d8-3794-4262-a6a6-e5980742ce70;historical=1;rm-received-ts=1704558173772;client-nonce=09c07f927f3b2f8e5dacf743eb846a1b;room-id=62300805;emotes=;display-name=mnqn18;mod=0;turbo=0;badge-info=subscriber/2;user-type=;tmi-sent-ts=1704558173601;user-id=474204887;badges=subscriber/0,premium/1 :mnqn18!mnqn18@mnqn18.tmi.twitch.tv PRIVMSG #nymn IVEGONEPASTHEPOINTOFINSANITY","@returning-chatter=0;badge-info=;room-id=62300805;color=#5F9EA0;emotes=;mod=0;turbo=0;user-id=133344079;badges=no_audio/1;client-nonce=7f803033a32f858c135d028e3e4638b5;historical=1;rm-received-ts=1704558173915;tmi-sent-ts=1704558173719;flags=;id=9a55b087-901f-4daf-b967-7d6a4243715b;display-name=sehtt_;user-type=;first-msg=0;subscriber=0 :sehtt_!sehtt_@sehtt_.tmi.twitch.tv PRIVMSG #nymn ::","@returning-chatter=0;rm-received-ts=1704558174136;flags=;subscriber=1;id=8829a9b5-b02b-4d0d-9827-0a0671856cf1;mod=1;turbo=0;first-msg=0;user-id=41157245;badge-info=subscriber/67;tmi-sent-ts=1704558173961;emotes=;display-name=Mr0lle;room-id=62300805;badges=moderator/1,subscriber/60,rplace-2023/1;user-type=mod;color=#9146FF;historical=1 :mr0lle!mr0lle@mr0lle.tmi.twitch.tv PRIVMSG #nymn forsenParty","@mod=0;tmi-sent-ts=1704558174127;user-id=22733078;rm-received-ts=1704558174314;room-id=62300805;badge-info=subscriber/2;id=7f1c7958-03b6-489d-b0e8-d89dbc6318df;flags=;subscriber=1;display-name=miniwoffer;emotes=;returning-chatter=0;first-msg=0;color=#FF69B4;user-type=;turbo=0;client-nonce=90e8710a650c223c523c5da211bda469;badges=subscriber/0,bits/1;historical=1 :miniwoffer!miniwoffer@miniwoffer.tmi.twitch.tv PRIVMSG #nymn :DOCING What is this game about","@color=#FF0000;first-msg=0;mod=0;emotes=;room-id=62300805;badges=;id=b80d3a07-2ed4-4656-a188-6493834e856c;returning-chatter=0;tmi-sent-ts=1704558174500;subscriber=0;flags=;historical=1;user-type=;display-name=Patixxl;rm-received-ts=1704558174665;badge-info=;user-id=51967700;turbo=0;client-nonce=e8f0fe09de0a2a9595df06c57bd0e013 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn forsenUnpleased","@flags=;returning-chatter=0;display-name=Patixxl;rm-received-ts=1704558178268;mod=0;tmi-sent-ts=1704558178103;user-type=;user-id=51967700;subscriber=0;emotes=;id=f572bb10-d704-4c5f-8ac0-19f26f6d4635;turbo=0;historical=1;first-msg=0;client-nonce=e35e4e7941fc0a7e9d1feab0783c30e3;badges=;room-id=62300805;color=#FF0000;badge-info= :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenUnpleased 󠀀","@turbo=0;badges=subscriber/36,twitch-recap-2023/1;returning-chatter=0;color=#63BD68;id=050a6072-26cc-4418-913b-c6c17e267e9d;user-type=;badge-info=subscriber/38;tmi-sent-ts=1704558178487;room-id=62300805;rm-received-ts=1704558178675;emotes=;user-id=433352132;mod=0;first-msg=0;subscriber=1;display-name=jontEmillian;flags=;historical=1 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn forsenParty","@emotes=;badge-info=subscriber/2;subscriber=1;room-id=62300805;tmi-sent-ts=1704558178621;display-name=jqxlol;client-nonce=af96c8566b07c63d5e1cd7f0b00e1a70;turbo=0;mod=0;badges=subscriber/0,no_video/1;user-id=80542722;user-type=;historical=1;rm-received-ts=1704558178804;first-msg=0;id=859d8091-2eed-408f-904e-0f53ea223f11;flags=;color=#00FF7F;returning-chatter=0 :jqxlol!jqxlol@jqxlol.tmi.twitch.tv PRIVMSG #nymn :Okayeg 💢 MUZIKA ? Okayeg 💢 MUZIKA ? Okayeg 💢 MUZIKA ? Okayeg 💢 MUZIKA ? Okayeg 💢 MUZIKA ? Okayeg 💢 MUZIKA ?","@flags=;badges=vip/1,subscriber/72,rplace-2023/1;returning-chatter=0;id=848a18d9-1b18-4236-a781-1c36089749c8;first-msg=0;color=#D52AFF;room-id=62300805;rm-received-ts=1704558178994;badge-info=subscriber/77;display-name=Joshlad;user-type=;historical=1;turbo=0;mod=0;vip=1;user-id=87120320;emotes=;subscriber=1;tmi-sent-ts=1704558178820 :joshlad!joshlad@joshlad.tmi.twitch.tv PRIVMSG #nymn monkaS","@turbo=0;tmi-sent-ts=1704558179829;display-name=Intel_power;returning-chatter=0;historical=1;mod=0;user-type=;client-nonce=c00a231f8934624eafd79a64c3e97668;rm-received-ts=1704558180008;badge-info=;room-id=62300805;first-msg=0;id=33a78c35-dce0-484c-8be5-9222a2781611;subscriber=0;user-id=103665668;emotes=;flags=;color=#0000FF;badges=bits-charity/1 :intel_power!intel_power@intel_power.tmi.twitch.tv PRIVMSG #nymn IVEGONEPASTHEPOINTOFINSANITY","@returning-chatter=0;tmi-sent-ts=1704558180325;first-msg=0;mod=0;user-id=137782780;flags=;badge-info=subscriber/4;client-nonce=0a1b266a425b6cd205248923aa6057e2;historical=1;id=840635bb-3ca0-4636-a1f1-4a6debb37454;rm-received-ts=1704558180499;room-id=62300805;display-name=pleasekeepconnor6silly;user-type=;badges=subscriber/3,no_audio/1;color=#8A2BE2;subscriber=1;turbo=0;emotes= :pleasekeepconnor6silly!pleasekeepconnor6silly@pleasekeepconnor6silly.tmi.twitch.tv PRIVMSG #nymn IVEGONEPASTHEPOINTOFINSANITY","@color=#B22222;user-type=;badge-info=subscriber/7;emotes=emotesv2_e16d73fce3b840949d5474dfaca63ffd:12-22;turbo=0;subscriber=1;historical=1;room-id=62300805;mod=0;first-msg=0;rm-received-ts=1704558181805;display-name=crazyjuni0r_;id=4f667a25-6ce9-47e7-8486-511ab1e1aee5;returning-chatter=0;badges=subscriber/6,chatter-cs-go-2022/1;tmi-sent-ts=1704558181619;flags=;user-id=222340799;client-nonce=d536c94bbe04001059053d52e419d299 :crazyjuni0r_!crazyjuni0r_@crazyjuni0r_.tmi.twitch.tv PRIVMSG #nymn :!#showemote spacea32HOM","@turbo=0;badge-info=;first-msg=0;historical=1;tmi-sent-ts=1704558182428;display-name=forsenkkona_;rm-received-ts=1704558182599;user-type=;emotes=;id=a02c8cb3-6204-4f6f-90dc-aed0b9a4349c;user-id=151423066;color=#FF69B4;room-id=62300805;mod=0;badges=;returning-chatter=0;subscriber=0;flags= :forsenkkona_!forsenkkona_@forsenkkona_.tmi.twitch.tv PRIVMSG #nymn forsenUnpleased","@first-msg=0;id=e7096162-fcb9-40c1-9f4e-4ffbcecef179;flags=0-2:P.3;historical=1;mod=0;turbo=0;tmi-sent-ts=1704558182845;room-id=62300805;badge-info=subscriber/15;user-type=;emotes=;returning-chatter=0;subscriber=1;badges=subscriber/12,twitch-recap-2023/1;color=#E2C92B;client-nonce=46af8ee370f349edbde8b61dd36cae62;rm-received-ts=1704558183038;user-id=90319443;display-name=huSiOx :husiox!husiox@husiox.tmi.twitch.tv PRIVMSG #nymn :WTF IS THIS GAME ABOUT docYell","@rm-received-ts=1704558183040;first-msg=0;badge-info=subscriber/17;user-id=69072013;emotes=;room-id=62300805;client-nonce=ef4253731729ef8bf3d7d03540d62e0a;mod=0;badges=subscriber/12,twitch-recap-2023/1;display-name=SnuggleUncle;flags=;turbo=0;subscriber=1;color=#0000FF;id=1194ab99-cf4e-4aa8-a2ec-f9c7978d704c;returning-chatter=0;historical=1;user-type=;tmi-sent-ts=1704558182859 :snuggleuncle!snuggleuncle@snuggleuncle.tmi.twitch.tv PRIVMSG #nymn :don't look behind you monkaOMEGA","@emotes=;first-msg=0;badges=bits/1000;flags=;display-name=HajleSellasje;user-id=45923155;badge-info=;returning-chatter=0;room-id=62300805;id=9c8f325e-739c-477e-a00e-97130ad55bd9;turbo=0;user-type=;tmi-sent-ts=1704558183769;color=#FF0000;subscriber=0;rm-received-ts=1704558183941;historical=1;mod=0 :hajlesellasje!hajlesellasje@hajlesellasje.tmi.twitch.tv PRIVMSG #nymn forsenUnpleased","@user-type=;flags=;historical=1;emotes=;room-id=62300805;mod=0;user-id=216144449;returning-chatter=0;rm-received-ts=1704558184490;tmi-sent-ts=1704558184313;color=#00FF7F;badges=turbo/1;subscriber=0;badge-info=;turbo=1;client-nonce=dc732730e69b18ba1fa63aa486cf17e0;id=ca468ddc-af12-498d-b3f8-438c2245551e;first-msg=0;display-name=FollowProtoBuddy :followprotobuddy!followprotobuddy@followprotobuddy.tmi.twitch.tv PRIVMSG #nymn monkaGIGA","@returning-chatter=0;display-name=sehtt_;badges=no_audio/1;client-nonce=537c9c8c12461d79d5fa60d8231ae6b1;turbo=0;id=b2ec4298-a0aa-40d7-88f0-2dcbd278abdd;emotes=;user-type=;historical=1;badge-info=;subscriber=0;mod=0;rm-received-ts=1704558184542;tmi-sent-ts=1704558184317;first-msg=0;room-id=62300805;user-id=133344079;color=#5F9EA0;flags= :sehtt_!sehtt_@sehtt_.tmi.twitch.tv PRIVMSG #nymn IVEGONEPASTHEPOINTOFINSANITY","@first-msg=0;subscriber=0;badges=rplace-2023/1;badge-info=;room-id=62300805;user-id=246452436;rm-received-ts=1704558184595;id=eb73f38e-2118-4e36-90dc-6002b20c2855;returning-chatter=0;display-name=kb_h;client-nonce=63249421ede84223b1ba7cccd9605b8b;historical=1;user-type=;tmi-sent-ts=1704558184423;mod=0;color=#1E90FF;emotes=;flags=;turbo=0 :kb_h!kb_h@kb_h.tmi.twitch.tv PRIVMSG #nymn :what is this, liminal space for rats?","@subscriber=0;user-id=38635616;display-name=KelemvorUber;emotes=;tmi-sent-ts=1704558185042;returning-chatter=0;badges=twitch-recap-2023/1;flags=;historical=1;user-type=;mod=0;badge-info=;turbo=0;color=#F7FF00;rm-received-ts=1704558185220;client-nonce=2c80e220e9db19558882bfa42075a04a;room-id=62300805;first-msg=0;id=f12dc210-68d0-4f1d-8106-fe0bb20970f5 :kelemvoruber!kelemvoruber@kelemvoruber.tmi.twitch.tv PRIVMSG #nymn :anyone feeling extra fine","@id=dfdc46c6-3125-4603-b48a-a64a800e4072;room-id=62300805;flags=;subscriber=1;historical=1;rm-received-ts=1704558185421;user-type=;badges=subscriber/36,no_audio/1;user-id=85837900;turbo=0;client-nonce=fd40b10744c17a6487b112fce5b65dcd;badge-info=subscriber/37;display-name=DontCagePlebs;first-msg=0;returning-chatter=0;emotes=;color=#DAA520;tmi-sent-ts=1704558185237;mod=0 :dontcageplebs!dontcageplebs@dontcageplebs.tmi.twitch.tv PRIVMSG #nymn IVEGONEPASTHEPOINTOFINSANITY","@color=#008000;user-type=;first-msg=0;badges=subscriber/9,chatter-cs-go-2022/1;historical=1;room-id=62300805;tmi-sent-ts=1704558185971;mod=0;rm-received-ts=1704558186127;emotes=;flags=;returning-chatter=0;user-id=278896263;badge-info=subscriber/9;display-name=Phant0mBlades;id=97bddc9b-c183-4619-92d9-b8ea731a0314;subscriber=1;turbo=0 :phant0mblades!phant0mblades@phant0mblades.tmi.twitch.tv PRIVMSG #nymn :forsenUnpleased 󠀀","@subscriber=0;badges=no_audio/1;user-type=;badge-info=;user-id=431946171;display-name=jonhycrack;emotes=;tmi-sent-ts=1704558186695;historical=1;turbo=0;color=#008000;flags=;id=1bcbc165-734a-49f7-9260-8a2dd3d7ec04;mod=0;rm-received-ts=1704558186873;room-id=62300805;returning-chatter=0;first-msg=0 :jonhycrack!jonhycrack@jonhycrack.tmi.twitch.tv PRIVMSG #nymn nym","@room-id=62300805;id=118c7135-35f0-4a4c-9741-d9ebb8810044;historical=1;rm-received-ts=1704558191191;badges=subscriber/36,twitch-recap-2023/1;emotes=;first-msg=0;user-id=433352132;subscriber=1;flags=;tmi-sent-ts=1704558191033;user-type=;mod=0;badge-info=subscriber/38;turbo=0;returning-chatter=0;color=#63BD68;display-name=jontEmillian :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn :forsenParty 󠀀","@id=6e0777f7-4242-493d-981c-abd1de6ded9a;display-name=Kotzblitz20;user-id=40037186;returning-chatter=0;tmi-sent-ts=1704558191015;subscriber=1;room-id=62300805;user-type=;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138;badge-info=subscriber/9;turbo=1;badges=subscriber/9,turbo/1;rm-received-ts=1704558191212;historical=1;color=#FFFF00;first-msg=0;flags=;mod=0;client-nonce=9756ed05932eb83f60af7d2746fa1b03 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@client-nonce=65473cb037b882cbf5737e4d0f83da06;first-msg=0;returning-chatter=0;turbo=0;id=03ba3439-6fb3-4ad1-a02a-c556eb47944e;user-type=;rm-received-ts=1704558191295;tmi-sent-ts=1704558191115;emotes=;badge-info=;historical=1;user-id=63372784;room-id=62300805;color=#25E000;badges=bits/100;mod=0;display-name=DM8917;flags=;subscriber=0 :dm8917!dm8917@dm8917.tmi.twitch.tv PRIVMSG #nymn IVEGONEPASTHEPOINTOFINSANITY","@first-msg=0;turbo=0;subscriber=1;badges=subscriber/24,sub-gifter/5;id=85b5451f-81d8-4fe7-982d-175b3e871a79;client-nonce=6d80f47330297015c90d434aac43e117;user-id=544395745;badge-info=subscriber/29;rm-received-ts=1704558191465;returning-chatter=0;color=#9ACD32;flags=;mod=0;room-id=62300805;display-name=PepePge;user-type=;historical=1;tmi-sent-ts=1704558191289;emotes= :pepepge!pepepge@pepepge.tmi.twitch.tv PRIVMSG #nymn :!#showemote vegan 󠀀","@returning-chatter=0;client-nonce=e68ef3f7a0d67f4dd5532268bb6c2f0d;id=60cae5a1-34ef-4aa7-b652-b2dd41ca46a1;turbo=0;user-type=;rm-received-ts=1704558192415;color=#00FF7F;display-name=jqxlol;badge-info=subscriber/2;subscriber=1;room-id=62300805;first-msg=0;historical=1;mod=0;emotes=;tmi-sent-ts=1704558192239;badges=subscriber/0,no_video/1;user-id=80542722;flags= :jqxlol!jqxlol@jqxlol.tmi.twitch.tv PRIVMSG #nymn :Okayeg 💢 MUZIKA ? Okayeg 💢 MUZIKA ? Okayeg 💢 MUZIKA ? Okayeg 💢 MUZIKA ? Okayeg 💢 MUZIKA ? Okayeg 💢 MUZIKA ? 󠀀","@emotes=;display-name=jontEmillian;rm-received-ts=1704558192720;first-msg=0;id=27451f45-e6bb-44eb-be76-bdc45a8297a8;flags=;historical=1;mod=0;tmi-sent-ts=1704558192549;turbo=0;user-type=;subscriber=1;color=#63BD68;badge-info=subscriber/38;badges=subscriber/36,twitch-recap-2023/1;returning-chatter=0;user-id=433352132;room-id=62300805 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn forsenParty","@display-name=Patixxl;badges=;mod=0;color=#FF0000;first-msg=0;tmi-sent-ts=1704558193047;user-id=51967700;turbo=0;client-nonce=307d7bac6173ef7a5d8a2b54d14a571c;returning-chatter=0;emotes=;historical=1;rm-received-ts=1704558193230;user-type=;id=70142434-1817-4745-b8a1-90144782d1b3;flags=;room-id=62300805;badge-info=;subscriber=0 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@room-id=62300805;rm-received-ts=1704558193486;id=d06f0674-5ee3-4001-860d-bb899ec2ae31;tmi-sent-ts=1704558193291;vip=1;display-name=Joshlad;historical=1;flags=;badges=vip/1,subscriber/72,rplace-2023/1;user-type=;user-id=87120320;subscriber=1;turbo=0;emotes=;badge-info=subscriber/77;color=#D52AFF;first-msg=0;returning-chatter=0;mod=0 :joshlad!joshlad@joshlad.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@returning-chatter=0;user-type=;mod=0;client-nonce=864b2e02368c629b447719779a0655ed;flags=;historical=1;color=#FFFF00;user-id=40037186;display-name=Kotzblitz20;subscriber=1;rm-received-ts=1704558194226;badges=subscriber/9,turbo/1;turbo=1;id=65e474d7-e552-43bd-a8f1-a026ad84c2f1;room-id=62300805;tmi-sent-ts=1704558194014;first-msg=0;badge-info=subscriber/9;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170,176-186,192-202,208-218 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@user-type=;returning-chatter=0;badges=;subscriber=0;client-nonce=dbb17564b4ec368b83b92e9c7b80bfaa;badge-info=;mod=0;tmi-sent-ts=1704558194203;first-msg=0;color=#FF0000;room-id=62300805;id=b1ab5d5b-a62b-45af-a493-3e168d0fa240;emotes=;turbo=0;user-id=810718356;flags=;display-name=holy4uck;historical=1;rm-received-ts=1704558194391 :holy4uck!holy4uck@holy4uck.tmi.twitch.tv PRIVMSG #nymn forsenParty","@emotes=;rm-received-ts=1704558194545;room-id=62300805;mod=0;color=#DAA520;id=16047008-c158-49b6-a392-203f5fe0013b;user-id=85837900;client-nonce=1da6703a9d1af3ad52eb9c4c3edb391f;badges=subscriber/36,no_audio/1;first-msg=0;returning-chatter=0;display-name=DontCagePlebs;badge-info=subscriber/37;historical=1;tmi-sent-ts=1704558194367;flags=;subscriber=1;user-type=;turbo=0 :dontcageplebs!dontcageplebs@dontcageplebs.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@badge-info=subscriber/49;badges=subscriber/48,bits/25000;flags=;user-type=;room-id=62300805;turbo=0;subscriber=1;returning-chatter=0;display-name=ME_ME;rm-received-ts=1704558194607;user-id=159210800;first-msg=0;historical=1;client-nonce=f69c8172c28414a3f21ac62c589c7597;mod=0;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10;id=3dddf9bf-2511-4375-8278-895fccc05146;tmi-sent-ts=1704558194423;emote-only=1;color=#FF2424 :me_me!me_me@me_me.tmi.twitch.tv PRIVMSG #nymn forsenParty","@id=b89eb516-d083-4033-8cc8-454cb1c299e1;flags=;turbo=0;tmi-sent-ts=1704558194613;first-msg=0;returning-chatter=0;mod=0;emotes=;color=#FF0000;badges=;display-name=Patixxl;user-id=51967700;user-type=;subscriber=0;badge-info=;historical=1;client-nonce=9ac0bd656414469f58c799b1934a1bb7;room-id=62300805;rm-received-ts=1704558194778 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@room-id=62300805;user-id=135853293;display-name=theKiryu;user-type=;color=#9ACD32;mod=0;id=2c19bc99-9cc2-42f0-b519-56b023460b16;subscriber=0;flags=;historical=1;first-msg=0;emotes=;badge-info=;returning-chatter=0;tmi-sent-ts=1704558194737;client-nonce=768eac8314920773a77c8851af644705;badges=twitch-recap-2023/1;turbo=0;rm-received-ts=1704558194915 :thekiryu!thekiryu@thekiryu.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@flags=;first-msg=0;historical=1;color=#FF69B4;id=14dfe43b-a22f-4e45-807f-a067cb210f25;user-type=;badges=subscriber/0,bits/1;user-id=22733078;mod=0;badge-info=subscriber/2;rm-received-ts=1704558195317;subscriber=1;returning-chatter=0;tmi-sent-ts=1704558195115;display-name=miniwoffer;emotes=;room-id=62300805;turbo=0;client-nonce=c3726a9e001e7dfe357bc6b8832f4815 :miniwoffer!miniwoffer@miniwoffer.tmi.twitch.tv PRIVMSG #nymn monkaS","@user-type=;mod=0;id=24cdf813-af53-49d3-ad27-2a07ffc19545;room-id=62300805;returning-chatter=0;first-msg=0;tmi-sent-ts=1704558195441;badges=subscriber/9,chatter-cs-go-2022/1;emote-only=1;flags=;badge-info=subscriber/9;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10;turbo=0;color=#008000;subscriber=1;rm-received-ts=1704558195627;display-name=Phant0mBlades;user-id=278896263;historical=1 :phant0mblades!phant0mblades@phant0mblades.tmi.twitch.tv PRIVMSG #nymn forsenParty","@historical=1;tmi-sent-ts=1704558195576;mod=0;turbo=0;first-msg=0;user-type=;room-id=62300805;subscriber=1;returning-chatter=0;rm-received-ts=1704558195777;display-name=WhideX;client-nonce=2ce1a6a55fae4feaa11ef65e368fefdf;flags=;color=#FAD130;badges=subscriber/12,sub-gifter/5;emotes=;user-id=32852911;id=b5e04c52-5069-482d-9121-82412c9ff57d;badge-info=subscriber/15 :whidex!whidex@whidex.tmi.twitch.tv PRIVMSG #nymn :forsenboys? more like forsen re tiredboys","@first-msg=0;room-id=62300805;color=#41FF00;badges=subscriber/36;turbo=0;tmi-sent-ts=1704558195686;user-id=92402102;emotes=;historical=1;badge-info=subscriber/41;rm-received-ts=1704558195860;mod=0;id=ddf08051-10af-4e75-8002-e3183c232165;flags=;subscriber=1;display-name=liber7as;returning-chatter=0;client-nonce=cb6f3d498c23949492defe2be8dbbd4d;user-type= :liber7as!liber7as@liber7as.tmi.twitch.tv PRIVMSG #nymn :THIS ??????","@subscriber=0;display-name=SadRosh;returning-chatter=0;historical=1;turbo=0;user-type=;color=#B22222;mod=0;first-msg=0;user-id=184644555;emotes=;badges=no_audio/1;badge-info=;client-nonce=67898821b9e8192ce8af839995ac0b59;room-id=62300805;tmi-sent-ts=1704558195772;rm-received-ts=1704558195968;id=36e3dfc8-61a4-469c-94e0-efbad57f377a;flags= :sadrosh!sadrosh@sadrosh.tmi.twitch.tv PRIVMSG #nymn forsenParty","@emotes=;turbo=0;historical=1;user-type=;display-name=Patixxl;user-id=51967700;rm-received-ts=1704558196337;badges=;badge-info=;client-nonce=1e4383b9f54ff0d37dee5cd7531472e8;tmi-sent-ts=1704558196141;color=#FF0000;room-id=62300805;id=4659b954-fde3-41fc-836a-4c3ce7c7e71a;returning-chatter=0;subscriber=0;first-msg=0;flags=;mod=0 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@user-id=87120320;display-name=Joshlad;historical=1;id=776f0ade-3419-4e60-89b5-fe37674a7bb9;mod=0;first-msg=0;user-type=;color=#D52AFF;subscriber=1;room-id=62300805;vip=1;rm-received-ts=1704558196519;badges=vip/1,subscriber/72,rplace-2023/1;flags=;badge-info=subscriber/77;emotes=;tmi-sent-ts=1704558196338;turbo=0;returning-chatter=0 :joshlad!joshlad@joshlad.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@badges=subscriber/36,twitch-recap-2023/1;mod=0;user-id=433352132;room-id=62300805;returning-chatter=0;user-type=;emotes=;display-name=jontEmillian;tmi-sent-ts=1704558196372;historical=1;rm-received-ts=1704558196556;first-msg=0;badge-info=subscriber/38;id=3343f7f5-c68b-4dab-88f1-84f5244affe9;color=#63BD68;turbo=0;subscriber=1;flags= :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn :forsenParty 󠀀","@color=#000000;emotes=;flags=;mod=0;first-msg=0;turbo=0;user-type=;badge-info=;badges=;returning-chatter=0;id=99e16736-0820-48a3-9b10-04a9f9bca7cb;tmi-sent-ts=1704558196398;display-name=Duchene;rm-received-ts=1704558196575;subscriber=0;client-nonce=3a0e84b88167286bf31f77c97c826a59;historical=1;user-id=205837377;room-id=62300805 :duchene!duchene@duchene.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@rm-received-ts=1704558197134;display-name=buong1;user-id=75144877;color=#FF0000;room-id=62300805;flags=;emotes=;historical=1;mod=0;badge-info=subscriber/5;first-msg=0;returning-chatter=0;turbo=0;client-nonce=7ace2fd39212e9f61b377f68887a7a06;id=771aaf2c-1627-4ebc-aab8-12703b4f5905;user-type=;tmi-sent-ts=1704558196942;subscriber=1;badges=subscriber/3,no_video/1 :buong1!buong1@buong1.tmi.twitch.tv PRIVMSG #nymn forsenParty","@room-id=62300805;badges=subscriber/0,premium/1;rm-received-ts=1704558197216;user-type=;subscriber=1;user-id=474204887;client-nonce=471fd7c0712a2c8c877e4ada58908a34;tmi-sent-ts=1704558197040;badge-info=subscriber/2;display-name=mnqn18;id=f9643840-be50-4cd0-b7cc-816ad3cb7195;turbo=0;returning-chatter=0;first-msg=0;historical=1;emotes=;mod=0;color=#1E90FF;flags= :mnqn18!mnqn18@mnqn18.tmi.twitch.tv PRIVMSG #nymn :Ratge RaveTime","@returning-chatter=0;emotes=;display-name=tedkaczynski___;room-id=62300805;badges=premium/1;flags=;mod=0;client-nonce=59a1029f8ec4768aa10c721ae74065d2;first-msg=0;id=0f6da476-af72-4312-9309-060489b50a93;color=#DA197A;rm-received-ts=1704558197325;user-type=;historical=1;user-id=199969143;badge-info=;subscriber=0;turbo=0;tmi-sent-ts=1704558197130 :tedkaczynski___!tedkaczynski___@tedkaczynski___.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@rm-received-ts=1704558197611;badges=subscriber/3030,glhf-pledge/1;id=4477366c-cd8a-49b2-82e5-a5924aabf42a;historical=1;first-msg=0;returning-chatter=0;emote-only=1;flags=;display-name=LOZZLY;room-id=62300805;subscriber=1;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10;mod=0;badge-info=subscriber/30;user-id=84185616;tmi-sent-ts=1704558197394;turbo=0;color=#00CCCC;user-type= :lozzly!lozzly@lozzly.tmi.twitch.tv PRIVMSG #nymn forsenParty","@historical=1;display-name=ME_ME;turbo=0;user-type=;room-id=62300805;subscriber=1;color=#FF2424;mod=0;returning-chatter=0;badges=subscriber/48,bits/25000;tmi-sent-ts=1704558197491;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10;rm-received-ts=1704558197657;badge-info=subscriber/49;flags=;id=b68d4108-9187-4450-8168-6294dbc22bde;user-id=159210800;first-msg=0;client-nonce=82bf9d47e479c64a89ce0da9628b7ea3 :me_me!me_me@me_me.tmi.twitch.tv PRIVMSG #nymn :forsenParty 󠀀","@badge-info=subscriber/9;user-type=;badges=subscriber/9,chatter-cs-go-2022/1;historical=1;flags=;user-id=278896263;id=4232ed07-2f56-4541-afcb-346014e83b7a;mod=0;tmi-sent-ts=1704558197694;color=#008000;display-name=Phant0mBlades;first-msg=0;room-id=62300805;subscriber=1;returning-chatter=0;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10;turbo=0;rm-received-ts=1704558197873 :phant0mblades!phant0mblades@phant0mblades.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM","@returning-chatter=0;subscriber=0;user-id=51967700;turbo=0;id=033989bd-8622-4bdd-98d6-94fbf51ef094;flags=;display-name=Patixxl;color=#FF0000;badge-info=;room-id=62300805;first-msg=0;tmi-sent-ts=1704558197840;historical=1;mod=0;emotes=;badges=;client-nonce=9502f37d34a369a9abcc48e12fbe50eb;user-type=;rm-received-ts=1704558198027 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@emotes=;turbo=0;display-name=DontCagePlebs;badges=subscriber/36,no_audio/1;returning-chatter=0;badge-info=subscriber/37;first-msg=0;client-nonce=ee6467bb1a9b9570f89164245452d04c;color=#DAA520;id=27e9162f-4e01-4c78-b3b1-569f998c036a;user-id=85837900;room-id=62300805;user-type=;tmi-sent-ts=1704558197970;flags=;subscriber=1;historical=1;rm-received-ts=1704558198248;mod=0 :dontcageplebs!dontcageplebs@dontcageplebs.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@badges=subscriber/9,chatter-cs-go-2022/1;first-msg=0;user-id=278896263;returning-chatter=0;mod=0;display-name=Phant0mBlades;historical=1;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10;tmi-sent-ts=1704558198855;flags=;subscriber=1;user-type=;badge-info=subscriber/9;turbo=0;id=9ad761e6-d77b-411a-ac8d-de3ce7dc6756;rm-received-ts=1704558199039;room-id=62300805;color=#008000 :phant0mblades!phant0mblades@phant0mblades.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM","@user-id=32852911;flags=;subscriber=1;returning-chatter=0;first-msg=0;tmi-sent-ts=1704558198874;user-type=;historical=1;color=#FAD130;room-id=62300805;emotes=;id=56abb64c-97d3-4e47-819d-9d90244510ec;badges=subscriber/12,sub-gifter/5;display-name=WhideX;client-nonce=cff614e4c68b0ce1fafa42c2a7c9c296;rm-received-ts=1704558199045;turbo=0;mod=0;badge-info=subscriber/15 :whidex!whidex@whidex.tmi.twitch.tv PRIVMSG #nymn :forsenboys? more like forsen re tiredboys 󠀀","@turbo=0;client-nonce=39b2fc175d8bbba13685b5c64a7e84e9;tmi-sent-ts=1704558199554;historical=1;id=6f0098b2-d6e4-476f-9297-8ec51bf10464;badges=;color=#FF0000;first-msg=0;display-name=Patixxl;emotes=;room-id=62300805;flags=;user-id=51967700;mod=0;returning-chatter=0;user-type=;badge-info=;subscriber=0;rm-received-ts=1704558199732 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@user-type=;turbo=0;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10;room-id=62300805;mod=0;badge-info=;flags=12-15:P.3;rm-received-ts=1704558199945;user-id=29764188;first-msg=0;historical=1;badges=;display-name=zzlint;subscriber=0;tmi-sent-ts=1704558199774;id=973f3cf1-cd6f-4d51-92b9-7f08eddd9dd4;returning-chatter=0;client-nonce=67353085a1089e47a046bb4ff1bbf671;color=#1E90FF :zzlint!zzlint@zzlint.tmi.twitch.tv PRIVMSG #nymn :forsenParty shit","@rm-received-ts=1704558200172;turbo=0;first-msg=0;returning-chatter=0;subscriber=0;badges=;color=#FF0000;mod=0;user-id=810718356;id=3a36a314-0444-4591-b21f-ce1cbb0537a3;badge-info=;display-name=holy4uck;historical=1;room-id=62300805;tmi-sent-ts=1704558200005;flags=;client-nonce=0c1c160767de6663b630dd7c7a63ec9b;emotes=;user-type= :holy4uck!holy4uck@holy4uck.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM","@first-msg=0;subscriber=1;flags=;display-name=jontEmillian;badge-info=subscriber/38;mod=0;emotes=;returning-chatter=0;tmi-sent-ts=1704558200345;room-id=62300805;turbo=0;user-id=433352132;badges=subscriber/36,twitch-recap-2023/1;historical=1;color=#63BD68;user-type=;rm-received-ts=1704558200535;id=57b00358-5a65-424e-9a1d-3075421ea987 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn forsenParty","@badge-info=subscriber/29;rm-received-ts=1704558201021;subscriber=1;turbo=0;user-type=;color=#9ACD32;flags=;historical=1;badges=subscriber/24,sub-gifter/5;room-id=62300805;tmi-sent-ts=1704558200836;display-name=PepePge;user-id=544395745;client-nonce=4d66f94dcb573498a4e1a2931cf48c9c;mod=0;first-msg=0;id=42b37fdb-97cf-408d-be10-f9aa2fb80abc;returning-chatter=0;emotes= :pepepge!pepepge@pepepge.tmi.twitch.tv PRIVMSG #nymn :!#showemote vegan","@display-name=Joshlad;color=#D52AFF;subscriber=1;turbo=0;flags=;returning-chatter=0;user-type=;emotes=;badges=vip/1,subscriber/72,rplace-2023/1;badge-info=subscriber/77;id=80383c59-7ebd-41c4-8ad1-142f2276b71e;room-id=62300805;historical=1;user-id=87120320;vip=1;mod=0;first-msg=0;tmi-sent-ts=1704558201151;rm-received-ts=1704558201321 :joshlad!joshlad@joshlad.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@returning-chatter=0;color=#008000;user-id=278896263;turbo=0;rm-received-ts=1704558201558;badges=subscriber/9,chatter-cs-go-2022/1;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10;badge-info=subscriber/9;subscriber=1;first-msg=0;tmi-sent-ts=1704558201374;room-id=62300805;user-type=;display-name=Phant0mBlades;mod=0;historical=1;id=a42808d7-7f4d-4a11-b705-0a452c0354e8;flags= :phant0mblades!phant0mblades@phant0mblades.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM","@flags=;tmi-sent-ts=1704558201591;room-id=62300805;rm-received-ts=1704558201774;emotes=;id=8fdcd2c4-5ae6-48b6-9794-2f08d82b574b;mod=0;first-msg=0;returning-chatter=0;subscriber=0;client-nonce=7b37f78ba5b7ae660ac7458312fa2917;user-type=;badge-info=;user-id=51967700;historical=1;badges=;color=#FF0000;turbo=0;display-name=Patixxl :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@flags=;historical=1;subscriber=1;client-nonce=48ac4d7618c53399e007825e7d8413cd;mod=0;user-type=;id=f60be363-c6e9-435d-8e8c-05f7035e11e7;first-msg=0;returning-chatter=0;user-id=40037186;turbo=1;badges=subscriber/9,turbo/1;color=#FFFF00;display-name=Kotzblitz20;tmi-sent-ts=1704558201971;rm-received-ts=1704558202179;badge-info=subscriber/9;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90;room-id=62300805 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@user-type=;room-id=62300805;mod=0;emote-only=1;id=b02a2980-b6d8-46ef-a305-2392a1700644;turbo=0;historical=1;badge-info=subscriber/49;rm-received-ts=1704558203272;first-msg=0;client-nonce=14879c16526748e58d2830173560abea;flags=;color=#FF2424;user-id=159210800;badges=subscriber/48,bits/25000;display-name=ME_ME;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10;subscriber=1;returning-chatter=0;tmi-sent-ts=1704558203063 :me_me!me_me@me_me.tmi.twitch.tv PRIVMSG #nymn forsenParty","@color=#FF0000;emotes=;user-id=810718356;subscriber=0;first-msg=0;user-type=;id=b2cf343e-8d80-4171-b5ac-484e1a1b2ca9;display-name=holy4uck;historical=1;rm-received-ts=1704558203551;badges=;returning-chatter=0;turbo=0;flags=;client-nonce=d34ab6874f8e6344b06d4cc2b980b83a;mod=0;tmi-sent-ts=1704558203388;badge-info=;room-id=62300805 :holy4uck!holy4uck@holy4uck.tmi.twitch.tv PRIVMSG #nymn TriKool","@emotes=;id=ec5d5821-4a54-41ea-81e3-a8dea0978666;first-msg=0;subscriber=1;user-type=;color=#FF0000;badges=subscriber/3,no_video/1;user-id=75144877;historical=1;display-name=buong1;badge-info=subscriber/5;tmi-sent-ts=1704558203712;rm-received-ts=1704558203896;room-id=62300805;flags=;client-nonce=ce1ae06dc8b511922aae6b5196416d52;turbo=0;mod=0;returning-chatter=0 :buong1!buong1@buong1.tmi.twitch.tv PRIVMSG #nymn :rat battle","@user-type=;user-id=433352132;first-msg=0;color=#63BD68;historical=1;flags=;room-id=62300805;emotes=;tmi-sent-ts=1704558203749;mod=0;turbo=0;rm-received-ts=1704558203914;subscriber=1;badge-info=subscriber/38;returning-chatter=0;badges=subscriber/36,twitch-recap-2023/1;id=5b0e741b-de67-48ba-9385-aceebd5db91d;display-name=jontEmillian :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn :forsenParty 󠀀","@display-name=DontCagePlebs;tmi-sent-ts=1704558203857;user-type=;subscriber=1;room-id=62300805;emotes=;color=#DAA520;client-nonce=694fdd9aecda75d281c5e91360af364c;turbo=0;mod=0;user-id=85837900;historical=1;first-msg=0;badges=subscriber/36,no_audio/1;rm-received-ts=1704558204016;badge-info=subscriber/37;id=00121252-2036-475d-a10e-98aa62ec6477;flags=;returning-chatter=0 :dontcageplebs!dontcageplebs@dontcageplebs.tmi.twitch.tv PRIVMSG #nymn TriKool","@first-msg=0;rm-received-ts=1704558204055;tmi-sent-ts=1704558203880;flags=;user-type=;client-nonce=5bdfec2a4dacd6e7f3ec6957a9757b9a;badges=;subscriber=0;returning-chatter=0;room-id=62300805;mod=0;turbo=0;historical=1;user-id=137332535;badge-info=;display-name=BlueAves;id=5a56af8b-5f39-4aec-afa4-d252210a5f8e;color=#1E90FF;emotes= :blueaves!blueaves@blueaves.tmi.twitch.tv PRIVMSG #nymn :RAT BATTLE","@flags=;color=#0000FF;room-id=62300805;mod=0;first-msg=0;turbo=0;user-type=;badge-info=;tmi-sent-ts=1704558203945;subscriber=0;user-id=103665668;emotes=;badges=bits-charity/1;historical=1;rm-received-ts=1704558204127;client-nonce=4ad780f4eb3dc6c75d6bfbcd26942b62;id=98bf5a98-4537-4fe4-bc9b-27d71fb96bd1;returning-chatter=0;display-name=Intel_power :intel_power!intel_power@intel_power.tmi.twitch.tv PRIVMSG #nymn :RAT BATTLE","@flags=;id=29010001-9bb5-4678-b689-25acec58df6c;client-nonce=2751fd57df2c3e1128b3d2ce1cfb67c8;badge-info=subscriber/9;subscriber=1;user-id=40037186;mod=0;rm-received-ts=1704558204198;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10;returning-chatter=0;historical=1;turbo=1;color=#FFFF00;badges=subscriber/9,turbo/1;tmi-sent-ts=1704558204021;first-msg=0;room-id=62300805;display-name=Kotzblitz20;user-type= :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM","@user-type=;display-name=SecretCarrot;color=#00615C;rm-received-ts=1704558204394;id=1c2a81b4-7207-4a7b-9e86-a770d731e00a;tmi-sent-ts=1704558204214;first-msg=0;room-id=62300805;badges=subscriber/54,bits/1000;user-id=103592036;returning-chatter=0;mod=0;historical=1;badge-info=subscriber/55;flags=;turbo=0;emotes=;client-nonce=f5321af192793b3a795055b3e796a3d6;subscriber=1 :secretcarrot!secretcarrot@secretcarrot.tmi.twitch.tv PRIVMSG #nymn TriKool","@mod=0;historical=1;subscriber=0;tmi-sent-ts=1704558204849;user-type=;display-name=Patixxl;color=#FF0000;badge-info=;returning-chatter=0;rm-received-ts=1704558205028;turbo=0;badges=;emotes=;flags=;room-id=62300805;client-nonce=1c340813bea2243106b8dd2bb1001aed;id=b1ba39c9-3f4b-4810-98f5-127997bc4f2d;first-msg=0;user-id=51967700 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@color=#FF69B4;subscriber=1;tmi-sent-ts=1704558205550;rm-received-ts=1704558205753;user-id=450253959;room-id=62300805;mod=0;historical=1;client-nonce=655006b9d5f9d121563dcb59b8369071;emotes=emotesv2_55b2b64121f44472bc69ed6b7adfedd8:0-6;id=34306fd0-cdca-4de7-99fd-6e7c8f47046f;badge-info=subscriber/29;returning-chatter=0;emote-only=1;user-type=;first-msg=0;display-name=Gayguh;flags=;badges=subscriber/24;turbo=0 :gayguh!gayguh@gayguh.tmi.twitch.tv PRIVMSG #nymn pspRave","@turbo=1;subscriber=1;client-nonce=aa655553b581654e869e10a0abea8b94;color=#FFFF00;badge-info=subscriber/9;user-type=;user-id=40037186;mod=0;historical=1;flags=;rm-received-ts=1704558206165;id=214aa31a-83ff-4eda-b528-0b9bc5875ef8;first-msg=0;room-id=62300805;returning-chatter=0;emotes=;badges=subscriber/9,turbo/1;tmi-sent-ts=1704558205969;display-name=Kotzblitz20 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn TriKool","@badge-info=subscriber/77;subscriber=1;first-msg=0;user-type=;returning-chatter=0;historical=1;mod=0;room-id=62300805;turbo=0;id=ab1af198-16be-48e9-a707-7f4b6ea7eb26;user-id=87120320;badges=vip/1,subscriber/72,rplace-2023/1;flags=;rm-received-ts=1704558206310;display-name=Joshlad;tmi-sent-ts=1704558206149;emotes=;vip=1;color=#D52AFF :joshlad!joshlad@joshlad.tmi.twitch.tv PRIVMSG #nymn :rat battle Painsge","@badges=subscriber/0,premium/1;turbo=0;client-nonce=6837b30857f233c00c167f4e77a1fccb;historical=1;returning-chatter=0;room-id=62300805;first-msg=0;color=#1E90FF;tmi-sent-ts=1704558207167;id=4355ed15-8401-4fcd-93c1-9e5ccb92d0b4;rm-received-ts=1704558207343;display-name=mnqn18;badge-info=subscriber/2;subscriber=1;user-type=;flags=;mod=0;user-id=474204887;emotes= :mnqn18!mnqn18@mnqn18.tmi.twitch.tv PRIVMSG #nymn emiNymN","@rm-received-ts=1704558207703;emotes=;tmi-sent-ts=1704558207544;client-nonce=ae1fe673f6c5b331b258eb305b7d991d;first-msg=0;subscriber=0;historical=1;mod=0;badges=;display-name=Patixxl;badge-info=;turbo=0;user-id=51967700;user-type=;color=#FF0000;room-id=62300805;flags=;returning-chatter=0;id=54230e05-eb91-4498-84a2-53bb05c1d169 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn vegan","@mod=0;badge-info=subscriber/49;client-nonce=b819213c70d584e2c178bc4edcfd00e5;emote-only=1;returning-chatter=0;user-type=;turbo=0;emotes=300799759:0-7;badges=subscriber/48,bits/25000;display-name=ME_ME;flags=;first-msg=0;room-id=62300805;rm-received-ts=1704558207862;color=#FF2424;tmi-sent-ts=1704558207681;user-id=159210800;subscriber=1;historical=1;id=0e9ee8cc-bf2b-46fa-9c5e-5c93a78bb491 :me_me!me_me@me_me.tmi.twitch.tv PRIVMSG #nymn forsenBB","@subscriber=0;flags=;user-id=45923155;user-type=;id=5aeef6a8-bc05-49f7-8c77-660691fff28f;mod=0;color=#FF0000;room-id=62300805;returning-chatter=0;first-msg=0;historical=1;badges=bits/1000;badge-info=;tmi-sent-ts=1704558208660;turbo=0;display-name=HajleSellasje;rm-received-ts=1704558208835;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170,176-186,192-202 :hajlesellasje!hajlesellasje@hajlesellasje.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@turbo=0;returning-chatter=0;mod=0;subscriber=0;client-nonce=8921b2cd566a3aeebf63745845f67200;emotes=;rm-received-ts=1704558208957;user-id=38635616;tmi-sent-ts=1704558208779;display-name=KelemvorUber;room-id=62300805;id=24611b0c-0ffb-4ac2-887c-870cc0c83bea;badges=twitch-recap-2023/1;color=#F7FF00;badge-info=;user-type=;historical=1;first-msg=0;flags= :kelemvoruber!kelemvoruber@kelemvoruber.tmi.twitch.tv PRIVMSG #nymn NymnPretendingToEnjoyHisCrappyRemix","@rm-received-ts=1704558209545;turbo=1;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170,176-186,192-202,208-218,224-234,240-250,256-266,272-282;id=b9130abd-39bb-4c10-8699-bf0366b4f5a2;badges=subscriber/9,turbo/1;client-nonce=e4b06b826af6611004071c97149c24b8;display-name=Kotzblitz20;first-msg=0;returning-chatter=0;flags=;mod=0;tmi-sent-ts=1704558209353;user-type=;user-id=40037186;room-id=62300805;subscriber=1;badge-info=subscriber/9;historical=1;color=#FFFF00 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@first-msg=0;returning-chatter=0;turbo=0;badge-info=subscriber/53;rm-received-ts=1704558210664;user-type=;badges=subscriber/48;mod=0;user-id=29649547;display-name=orange_bean;historical=1;room-id=62300805;flags=;tmi-sent-ts=1704558210489;id=ead36bca-f116-4bb7-a0b1-d66f1dd4dc17;subscriber=1;color=#FF7F50;emotes= :orange_bean!orange_bean@orange_bean.tmi.twitch.tv PRIVMSG #nymn :OOOO bars","@room-id=62300805;flags=;client-nonce=a11f66001ca32adf4b2eb625fda85522;mod=0;subscriber=0;user-id=51967700;rm-received-ts=1704558210701;turbo=0;historical=1;first-msg=0;display-name=Patixxl;badges=;color=#FF0000;tmi-sent-ts=1704558210509;badge-info=;id=ba53ae1e-e83e-44d0-83dd-8b09ee7b1ece;user-type=;returning-chatter=0;emotes= :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@subscriber=1;historical=1;first-msg=0;badge-info=subscriber/38;mod=0;color=#63BD68;flags=;returning-chatter=0;turbo=0;tmi-sent-ts=1704558211269;rm-received-ts=1704558211447;emotes=;user-id=433352132;room-id=62300805;display-name=jontEmillian;user-type=;badges=subscriber/36,twitch-recap-2023/1;id=ef897dcf-182a-41f8-aea2-ce1d7f907712 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn forsenParty","@display-name=HajleSellasje;returning-chatter=0;badges=bits/1000;mod=0;first-msg=0;badge-info=;id=db277f99-707b-423d-9127-f97200f8197d;rm-received-ts=1704558211623;user-type=;user-id=45923155;room-id=62300805;subscriber=0;color=#FF0000;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,17-27,33-43,49-59,65-75,81-91,97-107,113-123,129-139,145-155,161-171,177-187,193-203;tmi-sent-ts=1704558211446;flags=;turbo=0;historical=1 :hajlesellasje!hajlesellasje@hajlesellasje.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@subscriber=1;user-id=40037186;badge-info=subscriber/9;mod=0;first-msg=0;rm-received-ts=1704558212912;id=48b59f15-0096-4bbb-8ed3-a19a1668081e;user-type=;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106;room-id=62300805;returning-chatter=0;display-name=Kotzblitz20;turbo=1;tmi-sent-ts=1704558212728;historical=1;badges=subscriber/9,turbo/1;color=#FFFF00;flags=;client-nonce=840a243e8ad8c7188d703f555b3e17af :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@id=b1c249bc-1eb4-4335-80d0-d9dc590b4ec7;client-nonce=d4eda18886fe9bf410c60585925c6dd1;mod=0;flags=;display-name=Patixxl;returning-chatter=0;subscriber=0;historical=1;room-id=62300805;tmi-sent-ts=1704558213076;color=#FF0000;badge-info=;user-id=51967700;badges=;rm-received-ts=1704558213252;emotes=;turbo=0;user-type=;first-msg=0 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@color=#FF2424;emote-only=1;id=012e7ede-15cb-404f-aff2-45771a83a797;user-id=159210800;historical=1;rm-received-ts=1704558213256;display-name=ME_ME;emotes=300116349:9-16/300799759:0-7;badge-info=subscriber/49;first-msg=0;room-id=62300805;turbo=0;client-nonce=7b82233c57207aaf10dd4d93591aa7a1;returning-chatter=0;mod=0;user-type=;tmi-sent-ts=1704558213077;flags=;subscriber=1;badges=subscriber/48,bits/25000 :me_me!me_me@me_me.tmi.twitch.tv PRIVMSG #nymn :forsenBB SingsMic","@display-name=Phant0mBlades;id=2f667fa1-e582-4458-a082-f31857c128d5;flags=;historical=1;badge-info=subscriber/9;user-id=278896263;subscriber=1;room-id=62300805;returning-chatter=0;mod=0;first-msg=0;tmi-sent-ts=1704558213151;color=#008000;rm-received-ts=1704558213323;badges=subscriber/9,chatter-cs-go-2022/1;user-type=;turbo=0;emotes= :phant0mblades!phant0mblades@phant0mblades.tmi.twitch.tv PRIVMSG #nymn Pepepains","@vip=1;turbo=0;user-type=;user-id=87120320;emotes=;room-id=62300805;badges=vip/1,subscriber/72,rplace-2023/1;display-name=Joshlad;subscriber=1;rm-received-ts=1704558213468;badge-info=subscriber/77;historical=1;flags=;color=#D52AFF;returning-chatter=0;first-msg=0;mod=0;id=2058533b-f89d-438c-8cf0-cb95005ce63a;tmi-sent-ts=1704558213278 :joshlad!joshlad@joshlad.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@returning-chatter=0;turbo=0;mod=0;badges=bits/100;display-name=DM8917;color=#25E000;first-msg=0;user-id=63372784;flags=;client-nonce=a06c8322bf55c186d4d04f0b2de7a0a3;user-type=;rm-received-ts=1704558213645;id=1a6fd806-f61c-431a-8a51-243afaedf518;tmi-sent-ts=1704558213469;emotes=;badge-info=;room-id=62300805;subscriber=0;historical=1 :dm8917!dm8917@dm8917.tmi.twitch.tv PRIVMSG #nymn :#showemote vegan","@mod=0;color=#9ACD32;rm-received-ts=1704558214015;turbo=0;historical=1;tmi-sent-ts=1704558213812;client-nonce=e58bcd51a8e42742d976af08244a39bc;user-type=;badge-info=subscriber/42;user-id=68478828;badges=subscriber/42,bits/1000;display-name=wheeely;returning-chatter=0;room-id=62300805;emotes=;flags=;first-msg=0;id=a3a63eaa-6238-4927-bf7c-a68b5c967ca3;subscriber=1 :wheeely!wheeely@wheeely.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@historical=1;rm-received-ts=1704558214515;room-id=62300805;badge-info=;user-id=45923155;badges=bits/1000;id=dc0c4b83-5f02-4c8e-9881-803944c90fba;returning-chatter=0;mod=0;display-name=HajleSellasje;color=#FF0000;tmi-sent-ts=1704558214324;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170,176-186,192-202;flags=;first-msg=0;turbo=0;user-type=;subscriber=0 :hajlesellasje!hajlesellasje@hajlesellasje.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@flags=;badge-info=;badges=no_audio/1;emotes=;client-nonce=b48636ad5e758949425518c5c5d8b814;turbo=0;subscriber=0;tmi-sent-ts=1704558214722;rm-received-ts=1704558214923;color=#8A2BE2;id=7d7e3fb9-1d8e-4855-996c-46ed82a3e777;returning-chatter=0;mod=0;user-type=;display-name=Obiwun;user-id=46199261;room-id=62300805;first-msg=0;historical=1 :obiwun!obiwun@obiwun.tmi.twitch.tv PRIVMSG #nymn up","@emotes=;client-nonce=9e79c6f980c645566cb5fb1d7507d05b;badge-info=;flags=;user-type=;tmi-sent-ts=1704558215934;historical=1;room-id=62300805;turbo=0;subscriber=0;id=bbc633cf-25aa-4777-a13d-1464fbc22da4;mod=0;first-msg=0;badges=;rm-received-ts=1704558216153;color=#FF0000;returning-chatter=0;display-name=Patixxl;user-id=51967700 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@id=c1ac09b9-32ba-4897-b12c-bdbea202f1c7;emotes=;subscriber=1;mod=0;historical=1;tmi-sent-ts=1704558216228;rm-received-ts=1704558216419;client-nonce=0b6cc37409501651fd73b87ae67604a8;color=#9ACD32;display-name=wheeely;flags=;badge-info=subscriber/42;room-id=62300805;user-id=68478828;badges=subscriber/42,bits/1000;returning-chatter=0;user-type=;turbo=0;first-msg=0 :wheeely!wheeely@wheeely.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@turbo=0;client-nonce=17e37293303044ddbf0e197a6a039a28;id=c6cb5b87-457b-47da-ba51-1ee629795f82;display-name=DontCagePlebs;color=#DAA520;user-id=85837900;mod=0;room-id=62300805;user-type=;tmi-sent-ts=1704558216363;subscriber=1;flags=;returning-chatter=0;first-msg=0;emotes=;badges=subscriber/36,no_audio/1;historical=1;badge-info=subscriber/37;rm-received-ts=1704558216543 :dontcageplebs!dontcageplebs@dontcageplebs.tmi.twitch.tv PRIVMSG #nymn that","@subscriber=0;user-type=;color=#B22222;rm-received-ts=1704558216994;tmi-sent-ts=1704558216810;badges=;user-id=109362100;room-id=62300805;mod=0;returning-chatter=0;id=c55dfe2e-9a73-4417-a2b5-240ea77e0df8;turbo=0;badge-info=;client-nonce=fe08b067d55e940ce004afbb906b158f;display-name=Shinoerah;flags=;first-msg=0;historical=1;emotes= :shinoerah!shinoerah@shinoerah.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@badges=subscriber/9,turbo/1;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122;id=e0aafa0f-5fd8-4110-9368-39ae73a8b107;tmi-sent-ts=1704558218518;user-type=;client-nonce=df7426e2177ecedd715fea773f0e8bd9;turbo=1;room-id=62300805;first-msg=0;badge-info=subscriber/9;user-id=40037186;color=#FFFF00;subscriber=1;display-name=Kotzblitz20;mod=0;returning-chatter=0;rm-received-ts=1704558218703;flags=;historical=1 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@user-id=85115603;client-nonce=542f6d96cf7240e1b4c99df8eb7a5fa4;rm-received-ts=1704558218954;badges=no_audio/1;room-id=62300805;display-name=OfficialScrap;first-msg=0;emotes=emotesv2_01092a3cb1324c17b66c443373e12519:0-6;emote-only=1;subscriber=0;historical=1;turbo=0;tmi-sent-ts=1704558218740;flags=;color=#0000FF;id=13905bb2-7625-4b16-9dc5-cac661d00cbc;returning-chatter=0;user-type=;badge-info=;mod=0 :officialscrap!officialscrap@officialscrap.tmi.twitch.tv PRIVMSG #nymn p00sJAM","@emotes=;client-nonce=ff3b03da2760ca14759555471da4e83b;display-name=Patixxl;rm-received-ts=1704558219617;id=6695a2e7-e9ef-48f4-8163-0395e72c026c;flags=;user-id=51967700;room-id=62300805;first-msg=0;turbo=0;subscriber=0;badge-info=;mod=0;color=#FF0000;badges=;historical=1;returning-chatter=0;user-type=;tmi-sent-ts=1704558219452 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@returning-chatter=0;first-msg=0;client-nonce=20c4bc114bc6056a873c29c74d9364cb;flags=;tmi-sent-ts=1704558219440;emotes=;id=b55fd88f-3cd9-4934-abcc-284f0ae544df;historical=1;badge-info=;turbo=0;color=#0000FF;badges=premium/1;room-id=62300805;rm-received-ts=1704558219643;subscriber=0;mod=0;user-id=36237730;display-name=Raztheman;user-type= :raztheman!raztheman@raztheman.tmi.twitch.tv PRIVMSG #nymn <---","@emotes=;subscriber=1;turbo=0;color=#63BD68;badges=subscriber/36,twitch-recap-2023/1;returning-chatter=0;badge-info=subscriber/38;user-id=433352132;id=06df5f35-8762-426e-99dd-b8ba5322597e;tmi-sent-ts=1704558220587;first-msg=0;display-name=jontEmillian;flags=;mod=0;historical=1;room-id=62300805;rm-received-ts=1704558220758;user-type= :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn Concerned","@flags=;emotes=;historical=1;id=f4f5376a-1654-4a9e-8167-4b81dc130377;mod=0;badge-info=subscriber/77;returning-chatter=0;vip=1;user-id=87120320;badges=vip/1,subscriber/72,rplace-2023/1;user-type=;turbo=0;subscriber=1;display-name=Joshlad;room-id=62300805;color=#D52AFF;rm-received-ts=1704558221293;first-msg=0;tmi-sent-ts=1704558221139 :joshlad!joshlad@joshlad.tmi.twitch.tv PRIVMSG #nymn HUH","@mod=0;subscriber=1;emotes=;display-name=SecretCarrot;historical=1;first-msg=0;rm-received-ts=1704558221481;color=#00615C;turbo=0;tmi-sent-ts=1704558221312;badge-info=subscriber/55;returning-chatter=0;user-type=;room-id=62300805;flags=;id=e62b5ca4-2683-4a65-a3d7-28bac3523254;badges=subscriber/54,bits/1000;client-nonce=68bc13d6a87381b4d797b7073a4f2b28;user-id=103592036 :secretcarrot!secretcarrot@secretcarrot.tmi.twitch.tv PRIVMSG #nymn HUH","@flags=;badge-info=subscriber/29;first-msg=0;returning-chatter=0;mod=0;rm-received-ts=1704558221513;turbo=0;badges=subscriber/24,sub-gifter/5;id=6f2679ef-817b-4fac-8d8d-fde1ea356df0;display-name=PepePge;historical=1;emotes=425618:27-29;room-id=62300805;tmi-sent-ts=1704558221342;subscriber=1;client-nonce=7bf152662cc24fa515fa28944e2f04e6;color=#9ACD32;user-id=544395745;user-type= :pepepge!pepepge@pepepge.tmi.twitch.tv PRIVMSG #nymn :did a poro make this game? LUL","@display-name=Kotzblitz20;rm-received-ts=1704558221569;user-id=40037186;flags=;badges=subscriber/9,turbo/1;returning-chatter=0;id=6db9bd08-e46c-4035-8c3a-8f2d4bac0df9;subscriber=1;tmi-sent-ts=1704558221396;client-nonce=390a766d0665389aedb2dfb702ff5c0b;historical=1;first-msg=0;mod=0;user-type=;color=#FFFF00;badge-info=subscriber/9;turbo=1;room-id=62300805;emotes= :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn Concerned","@badges=twitch-recap-2023/1;user-type=;user-id=38635616;rm-received-ts=1704558221931;id=10824ced-3c2f-4a26-86bc-33e7b4eee126;emotes=;historical=1;subscriber=0;returning-chatter=0;client-nonce=8939dcb4340b62660c43232d70e1cc25;mod=0;room-id=62300805;badge-info=;tmi-sent-ts=1704558221760;flags=;first-msg=0;display-name=KelemvorUber;color=#F7FF00;turbo=0 :kelemvoruber!kelemvoruber@kelemvoruber.tmi.twitch.tv PRIVMSG #nymn :always left","@rm-received-ts=1704558222731;display-name=mnqn18;subscriber=1;historical=1;emotes=;mod=0;user-type=;first-msg=0;returning-chatter=0;tmi-sent-ts=1704558222548;badge-info=subscriber/2;id=107f12c8-cd85-4db6-ab52-cf28d0b33093;room-id=62300805;color=#1E90FF;client-nonce=641985ae66ce2fdbfdbf1b1d9865fa98;badges=subscriber/0,premium/1;flags=;user-id=474204887;turbo=0 :mnqn18!mnqn18@mnqn18.tmi.twitch.tv PRIVMSG #nymn HUH","@user-type=;turbo=0;rm-received-ts=1704558223092;emotes=;room-id=62300805;client-nonce=8243d06b2d8a99883a56516b046b3470;display-name=Patixxl;flags=;tmi-sent-ts=1704558222923;subscriber=0;badge-info=;historical=1;user-id=51967700;id=d45ae9af-27a4-4d28-9d47-4048b9a00784;returning-chatter=0;first-msg=0;color=#FF0000;badges=;mod=0 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn HUH","@turbo=0;id=236de330-57cf-4386-b758-7b9eecbe6ce2;user-type=;user-id=222340799;returning-chatter=0;emotes=;badges=subscriber/6,chatter-cs-go-2022/1;badge-info=subscriber/7;rm-received-ts=1704558223208;subscriber=1;color=#B22222;tmi-sent-ts=1704558223014;mod=0;room-id=62300805;historical=1;client-nonce=1a4f40efea22d22c3edc1082ed4bbf94;first-msg=0;display-name=crazyjuni0r_;flags= :crazyjuni0r_!crazyjuni0r_@crazyjuni0r_.tmi.twitch.tv PRIVMSG #nymn HUH","@mod=0;returning-chatter=0;user-id=103665668;id=16c3ac01-33db-4065-83f3-27582f0f4bbe;user-type=;turbo=0;historical=1;first-msg=0;subscriber=0;badges=bits-charity/1;rm-received-ts=1704558223794;color=#0000FF;flags=;room-id=62300805;display-name=Intel_power;badge-info=;emotes=;client-nonce=6392bd15ea31c67a96244799eb9f730d;tmi-sent-ts=1704558223609 :intel_power!intel_power@intel_power.tmi.twitch.tv PRIVMSG #nymn Gayge","@color=#9ACD32;subscriber=0;id=b1f8cdcd-576b-49e5-b5f8-985d3a4efb0f;room-id=62300805;tmi-sent-ts=1704558223852;badges=twitch-recap-2023/1;client-nonce=f12d54f6a6ed9a6027c9345cb4354144;badge-info=;emotes=;flags=;historical=1;first-msg=0;mod=0;display-name=theKiryu;turbo=0;user-id=135853293;rm-received-ts=1704558224024;user-type=;returning-chatter=0 :thekiryu!thekiryu@thekiryu.tmi.twitch.tv PRIVMSG #nymn ????","@turbo=1;badge-info=subscriber/9;room-id=62300805;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58;historical=1;tmi-sent-ts=1704558223958;color=#FFFF00;flags=;client-nonce=a59c8038ada882868f5dccc6f858e9bf;display-name=Kotzblitz20;returning-chatter=0;user-id=40037186;user-type=;mod=0;first-msg=0;id=b751a54d-3844-496c-980f-c3f3d75d9add;rm-received-ts=1704558224152;badges=subscriber/9,turbo/1;subscriber=1 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@color=#000000;badge-info=;subscriber=0;user-id=205837377;tmi-sent-ts=1704558224324;client-nonce=782c1eb8c40a0704d92a886a6dc3b68a;returning-chatter=0;mod=0;id=d8fd79ea-dee8-41d6-a7ac-14ae49a2ad3a;display-name=Duchene;turbo=0;room-id=62300805;badges=;emotes=;first-msg=0;user-type=;flags=;rm-received-ts=1704558224500;historical=1 :duchene!duchene@duchene.tmi.twitch.tv PRIVMSG #nymn HUH'","@rm-received-ts=1704558224690;emotes=;first-msg=0;flags=;user-type=;turbo=0;badge-info=;returning-chatter=0;historical=1;display-name=BastunGuy1;user-id=232078107;subscriber=0;client-nonce=9b54e6ce823c0c29e0a65f1633a57be4;tmi-sent-ts=1704558224485;id=28f5d627-5fbb-4930-ba79-6494c0cdfb45;room-id=62300805;mod=0;badges=no_audio/1;color=#008000 :bastunguy1!bastunguy1@bastunguy1.tmi.twitch.tv PRIVMSG #nymn Corncerned","@returning-chatter=0;id=857f8b17-d7f9-43b8-8f4c-6e95aeaa8306;historical=1;mod=0;badge-info=;user-id=37931493;flags=;badges=no_audio/1;display-name=deever44;first-msg=0;rm-received-ts=1704558225088;subscriber=0;emotes=;client-nonce=a19c41ed0b5acf782bd483c005ce6112;color=;room-id=62300805;turbo=0;tmi-sent-ts=1704558224915;user-type= :deever44!deever44@deever44.tmi.twitch.tv PRIVMSG #nymn HUH","@turbo=0;returning-chatter=0;client-nonce=9857c522a35bea622d65cba536f101dd;room-id=62300805;id=db445036-c33d-4679-8168-630820812998;subscriber=0;user-id=133344079;user-type=;flags=;color=#5F9EA0;badges=no_audio/1;emotes=;tmi-sent-ts=1704558224950;rm-received-ts=1704558225138;first-msg=0;mod=0;badge-info=;display-name=sehtt_;historical=1 :sehtt_!sehtt_@sehtt_.tmi.twitch.tv PRIVMSG #nymn Corncerned","@first-msg=0;turbo=0;badges=twitch-recap-2023/1;mod=0;client-nonce=da9edb223c233e806137137acd23c1f7;returning-chatter=0;subscriber=0;color=#10E2E2;historical=1;badge-info=;tmi-sent-ts=1704558225186;flags=;room-id=62300805;rm-received-ts=1704558225363;id=c874099e-b05a-450a-9fa7-dd42342134e9;user-id=167633177;emotes=;display-name=ALotOfChickens;user-type= :alotofchickens!alotofchickens@alotofchickens.tmi.twitch.tv PRIVMSG #nymn LULE","@emotes=;color=#008000;user-id=278896263;tmi-sent-ts=1704558226128;first-msg=0;flags=;room-id=62300805;subscriber=1;id=df471785-73ef-4765-80db-7c25ef94cd8b;badge-info=subscriber/9;turbo=0;display-name=Phant0mBlades;returning-chatter=0;user-type=;historical=1;rm-received-ts=1704558226317;badges=subscriber/9,chatter-cs-go-2022/1;mod=0 :phant0mblades!phant0mblades@phant0mblades.tmi.twitch.tv PRIVMSG #nymn Concerned","@tmi-sent-ts=1704558226528;id=f32c091f-99e7-4b39-9f01-2cdbf546b0e8;user-type=;subscriber=1;emotes=;first-msg=0;rm-received-ts=1704558226715;room-id=62300805;badge-info=subscriber/15;returning-chatter=0;badges=subscriber/12,sub-gifter/5;historical=1;turbo=0;display-name=WhideX;color=#FAD130;client-nonce=6e63b8e40bbbb851c9370f758cad08db;mod=0;flags=;user-id=32852911 :whidex!whidex@whidex.tmi.twitch.tv PRIVMSG #nymn :doctorLeMonkePls where is lemonkeh","@subscriber=1;mod=0;flags=;badge-info=subscriber/9;display-name=Kotzblitz20;historical=1;room-id=62300805;user-type=;rm-received-ts=1704558227058;badges=subscriber/9,turbo/1;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138;returning-chatter=0;turbo=1;first-msg=0;client-nonce=97dce3755ff6270dd513a7d97975a2e0;user-id=40037186;id=51b4d0e9-9ade-448a-b797-bc7d3c575cdc;color=#FFFF00;tmi-sent-ts=1704558226865 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@returning-chatter=0;first-msg=0;subscriber=1;id=4d8afb5a-4010-47ab-a78e-fe5626e26023;emotes=;mod=0;color=#DAA520;badges=subscriber/36,no_audio/1;tmi-sent-ts=1704558227324;turbo=0;room-id=62300805;user-id=85837900;flags=;badge-info=subscriber/37;display-name=DontCagePlebs;historical=1;client-nonce=5a47549217db9176ece84064a2702e64;rm-received-ts=1704558227491;user-type= :dontcageplebs!dontcageplebs@dontcageplebs.tmi.twitch.tv PRIVMSG #nymn Concerned","@badges=subscriber/36,twitch-recap-2023/1;display-name=jontEmillian;emotes=;badge-info=subscriber/38;user-id=433352132;tmi-sent-ts=1704558227854;flags=;returning-chatter=0;color=#63BD68;mod=0;subscriber=1;room-id=62300805;turbo=0;rm-received-ts=1704558228032;historical=1;user-type=;id=de7d9806-30da-42a8-aaea-0b94639b509e;first-msg=0 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn forsenParty","@color=#FF0000;badges=;client-nonce=3a1035336e2636f20e9cefbdef867343;flags=;first-msg=0;returning-chatter=0;user-id=51967700;badge-info=;subscriber=0;mod=0;historical=1;rm-received-ts=1704558228200;turbo=0;emotes=;tmi-sent-ts=1704558228013;user-type=;room-id=62300805;display-name=Patixxl;id=ecc6d0e2-d998-4f0c-9eae-dfe539217c2f :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@tmi-sent-ts=1704558228290;user-id=159210800;badges=subscriber/48,bits/25000;mod=0;first-msg=0;turbo=0;user-type=;client-nonce=9f9b7d79143d37e46881fdf211942220;display-name=ME_ME;flags=;id=3349c792-b3e5-4933-b963-26a66336ca63;historical=1;emote-only=1;returning-chatter=0;color=#FF2424;badge-info=subscriber/49;subscriber=1;emotes=300799759:0-7;rm-received-ts=1704558228468;room-id=62300805 :me_me!me_me@me_me.tmi.twitch.tv PRIVMSG #nymn forsenBB","@flags=;badge-info=subscriber/2;rm-received-ts=1704558229149;subscriber=1;historical=1;mod=0;turbo=0;color=#FF69B4;tmi-sent-ts=1704558228978;user-id=22733078;badges=subscriber/0,bits/1;returning-chatter=0;user-type=;emotes=;id=4106f91f-7d43-48bc-9667-166ea9e6ba77;room-id=62300805;client-nonce=a1f82be67dd23bad82e1442f786c9a52;display-name=miniwoffer;first-msg=0 :miniwoffer!miniwoffer@miniwoffer.tmi.twitch.tv PRIVMSG #nymn :Ratge Ratbattle","@subscriber=1;historical=1;first-msg=0;client-nonce=e52bff711d003bd2c9eabfe85a18c268;flags=;user-id=40037186;display-name=Kotzblitz20;returning-chatter=0;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58;badge-info=subscriber/9;tmi-sent-ts=1704558230099;room-id=62300805;id=4ac512df-6f66-477d-bd0f-33cecffeb0d0;user-type=;color=#FFFF00;badges=subscriber/9,turbo/1;turbo=1;mod=0;rm-received-ts=1704558230284 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@badge-info=subscriber/51;historical=1;room-id=62300805;display-name=melito87;first-msg=0;user-id=35997610;id=597453bf-757c-4e2d-bee9-14c14b721a94;color=#008000;subscriber=1;turbo=0;tmi-sent-ts=1704558230227;client-nonce=edc2bb37d1376ea9547f9670a4da0bfb;user-type=;badges=subscriber/48,twitch-recap-2023/1;flags=;emotes=;rm-received-ts=1704558230408;mod=0;returning-chatter=0 :melito87!melito87@melito87.tmi.twitch.tv PRIVMSG #nymn DonkPls","@badges=;display-name=Patixxl;historical=1;first-msg=0;rm-received-ts=1704558231032;tmi-sent-ts=1704558230856;badge-info=;turbo=0;returning-chatter=0;client-nonce=8f93b3e7e0de063d9cba9dac62267be4;room-id=62300805;subscriber=0;mod=0;user-type=;id=4df42aa9-fe22-4d24-bc1f-d65cea09bd0a;emotes=;user-id=51967700;flags=;color=#FF0000 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@user-id=159210800;mod=0;room-id=62300805;rm-received-ts=1704558231049;badge-info=subscriber/49;returning-chatter=0;user-type=;emote-only=1;tmi-sent-ts=1704558230870;badges=subscriber/48,bits/25000;id=aff7f8a0-7f0b-4703-9a83-b74f2f1be13d;first-msg=0;color=#FF2424;historical=1;display-name=ME_ME;turbo=0;client-nonce=b9d4ea3cd0ccd2dc9b21de9a1de746ba;flags=;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10;subscriber=1 :me_me!me_me@me_me.tmi.twitch.tv PRIVMSG #nymn forsenParty","@flags=;historical=1;rm-received-ts=1704558232565;emotes=;returning-chatter=0;tmi-sent-ts=1704558232372;color=#8A2BE2;client-nonce=e98dc3b209b403537959d35a23ec2d2f;room-id=62300805;first-msg=0;mod=0;user-id=46199261;turbo=0;subscriber=0;user-type=;badges=no_audio/1;id=c302a6ae-f299-4fd6-a298-3e66d72b5d5e;badge-info=;display-name=Obiwun :obiwun!obiwun@obiwun.tmi.twitch.tv PRIVMSG #nymn NowWot","@mod=0;emotes=emotesv2_2f9a36844b054423833c817b5f8d4225:0-8;subscriber=0;display-name=zzlint;badges=;client-nonce=4b2d731da157c7e579169b1612e9f200;color=#1E90FF;turbo=0;id=a0d66900-ee81-4c75-8eab-4b4b06208794;room-id=62300805;first-msg=0;historical=1;flags=;user-id=29764188;returning-chatter=0;badge-info=;rm-received-ts=1704558232703;emote-only=1;user-type=;tmi-sent-ts=1704558232530 :zzlint!zzlint@zzlint.tmi.twitch.tv PRIVMSG #nymn forsenPls","@room-id=62300805;emotes=;client-nonce=cafdaa958a96a67bb92177ba6259f4be;tmi-sent-ts=1704558232712;id=8c655aa9-b507-427f-bba6-8b9cefd183cb;rm-received-ts=1704558232879;subscriber=0;user-id=135853293;badges=twitch-recap-2023/1;historical=1;mod=0;color=#9ACD32;user-type=;flags=;returning-chatter=0;turbo=0;badge-info=;first-msg=0;display-name=theKiryu :thekiryu!thekiryu@thekiryu.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,17-27,33-43,49-59,65-75,81-91,97-107,113-123,129-139,145-155,161-171,177-187,193-203;first-msg=0;id=918ad345-8302-409a-810a-f7d3cd990dd5;rm-received-ts=1704558233608;color=#FF0000;user-type=;returning-chatter=0;tmi-sent-ts=1704558233428;room-id=62300805;turbo=0;historical=1;user-id=45923155;badges=bits/1000;subscriber=0;mod=0;badge-info=;display-name=HajleSellasje;flags= :hajlesellasje!hajlesellasje@hajlesellasje.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@id=13de11dc-da21-46d9-b0f7-1f7e6203c6d6;historical=1;user-id=117088592;rm-received-ts=1704558233807;display-name=h_h410;turbo=0;mod=0;emotes=684692:0-6;tmi-sent-ts=1704558233611;first-msg=0;subscriber=1;room-id=62300805;user-type=;color=#00FF7F;returning-chatter=0;badges=subscriber/54,chatter-cs-go-2022/1;badge-info=subscriber/54;flags= :h_h410!h_h410@h_h410.tmi.twitch.tv PRIVMSG #nymn :forsenL PianoTime","@room-id=62300805;user-id=40037186;first-msg=0;flags=;subscriber=1;badges=subscriber/9,turbo/1;color=#FFFF00;id=72f74036-db1d-4416-b87d-fbe62ef77606;badge-info=subscriber/9;rm-received-ts=1704558234322;returning-chatter=0;tmi-sent-ts=1704558234153;mod=0;turbo=1;user-type=;historical=1;emotes=;client-nonce=0be525ec8c7b66646edee4cbf595a836;display-name=Kotzblitz20 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn FeelsWeirdMan","@client-nonce=0d54a5dca5773953600a2b07960510c5;badges=subscriber/36,no_audio/1;rm-received-ts=1704558235635;user-id=85837900;flags=;mod=0;turbo=0;display-name=DontCagePlebs;first-msg=0;user-type=;historical=1;badge-info=subscriber/37;color=#DAA520;id=2d491eee-7757-4799-8f17-0fc538735226;returning-chatter=0;subscriber=1;emotes=;room-id=62300805;tmi-sent-ts=1704558235452 :dontcageplebs!dontcageplebs@dontcageplebs.tmi.twitch.tv PRIVMSG #nymn Listening","@subscriber=0;emotes=;historical=1;user-id=51967700;room-id=62300805;color=#FF0000;mod=0;first-msg=0;turbo=0;badge-info=;user-type=;returning-chatter=0;tmi-sent-ts=1704558235610;badges=;client-nonce=1136cba2a04b2132b956f42a02b2dc8a;display-name=Patixxl;rm-received-ts=1704558235785;flags=;id=6e30133d-fd05-4675-81a0-eb5c9e2258bc :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn Listening","@user-id=35997610;display-name=melito87;tmi-sent-ts=1704558235866;subscriber=1;historical=1;id=511de0a6-72f7-4c89-bb3c-8ce5f2d52790;flags=;badges=subscriber/48,twitch-recap-2023/1;badge-info=subscriber/51;returning-chatter=0;client-nonce=58983d4cef6f60b72541c99b5ef10a5a;mod=0;first-msg=0;rm-received-ts=1704558236097;room-id=62300805;emotes=;turbo=0;color=#008000;user-type= :melito87!melito87@melito87.tmi.twitch.tv PRIVMSG #nymn Listening","@mod=0;returning-chatter=0;user-id=69072013;emotes=;historical=1;turbo=0;badge-info=subscriber/17;first-msg=0;id=a84debfc-6793-4a2c-a19d-f99b5bfecd93;badges=subscriber/12,twitch-recap-2023/1;color=#0000FF;subscriber=1;rm-received-ts=1704558236223;client-nonce=5a31d0f9b31c11e060cbf40608dff124;tmi-sent-ts=1704558236047;room-id=62300805;flags=;display-name=SnuggleUncle;user-type= :snuggleuncle!snuggleuncle@snuggleuncle.tmi.twitch.tv PRIVMSG #nymn doctorLeMonkePls","@rm-received-ts=1704558236406;first-msg=0;flags=;tmi-sent-ts=1704558236231;room-id=62300805;returning-chatter=0;display-name=Kotzblitz20;id=0a60976d-4da7-45a3-98c7-efc6f82bf566;client-nonce=22702fc3ca38b4db24aa635ad2ecb5ab;user-type=;color=#FFFF00;emotes=;turbo=1;historical=1;badge-info=subscriber/9;user-id=40037186;subscriber=1;mod=0;badges=subscriber/9,turbo/1 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn FeelsOkayMan","@user-id=87120320;user-type=;turbo=0;badge-info=subscriber/77;vip=1;mod=0;tmi-sent-ts=1704558236236;room-id=62300805;subscriber=1;rm-received-ts=1704558236409;display-name=Joshlad;color=#D52AFF;flags=;returning-chatter=0;emotes=emotesv2_11eff6a54749464c9dfa40570dd356bd:0-7;historical=1;id=b1dd88b1-e757-4f48-be25-f2660e24d9ac;badges=vip/1,subscriber/72,rplace-2023/1;first-msg=0 :joshlad!joshlad@joshlad.tmi.twitch.tv PRIVMSG #nymn :elisElis PianoTime","@tmi-sent-ts=1704558236400;turbo=0;first-msg=0;rm-received-ts=1704558236586;emotes=;display-name=MaxThurian;color=#00ED2A;client-nonce=819f5873de0312ca4856c51769e03293;user-id=60181947;badges=subscriber/42,twitch-recap-2023/1;historical=1;subscriber=1;user-type=;mod=0;returning-chatter=0;badge-info=subscriber/43;room-id=62300805;id=79445cd0-0e3f-4153-a868-e969a53e95a1;flags= :maxthurian!maxthurian@maxthurian.tmi.twitch.tv PRIVMSG #nymn AlienUnpleased","@display-name=boogkitty;user-type=;id=c213a2c3-4308-4122-99cd-b0a6c1b4bc68;tmi-sent-ts=1704558237380;user-id=154079285;subscriber=1;flags=;returning-chatter=0;turbo=0;room-id=62300805;color=#00FF7F;first-msg=0;emotes=;badges=subscriber/36;rm-received-ts=1704558237545;client-nonce=08a1cff4c641292bcc832efcaf00c22a;badge-info=subscriber/41;mod=0;historical=1 :boogkitty!boogkitty@boogkitty.tmi.twitch.tv PRIVMSG #nymn :Ratge RaveTime 󠀀","@turbo=0;historical=1;badges=bits/1000;returning-chatter=0;mod=0;flags=;emotes=;rm-received-ts=1704558237958;user-id=45923155;tmi-sent-ts=1704558237787;first-msg=0;subscriber=0;id=36029869-7918-4a73-a9a0-937fa8f42e13;user-type=;badge-info=;color=#FF0000;room-id=62300805;display-name=HajleSellasje :hajlesellasje!hajlesellasje@hajlesellasje.tmi.twitch.tv PRIVMSG #nymn Listening","@display-name=jontEmillian;first-msg=0;returning-chatter=0;badge-info=subscriber/38;flags=;historical=1;rm-received-ts=1704558238123;subscriber=1;emotes=;user-id=433352132;tmi-sent-ts=1704558237959;badges=subscriber/36,twitch-recap-2023/1;turbo=0;user-type=;mod=0;room-id=62300805;id=d831ed70-cc93-49d5-8b30-265717a30f3f;color=#63BD68 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn Life","@color=#9ACD32;id=eeb40d48-f4ca-4fe1-9dd1-0e3ee5e60001;flags=;turbo=0;emotes=;historical=1;tmi-sent-ts=1704558238507;room-id=62300805;display-name=PepePge;badges=subscriber/24,sub-gifter/5;subscriber=1;badge-info=subscriber/29;user-id=544395745;user-type=;returning-chatter=0;mod=0;client-nonce=86a9b509b1082e35e0fabdd8c74d5132;first-msg=0;rm-received-ts=1704558238680 :pepepge!pepepge@pepepge.tmi.twitch.tv PRIVMSG #nymn doctorLeMonkePls","@client-nonce=5a9c8701326742cedfa460ed9ec83aa0;returning-chatter=0;badge-info=;mod=0;flags=;turbo=0;historical=1;display-name=ALotOfChickens;room-id=62300805;user-id=167633177;first-msg=0;subscriber=0;tmi-sent-ts=1704558238667;rm-received-ts=1704558238859;user-type=;badges=twitch-recap-2023/1;emotes=;color=#10E2E2;id=773ad414-e2cf-4591-ab89-a9d75ebbcea7 :alotofchickens!alotofchickens@alotofchickens.tmi.twitch.tv PRIVMSG #nymn :Ratge PianoTime","@turbo=1;rm-received-ts=1704558240268;first-msg=0;historical=1;color=#FFFF00;badges=subscriber/9,turbo/1;mod=0;user-type=;returning-chatter=0;subscriber=1;display-name=Kotzblitz20;id=7d531d06-1573-4319-824c-7cf947a2d8b2;tmi-sent-ts=1704558240087;emotes=;client-nonce=49c72507f6f1d4de73b47e169bef09e1;room-id=62300805;flags=;badge-info=subscriber/9;user-id=40037186 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn Listening","@historical=1;rm-received-ts=1704558240269;badge-info=subscriber/54;id=9fdbaa4b-2d9c-49b7-a8f4-30c99ee3618c;emote-only=1;color=#00FF7F;subscriber=1;badges=subscriber/54,chatter-cs-go-2022/1;returning-chatter=0;mod=0;tmi-sent-ts=1704558240084;turbo=0;first-msg=0;display-name=h_h410;user-id=117088592;room-id=62300805;user-type=;flags=;emotes=31021:0-6 :h_h410!h_h410@h_h410.tmi.twitch.tv PRIVMSG #nymn forsenW","@subscriber=1;badges=subscriber/24,no_audio/1;emotes=;display-name=Tad0ch;flags=;mod=0;badge-info=subscriber/26;user-type=;turbo=0;first-msg=0;id=714bf216-1b0c-42b4-b626-2ce6c02e9f1a;user-id=110476113;room-id=62300805;tmi-sent-ts=1704558241625;color=#B22222;returning-chatter=0;client-nonce=54523ea77020fcefa6105f70a851baec;historical=1;rm-received-ts=1704558241823 :tad0ch!tad0ch@tad0ch.tmi.twitch.tv PRIVMSG #nymn :nymn? catAsk","@turbo=0;flags=;display-name=deever44;id=79d673ab-b725-4751-97be-ad7b2aa01a20;badge-info=;color=;user-id=37931493;subscriber=0;tmi-sent-ts=1704558241671;historical=1;returning-chatter=0;user-type=;mod=0;rm-received-ts=1704558241837;room-id=62300805;client-nonce=62314f590be588513f3134ddf9a610e2;first-msg=0;emotes=emotesv2_11eff6a54749464c9dfa40570dd356bd:0-7;badges=no_audio/1 :deever44!deever44@deever44.tmi.twitch.tv PRIVMSG #nymn :elisElis PianoTime","@flags=;user-id=278896263;rm-received-ts=1704558242068;historical=1;mod=0;badges=subscriber/9,chatter-cs-go-2022/1;emotes=;room-id=62300805;display-name=Phant0mBlades;tmi-sent-ts=1704558241899;first-msg=0;id=4165444c-fa5d-4bc8-9784-25fd51ab4b3e;badge-info=subscriber/9;returning-chatter=0;color=#008000;turbo=0;subscriber=1;user-type= :phant0mblades!phant0mblades@phant0mblades.tmi.twitch.tv PRIVMSG #nymn :2023 weeb arc 2024 furry arc Aware","@badges=vip/1,subscriber/72,rplace-2023/1;flags=;emotes=emotesv2_11eff6a54749464c9dfa40570dd356bd:0-7;mod=0;color=#D52AFF;room-id=62300805;badge-info=subscriber/77;first-msg=0;user-type=;subscriber=1;user-id=87120320;id=f6513acc-338c-4adb-872b-3cb0a5928b37;historical=1;rm-received-ts=1704558243297;returning-chatter=0;vip=1;display-name=Joshlad;tmi-sent-ts=1704558243121;turbo=0 :joshlad!joshlad@joshlad.tmi.twitch.tv PRIVMSG #nymn :elisElis PianoTime","@color=#00CC00;user-type=;badges=subscriber/6;returning-chatter=0;first-msg=0;turbo=0;historical=1;client-nonce=9396760182f94c208fd907379b9db206;user-id=35778622;display-name=jollyaustin1;badge-info=subscriber/7;id=8413fb34-d789-4d44-bb31-6fbfaca5085c;emotes=emotesv2_ba11c3b03cfd40f9bfa52851d03e4bdc:0-10,12-22,24-34,36-46;room-id=62300805;emote-only=1;tmi-sent-ts=1704558243173;subscriber=1;mod=0;rm-received-ts=1704558243367;flags= :jollyaustin1!jollyaustin1@jollyaustin1.tmi.twitch.tv PRIVMSG #nymn :jaboodyVibe jaboodyVibe jaboodyVibe jaboodyVibe","@tmi-sent-ts=1704558245601;returning-chatter=0;emote-only=1;historical=1;id=22057025-b7b1-4cfd-98a7-83ec1f80be1f;display-name=jonhycrack;color=#008000;user-type=;mod=0;badge-info=;flags=;emotes=31021:0-6;rm-received-ts=1704558245806;badges=no_audio/1;room-id=62300805;user-id=431946171;subscriber=0;turbo=0;first-msg=0 :jonhycrack!jonhycrack@jonhycrack.tmi.twitch.tv PRIVMSG #nymn forsenW","@user-id=40037186;subscriber=1;historical=1;tmi-sent-ts=1704558245763;client-nonce=66b87f5fa7a6b892285522c84d47d7c2;badge-info=subscriber/9;display-name=Kotzblitz20;flags=;mod=0;emotes=;returning-chatter=0;first-msg=0;color=#FFFF00;turbo=1;rm-received-ts=1704558245937;id=6d2d7bc0-7b07-4985-baba-f2769e258981;badges=subscriber/9,turbo/1;room-id=62300805;user-type= :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :Listening okay","@room-id=62300805;rm-received-ts=1704558248154;mod=0;user-type=;first-msg=0;emotes=;tmi-sent-ts=1704558247980;turbo=0;subscriber=0;badges=;id=6f0e4b33-9e32-4101-91dd-5f6454735b32;flags=;returning-chatter=0;display-name=123homo;user-id=133862911;badge-info=;color=#FF0000;client-nonce=e87c7a52e31410d47a51a845a33de3c3;historical=1 :123homo!123homo@123homo.tmi.twitch.tv PRIVMSG #nymn :I HATE ROBOTS","@historical=1;first-msg=0;badges=subscriber/48,bits/25000;subscriber=1;returning-chatter=0;flags=;user-id=159210800;emotes=;badge-info=subscriber/49;client-nonce=1077e8a58a4d3eccf8d477f206eefc2d;display-name=ME_ME;id=625d3098-2544-4ed8-bbab-03aea8a75bf4;user-type=;rm-received-ts=1704558249138;mod=0;room-id=62300805;color=#FF2424;tmi-sent-ts=1704558248934;turbo=0 :me_me!me_me@me_me.tmi.twitch.tv PRIVMSG #nymn :Ratge PianoTime","@display-name=forsenkkona_;room-id=62300805;emotes=31021:0-6;badge-info=;first-msg=0;user-type=;badges=;emote-only=1;user-id=151423066;subscriber=0;mod=0;color=#FF69B4;turbo=0;tmi-sent-ts=1704558249935;historical=1;flags=;id=61c00d37-d162-417b-a643-9eb235d2fb0b;rm-received-ts=1704558250118;returning-chatter=0 :forsenkkona_!forsenkkona_@forsenkkona_.tmi.twitch.tv PRIVMSG #nymn forsenW","@user-type=;rm-received-ts=1704558250972;subscriber=1;user-id=103592036;mod=0;badges=subscriber/54,bits/1000;emotes=300740045:0-7;badge-info=subscriber/55;display-name=SecretCarrot;id=55ac1256-2938-4d27-ac4b-7b6bf01bfcb4;turbo=0;tmi-sent-ts=1704558250799;color=#00615C;returning-chatter=0;room-id=62300805;client-nonce=de42bdc9495986fc2f785cd0a8a68c28;first-msg=0;flags=;historical=1 :secretcarrot!secretcarrot@secretcarrot.tmi.twitch.tv PRIVMSG #nymn :nymnOkey PianoTime","@historical=1;badge-info=;tmi-sent-ts=1704558251693;emotes=;turbo=0;client-nonce=f39deef850c729323b43d79168b82a54;first-msg=0;returning-chatter=0;display-name=Sailx;rm-received-ts=1704558251879;color=#B22222;mod=0;id=b860bc9b-fba9-4393-81cb-2aa6fcf56ca2;flags=;room-id=62300805;badges=glitchcon2020/1;subscriber=0;user-type=;user-id=40181060 :sailx!sailx@sailx.tmi.twitch.tv PRIVMSG #nymn :im relaxed pal","@room-id=62300805;mod=0;color=#B22222;id=0afec80d-fe09-40b9-b42b-35af5540e3fb;user-type=;user-id=222340799;tmi-sent-ts=1704558251935;client-nonce=4ea2d3683d13ac062e60a28d7ea686e5;display-name=crazyjuni0r_;badges=subscriber/6,chatter-cs-go-2022/1;emotes=;historical=1;badge-info=subscriber/7;first-msg=0;rm-received-ts=1704558252114;subscriber=1;turbo=0;returning-chatter=0;flags= :crazyjuni0r_!crazyjuni0r_@crazyjuni0r_.tmi.twitch.tv PRIVMSG #nymn 2024NymN","@user-id=117088592;badge-info=subscriber/54;emotes=;badges=subscriber/54,chatter-cs-go-2022/1;color=#00FF7F;turbo=0;user-type=;tmi-sent-ts=1704558253116;historical=1;returning-chatter=0;room-id=62300805;display-name=h_h410;rm-received-ts=1704558253308;id=80ebc3ad-db2d-4b83-987b-873ed316cd17;flags=;first-msg=0;subscriber=1;mod=0 :h_h410!h_h410@h_h410.tmi.twitch.tv PRIVMSG #nymn forseninsane","@tmi-sent-ts=1704558253145;room-id=62300805;user-id=85115603;subscriber=0;user-type=;client-nonce=d787e5e2c41c80a096df278f4daa6924;historical=1;emotes=;display-name=OfficialScrap;badges=no_audio/1;color=#0000FF;mod=0;id=a47f89c4-37aa-4101-afac-d14e4d98a341;returning-chatter=0;badge-info=;first-msg=0;turbo=0;flags=;rm-received-ts=1704558253349 :officialscrap!officialscrap@officialscrap.tmi.twitch.tv PRIVMSG #nymn relax","@historical=1;mod=0;turbo=0;display-name=lustforlife777;client-nonce=f68e38a70330c2b734fe06632d72d412;emotes=;badges=;user-id=748810228;first-msg=0;subscriber=0;rm-received-ts=1704558255033;flags=;returning-chatter=0;room-id=62300805;tmi-sent-ts=1704558254848;id=e61a5184-2694-47e7-a936-8603dc420741;user-type=;badge-info=;color= :lustforlife777!lustforlife777@lustforlife777.tmi.twitch.tv PRIVMSG #nymn :PagMan LEAN","@turbo=0;historical=1;user-id=765575336;emotes=;returning-chatter=0;id=c2eb1c85-2443-4705-bce2-48c8695667ed;badges=subscriber/3;user-type=;flags=;first-msg=0;client-nonce=b2123d92cdbbc3dbb343848a96febee5;subscriber=1;display-name=drtumbleweed1;room-id=62300805;badge-info=subscriber/5;tmi-sent-ts=1704558254965;color=#8A2BE2;mod=0;rm-received-ts=1704558255155 :drtumbleweed1!drtumbleweed1@drtumbleweed1.tmi.twitch.tv PRIVMSG #nymn :relax, will ya? @NymN","@historical=1;badge-info=subscriber/49;room-id=62300805;mod=0;display-name=ME_ME;flags=;returning-chatter=0;client-nonce=312a76f280971dd366c6814611645963;emotes=;id=9c25f9b8-40a2-46f0-98f4-ff56f8763503;badges=subscriber/48,bits/25000;rm-received-ts=1704558255595;user-type=;first-msg=0;user-id=159210800;color=#FF2424;tmi-sent-ts=1704558255435;subscriber=1;turbo=0 :me_me!me_me@me_me.tmi.twitch.tv PRIVMSG #nymn :Ratge PianoTime 󠀀","@emotes=emotesv2_7e256eaeb9cf4316bd2f0d7acd8ced78:107-115;returning-chatter=0;turbo=0;room-id=62300805;user-type=mod;user-id=97661864;historical=1;display-name=botnextdoor;badges=moderator/1,subscriber/72;badge-info=subscriber/101;tmi-sent-ts=1704558255942;subscriber=1;flags=;first-msg=0;id=b0d4ce2a-7e94-4322-b3b5-30a0d2fb9fd7;mod=1;color=#FF69B4;rm-received-ts=1704558256116 :botnextdoor!botnextdoor@botnextdoor.tmi.twitch.tv PRIVMSG #nymn :\u0001ACTION Make sure you follow NymN on Twitter to stay up-to-date on stream information: https://twitter.com/nymnion nymnBenis\u0001","@client-nonce=d8e626a3a2ff2d951b37ac92e6e32b9e;color=#8A2BE2;user-type=;emotes=;display-name=Obiwun;badges=no_audio/1;historical=1;badge-info=;turbo=0;rm-received-ts=1704558256176;room-id=62300805;tmi-sent-ts=1704558255984;subscriber=0;first-msg=0;id=e19e930e-f7b2-4006-a8bd-f8c7f79f6916;flags=;user-id=46199261;returning-chatter=0;mod=0 :obiwun!obiwun@obiwun.tmi.twitch.tv PRIVMSG #nymn :LEAN PagMan","@room-id=62300805;emotes=emotesv2_10304fc8867a4d3586aadf2c409b153a:0-14;turbo=0;returning-chatter=0;tmi-sent-ts=1704558256029;first-msg=0;historical=1;rm-received-ts=1704558256209;mod=0;subscriber=0;badges=no_audio/1;badge-info=;display-name=jonhycrack;flags=;id=35c6e928-e8fe-4a5a-ad20-c28eaa69d327;user-id=431946171;user-type=;color=#008000;emote-only=1 :jonhycrack!jonhycrack@jonhycrack.tmi.twitch.tv PRIVMSG #nymn forsenPossessed","@flags=;rm-received-ts=1704558256213;returning-chatter=0;tmi-sent-ts=1704558256035;id=166acfe3-8671-43d7-9236-082e25060cdc;historical=1;turbo=1;first-msg=0;client-nonce=505dd03bd2b4cacb1198c51b66f43813;color=#FFFF00;badges=subscriber/9,turbo/1;display-name=Kotzblitz20;subscriber=1;mod=0;room-id=62300805;user-type=;badge-info=subscriber/9;emotes=;user-id=40037186 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn IVEGONEPASTHEPOINTOFINSANITY","@returning-chatter=0;rm-received-ts=1704558256716;flags=;mod=0;badge-info=subscriber/38;user-id=433352132;user-type=;color=#63BD68;id=fa3c37e7-ba76-4657-b8c2-f3829cb79c7b;emotes=;badges=subscriber/36,twitch-recap-2023/1;room-id=62300805;tmi-sent-ts=1704558256539;subscriber=1;historical=1;display-name=jontEmillian;turbo=0;first-msg=0 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn :Life PianoTime","@color=#FF2424;tmi-sent-ts=1704558258865;historical=1;returning-chatter=0;user-type=;mod=0;badges=subscriber/48,bits/25000;user-id=159210800;badge-info=subscriber/49;id=2ca5796d-28d8-4500-9920-acd936161661;subscriber=1;display-name=ME_ME;flags=;first-msg=0;client-nonce=9cfacb4faf8fabb2bde2cb5a262b13c4;emotes=;turbo=0;room-id=62300805;rm-received-ts=1704558259043 :me_me!me_me@me_me.tmi.twitch.tv PRIVMSG #nymn :Based hating guy","@badges=no_audio/1;emotes=emotesv2_10304fc8867a4d3586aadf2c409b153a:0-14;first-msg=0;user-id=431946171;color=#008000;turbo=0;flags=;tmi-sent-ts=1704558258928;returning-chatter=0;id=696784a5-d335-4d95-87a3-ad2a502cb60c;subscriber=0;display-name=jonhycrack;user-type=;historical=1;rm-received-ts=1704558259103;room-id=62300805;mod=0;badge-info= :jonhycrack!jonhycrack@jonhycrack.tmi.twitch.tv PRIVMSG #nymn :forsenPossessed PianoTime","@rm-received-ts=1704558259408;first-msg=0;user-type=;id=82d32b72-6088-447f-bcd3-c6e5a77c3b24;mod=0;turbo=0;subscriber=0;badges=no_audio/1;emotes=;tmi-sent-ts=1704558259226;user-id=37931493;historical=1;badge-info=;client-nonce=ae3f42e962f06331b994b07ea4e94119;color=;returning-chatter=0;room-id=62300805;flags=;display-name=deever44 :deever44!deever44@deever44.tmi.twitch.tv PRIVMSG #nymn Listening","@user-type=;historical=1;subscriber=1;rm-received-ts=1704558260833;emotes=emotesv2_11eff6a54749464c9dfa40570dd356bd:0-7;turbo=0;id=492551a1-439f-46d5-a85c-ea22287b9ef6;user-id=87120320;mod=0;room-id=62300805;vip=1;color=#D52AFF;first-msg=0;returning-chatter=0;tmi-sent-ts=1704558260669;badges=vip/1,subscriber/72,rplace-2023/1;display-name=Joshlad;badge-info=subscriber/77;flags= :joshlad!joshlad@joshlad.tmi.twitch.tv PRIVMSG #nymn :elisElis PianoTime","@display-name=jamboz77;client-nonce=3ba324b439234a5c1ad64808bc74beb2;emotes=;historical=1;tmi-sent-ts=1704558261265;flags=;user-type=;badge-info=subscriber/2;color=#008000;subscriber=1;rm-received-ts=1704558261439;returning-chatter=0;badges=subscriber/0;mod=0;id=a2162bac-fce5-4c04-b935-b8ffb660ecb7;turbo=0;first-msg=0;user-id=899238522;room-id=62300805 :jamboz77!jamboz77@jamboz77.tmi.twitch.tv PRIVMSG #nymn xddShrug","@color=#008000;mod=0;id=9ab70b41-b87f-4c9d-988a-7d9cf3d6eadb;badges=no_audio/1;subscriber=0;badge-info=;historical=1;tmi-sent-ts=1704558261453;user-id=431946171;display-name=jonhycrack;turbo=0;returning-chatter=0;room-id=62300805;first-msg=0;flags=;rm-received-ts=1704558261626;emotes=emotesv2_10304fc8867a4d3586aadf2c409b153a:0-14;user-type= :jonhycrack!jonhycrack@jonhycrack.tmi.twitch.tv PRIVMSG #nymn :forsenPossessed PianoTime","@id=59018444-0a6a-4ea5-9d7d-096837e4e708;mod=0;first-msg=0;display-name=forsenkkona_;returning-chatter=0;historical=1;user-type=;subscriber=0;color=#FF69B4;room-id=62300805;flags=;emotes=;badge-info=;tmi-sent-ts=1704558261646;turbo=0;rm-received-ts=1704558261818;badges=;user-id=151423066 :forsenkkona_!forsenkkona_@forsenkkona_.tmi.twitch.tv PRIVMSG #nymn pepoJS","@badge-info=;rm-received-ts=1704558263279;mod=0;first-msg=0;flags=;client-nonce=9b504719660efeecfebbcf06661d74bd;badges=no_audio/1;user-id=232078107;tmi-sent-ts=1704558263094;id=aa12b892-419c-4680-8d93-a3e2edda95f8;returning-chatter=0;color=#008000;subscriber=0;turbo=0;user-type=;historical=1;display-name=BastunGuy1;emotes=;room-id=62300805 :bastunguy1!bastunguy1@bastunguy1.tmi.twitch.tv PRIVMSG #nymn :sippin on waka","@color=#DAA520;room-id=62300805;user-id=85837900;client-nonce=9b7bdfc126d2bb8a4988d8035e9e7e03;subscriber=1;flags=;emotes=;badges=subscriber/36,no_audio/1;display-name=DontCagePlebs;tmi-sent-ts=1704558264032;rm-received-ts=1704558264208;badge-info=subscriber/37;first-msg=0;user-type=;returning-chatter=0;mod=0;turbo=0;id=c9bb57a8-c98e-429e-8daf-866e9d4c164c;historical=1 :dontcageplebs!dontcageplebs@dontcageplebs.tmi.twitch.tv PRIVMSG #nymn :Listening zimmer","@badge-info=;subscriber=0;tmi-sent-ts=1704558264302;client-nonce=c9d4cc765aa9681bba45b5d51fb36c4e;turbo=0;room-id=62300805;first-msg=0;id=d522ca94-8f25-49ac-9679-e443172b320b;badges=bits/100;color=#25E000;historical=1;display-name=DM8917;mod=0;emotes=;user-id=63372784;returning-chatter=0;user-type=;flags=;rm-received-ts=1704558264492 :dm8917!dm8917@dm8917.tmi.twitch.tv PRIVMSG #nymn IVEGONEPASTHEPOINTOFINSANITY","@badges=subscriber/9,turbo/1;flags=;color=#FFFF00;user-type=;emotes=;display-name=Kotzblitz20;client-nonce=1a055b2faf02af4d702030a0ad287a1a;first-msg=0;turbo=1;rm-received-ts=1704558264547;room-id=62300805;tmi-sent-ts=1704558264381;returning-chatter=0;user-id=40037186;id=b9bfb05e-9691-40f1-be2b-62fc440aabd1;historical=1;mod=0;badge-info=subscriber/9;subscriber=1 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn notListening","@color=#1E90FF;rm-received-ts=1704558264981;badge-info=subscriber/2;display-name=mnqn18;badges=subscriber/0,premium/1;client-nonce=81ae041d66adf9c10dcac477ae532431;mod=0;first-msg=0;subscriber=1;room-id=62300805;user-type=;user-id=474204887;flags=;id=8aa6d737-feff-4dd7-b21f-c7f7f9ef94a4;emotes=;turbo=0;returning-chatter=0;tmi-sent-ts=1704558264812;historical=1 :mnqn18!mnqn18@mnqn18.tmi.twitch.tv PRIVMSG #nymn silly","@id=2cb48d7c-b30b-4364-8fc0-7a6f67136f02;badges=subscriber/6;badge-info=subscriber/7;subscriber=1;color=#00CC00;user-id=35778622;tmi-sent-ts=1704558264864;emotes=emotesv2_ba11c3b03cfd40f9bfa52851d03e4bdc:0-10,12-22,24-34;mod=0;room-id=62300805;turbo=0;flags=;user-type=;display-name=jollyaustin1;rm-received-ts=1704558265068;historical=1;returning-chatter=0;first-msg=0;emote-only=1;client-nonce=118f3f4b68f0f321aae13058a5921707 :jollyaustin1!jollyaustin1@jollyaustin1.tmi.twitch.tv PRIVMSG #nymn :jaboodyVibe jaboodyVibe jaboodyVibe","@color=#FF2424;mod=0;first-msg=0;subscriber=1;historical=1;user-type=;flags=;display-name=ME_ME;user-id=159210800;returning-chatter=0;tmi-sent-ts=1704558264950;turbo=0;badges=subscriber/48,bits/25000;room-id=62300805;rm-received-ts=1704558265145;emotes=;badge-info=subscriber/49;id=69a75a54-418c-4463-8a33-6aa0c8826ee3;client-nonce=eaedd8b040f1725f1d57db4a42f4ced4 :me_me!me_me@me_me.tmi.twitch.tv PRIVMSG #nymn :Ratge PianoTime","@display-name=jonhycrack;room-id=62300805;emotes=emotesv2_10304fc8867a4d3586aadf2c409b153a:0-14;flags=;historical=1;user-id=431946171;badges=no_audio/1;first-msg=0;id=4fd64510-d895-4c5f-a520-dfd6accb81a8;returning-chatter=0;badge-info=;rm-received-ts=1704558265826;color=#008000;tmi-sent-ts=1704558265645;subscriber=0;user-type=;turbo=0;mod=0 :jonhycrack!jonhycrack@jonhycrack.tmi.twitch.tv PRIVMSG #nymn :forsenPossessed PianoTime","@subscriber=0;emotes=;id=415e8239-28f4-405b-9172-91f7dae52e4b;rm-received-ts=1704558268847;tmi-sent-ts=1704558268657;client-nonce=c64bf406f2c9f7a129caacc57fd40711;first-msg=0;badge-info=;returning-chatter=0;display-name=DrBenDover69;flags=;user-type=;mod=0;historical=1;turbo=0;color=#9ACD32;room-id=62300805;user-id=218692219;badges=no_video/1 :drbendover69!drbendover69@drbendover69.tmi.twitch.tv PRIVMSG #nymn :drink sludge","@color=;display-name=deever44;subscriber=0;tmi-sent-ts=1704558269826;client-nonce=8c647aa0cf4a340ecc86bd41f6643b6c;historical=1;badge-info=;id=8076f2be-7dfa-409d-913d-778f4bf418be;rm-received-ts=1704558269993;badges=no_audio/1;returning-chatter=0;emotes=;flags=;turbo=0;room-id=62300805;user-type=;first-msg=0;mod=0;user-id=37931493 :deever44!deever44@deever44.tmi.twitch.tv PRIVMSG #nymn :i like this","@room-id=62300805;first-msg=0;client-nonce=c747aae941b9dc403961f7c0e1e650dc;id=c84da173-1c03-4e80-b280-d0e4c5a6040b;turbo=0;mod=0;color=#1E90FF;user-type=;display-name=kb_h;user-id=246452436;badges=rplace-2023/1;flags=;subscriber=0;historical=1;rm-received-ts=1704558271513;badge-info=;tmi-sent-ts=1704558271320;returning-chatter=0;emotes= :kb_h!kb_h@kb_h.tmi.twitch.tv PRIVMSG #nymn :the ratrooms","@color=#008000;room-id=62300805;turbo=0;first-msg=0;emotes=emotesv2_10304fc8867a4d3586aadf2c409b153a:0-14;returning-chatter=0;tmi-sent-ts=1704558271505;badge-info=;rm-received-ts=1704558271674;user-id=431946171;historical=1;id=b0ede228-f358-43a3-b7c1-1bf80bb4ffc4;subscriber=0;flags=;mod=0;user-type=;badges=no_audio/1;display-name=jonhycrack :jonhycrack!jonhycrack@jonhycrack.tmi.twitch.tv PRIVMSG #nymn :forsenPossessed PianoTime","@tmi-sent-ts=1704558273963;badge-info=subscriber/47;turbo=0;historical=1;room-id=62300805;flags=;rm-received-ts=1704558274152;user-type=;color=#FF0000;display-name=Mawsonator;returning-chatter=0;mod=0;user-id=92529125;first-msg=0;id=4cd50bb8-a43e-4363-9f47-90a94ede725a;subscriber=1;badges=subscriber/42,twitch-recap-2023/1;client-nonce=e451c38f001915e49f1cf9806b45b0eb;emotes= :mawsonator!mawsonator@mawsonator.tmi.twitch.tv PRIVMSG #nymn :Ratge PianoTime","@returning-chatter=0;user-id=133862911;display-name=123homo;color=#FF0000;turbo=0;flags=;subscriber=0;rm-received-ts=1704558276750;user-type=;first-msg=0;tmi-sent-ts=1704558276576;room-id=62300805;badges=;historical=1;id=1eae5b00-fcdd-4d7e-9d10-82e9fe4712bc;badge-info=;client-nonce=2ec844049390515d583d6f2299bbd115;mod=0;emotes= :123homo!123homo@123homo.tmi.twitch.tv PRIVMSG #nymn :I LIKE PIANO","@badges=;room-id=62300805;tmi-sent-ts=1704558277987;color=#FF0000;user-type=;client-nonce=e06360c705be1508bfd4b4a595fa25b7;emotes=;display-name=Patixxl;first-msg=0;returning-chatter=0;turbo=0;historical=1;flags=;badge-info=;subscriber=0;mod=0;user-id=51967700;id=cf56cfb0-672d-4b4c-a99e-5444af66e6e2;rm-received-ts=1704558278155 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn IVEGONEPASTHEPOINTOFINSANITY","@id=13aca2bd-2714-418f-b44e-c04367755dfa;tmi-sent-ts=1704558278196;user-type=;turbo=1;historical=1;room-id=62300805;display-name=Kotzblitz20;badges=subscriber/9,turbo/1;user-id=40037186;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122;color=#FFFF00;first-msg=0;subscriber=1;badge-info=subscriber/9;mod=0;flags=;returning-chatter=0;rm-received-ts=1704558278382;client-nonce=33769cc0cdaa95a18fe15be8e752dd77 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@turbo=0;display-name=jonhycrack;color=#008000;rm-received-ts=1704558278657;subscriber=0;id=52e2d672-5d66-4b3a-bff5-4a2867b00170;badges=no_audio/1;user-type=;historical=1;tmi-sent-ts=1704558278485;returning-chatter=0;room-id=62300805;mod=0;first-msg=0;flags=;badge-info=;user-id=431946171;emotes=emotesv2_10304fc8867a4d3586aadf2c409b153a:0-14 :jonhycrack!jonhycrack@jonhycrack.tmi.twitch.tv PRIVMSG #nymn :forsenPossessed PianoTime","@first-msg=0;tmi-sent-ts=1704558278749;returning-chatter=0;turbo=0;badge-info=subscriber/38;id=6f0d1001-ef3e-4f31-90d4-1618cf782082;color=#63BD68;user-type=;room-id=62300805;mod=0;historical=1;emotes=;subscriber=1;badges=subscriber/36,twitch-recap-2023/1;flags=;user-id=433352132;display-name=jontEmillian;rm-received-ts=1704558278919 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn forsenParty","@mod=0;badges=subscriber/54,chatter-cs-go-2022/1;rm-received-ts=1704558279245;historical=1;turbo=0;color=#00FF7F;badge-info=subscriber/54;flags=;first-msg=0;tmi-sent-ts=1704558279038;user-type=;display-name=h_h410;user-id=117088592;emotes=emotesv2_10304fc8867a4d3586aadf2c409b153a:0-14;id=1e5270df-c8ae-4140-96fa-e5106fa0af6e;subscriber=1;returning-chatter=0;room-id=62300805 :h_h410!h_h410@h_h410.tmi.twitch.tv PRIVMSG #nymn :forsenPossessed ♻","@flags=;badges=subscriber/0,no_audio/1;id=b66fe4d5-d546-437b-b514-72025a8e0443;mod=0;emotes=;tmi-sent-ts=1704558279121;color=#008000;subscriber=1;historical=1;first-msg=0;turbo=0;display-name=Lohatrons420;returning-chatter=0;rm-received-ts=1704558279290;user-type=;room-id=62300805;badge-info=subscriber/1;user-id=87509154 :lohatrons420!lohatrons420@lohatrons420.tmi.twitch.tv PRIVMSG #nymn :turn extension off nymn","@rm-received-ts=1704558280355;user-type=;room-id=62300805;tmi-sent-ts=1704558280174;flags=;emotes=;client-nonce=13a8b885fe4916a7637107826e811d5e;mod=0;turbo=0;first-msg=0;display-name=Patixxl;id=47781997-ef86-4641-a672-f00f38c92cd7;returning-chatter=0;subscriber=0;badges=;badge-info=;user-id=51967700;color=#FF0000;historical=1 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@user-type=;turbo=0;room-id=62300805;historical=1;flags=;rm-received-ts=1704558281463;user-id=85837900;first-msg=0;emotes=;subscriber=1;badges=subscriber/36,no_audio/1;tmi-sent-ts=1704558281272;color=#DAA520;id=09157acb-1435-4b95-8f36-330f880ca2bb;display-name=DontCagePlebs;client-nonce=fd61537ee54fe0cea08a9fe404338d32;badge-info=subscriber/37;returning-chatter=0;mod=0 :dontcageplebs!dontcageplebs@dontcageplebs.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@turbo=0;historical=1;returning-chatter=0;color=#9ACD32;flags=;emotes=;subscriber=0;first-msg=0;badges=twitch-recap-2023/1;user-type=;mod=0;user-id=135853293;id=5e7da21e-828d-44c5-9f0b-031531482989;client-nonce=16c0076df46b23825855f1dcd93e73a1;tmi-sent-ts=1704558281708;rm-received-ts=1704558281881;room-id=62300805;badge-info=;display-name=theKiryu :thekiryu!thekiryu@thekiryu.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@color=#008000;subscriber=1;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10;rm-received-ts=1704558281978;mod=0;first-msg=0;returning-chatter=0;flags=;historical=1;turbo=0;badge-info=subscriber/9;id=fec03a38-d33e-4308-abbb-3b53459bf6c2;user-type=;badges=subscriber/9,chatter-cs-go-2022/1;user-id=278896263;room-id=62300805;tmi-sent-ts=1704558281768;display-name=Phant0mBlades :phant0mblades!phant0mblades@phant0mblades.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM","@color=#FFFF00;client-nonce=0fb3374bae9f0e7b2817b2270494e3d1;flags=;user-id=40037186;badge-info=subscriber/9;first-msg=0;user-type=;turbo=1;badges=subscriber/9,turbo/1;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170,176-186,192-202,208-218,224-234,240-250,256-266,272-282;subscriber=1;display-name=Kotzblitz20;rm-received-ts=1704558282047;room-id=62300805;mod=0;returning-chatter=0;id=32e3766f-19a3-4a0c-8fb3-94590af0f090;tmi-sent-ts=1704558281856;historical=1 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@rm-received-ts=1704558282378;subscriber=1;client-nonce=a0c462e4a6da03f15a2a4d5268ac73c5;user-type=;display-name=SnuggleUncle;returning-chatter=0;room-id=62300805;emotes=;id=73778c4c-5457-4e7f-b51f-2c3c32bdeb26;mod=0;badges=subscriber/12,twitch-recap-2023/1;turbo=0;badge-info=subscriber/17;historical=1;tmi-sent-ts=1704558282189;color=#0000FF;first-msg=0;user-id=69072013;flags= :snuggleuncle!snuggleuncle@snuggleuncle.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@display-name=Joshlad;color=#D52AFF;vip=1;historical=1;tmi-sent-ts=1704558282715;flags=;emotes=;room-id=62300805;badges=vip/1,subscriber/72,rplace-2023/1;rm-received-ts=1704558282884;subscriber=1;badge-info=subscriber/77;user-id=87120320;user-type=;mod=0;id=4af9860d-97c1-467d-bf6f-eae593c2c08b;turbo=0;first-msg=0;returning-chatter=0 :joshlad!joshlad@joshlad.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@client-nonce=e2de0364c31b61506a35482bd829cfc0;emotes=;first-msg=0;color=#FF0000;returning-chatter=0;id=d51d2f38-9c2e-4ce3-b3c9-adf4241b6a20;flags=;badge-info=;rm-received-ts=1704558283246;subscriber=0;display-name=Patixxl;tmi-sent-ts=1704558283079;user-type=;mod=0;room-id=62300805;badges=;turbo=0;user-id=51967700;historical=1 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@display-name=jontEmillian;emotes=;turbo=0;color=#63BD68;tmi-sent-ts=1704558283447;flags=;subscriber=1;user-type=;badges=subscriber/36,twitch-recap-2023/1;rm-received-ts=1704558283609;historical=1;mod=0;returning-chatter=0;id=dca30460-3647-4650-ba94-2e6354489306;first-msg=0;room-id=62300805;user-id=433352132;badge-info=subscriber/38 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn :forsenParty 󠀀","@flags=;mod=0;badges=bits/1000;historical=1;rm-received-ts=1704558283899;room-id=62300805;color=#FF0000;badge-info=;display-name=HajleSellasje;turbo=0;first-msg=0;emotes=;subscriber=0;user-id=45923155;tmi-sent-ts=1704558283736;returning-chatter=0;id=b7a2aa46-8c0b-45db-823e-d49bf3bc64cb;user-type= :hajlesellasje!hajlesellasje@hajlesellasje.tmi.twitch.tv PRIVMSG #nymn :Listening 󠀀","@subscriber=0;user-id=59060199;tmi-sent-ts=1704558284051;color=;turbo=0;badges=;badge-info=;flags=;mod=0;room-id=62300805;user-type=;returning-chatter=0;emotes=;first-msg=0;display-name=bomberman2442;historical=1;client-nonce=eef2e2a42797ae0f7a0e7d2dcc297ed2;rm-received-ts=1704558284210;id=a58a9c53-f5bd-413c-9751-c16ecb29ce82 :bomberman2442!bomberman2442@bomberman2442.tmi.twitch.tv PRIVMSG #nymn :actually good game PagMan","@returning-chatter=0;user-type=;turbo=0;tmi-sent-ts=1704558284650;user-id=205837377;rm-received-ts=1704558284825;badge-info=;display-name=Duchene;mod=0;first-msg=0;subscriber=0;historical=1;emotes=;room-id=62300805;color=#000000;flags=;badges=;id=8e3d59ff-16b1-4463-b60f-e1b8d521b4ae :duchene!duchene@duchene.tmi.twitch.tv PRIVMSG #nymn :\u0001ACTION forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀\u0001","@flags=;subscriber=0;user-type=;badge-info=;historical=1;mod=0;returning-chatter=0;client-nonce=7de6532d5935f8414e0a06afe816e2c8;user-id=51967700;first-msg=0;display-name=Patixxl;room-id=62300805;badges=;rm-received-ts=1704558285130;emotes=;turbo=0;tmi-sent-ts=1704558284931;color=#FF0000;id=e27682df-de09-4927-a66e-aee2785075a8 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@color=#00ED2A;tmi-sent-ts=1704558285027;mod=0;badges=subscriber/42,twitch-recap-2023/1;historical=1;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170;room-id=62300805;user-type=;turbo=0;rm-received-ts=1704558285221;flags=;subscriber=1;client-nonce=8890033439d094fcc1d6136a276a9560;badge-info=subscriber/43;user-id=60181947;returning-chatter=0;display-name=MaxThurian;first-msg=0;id=77c0ef77-c1c1-4cc4-a786-79ffcb8a18b9 :maxthurian!maxthurian@maxthurian.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@returning-chatter=0;subscriber=0;user-type=;turbo=0;badges=bits/1000;historical=1;room-id=62300805;first-msg=0;id=c827a22a-8887-4f16-a2d5-1e3a1a76d05b;tmi-sent-ts=1704558285410;color=#FF0000;mod=0;badge-info=;flags=;display-name=HajleSellasje;user-id=45923155;rm-received-ts=1704558285604;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170,176-186,192-202 :hajlesellasje!hajlesellasje@hajlesellasje.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@client-nonce=a67ad87903178ff9050c8c4ce9cd5f3e;mod=0;user-id=40037186;rm-received-ts=1704558286507;display-name=Kotzblitz20;color=#FFFF00;room-id=62300805;subscriber=1;emotes=;badges=subscriber/9,turbo/1;id=1bea2b0b-38cb-4f31-b10b-7ff89973ae4c;first-msg=0;turbo=1;badge-info=subscriber/9;historical=1;tmi-sent-ts=1704558286332;returning-chatter=0;user-type=;flags= :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn IVEGONEPASTHEPOINTOFINSANITY","@subscriber=1;room-id=62300805;badges=subscriber/36,no_audio/1;badge-info=subscriber/37;id=ccdf49c5-dcdf-44f2-91bc-f86785e63a2e;emotes=;turbo=0;tmi-sent-ts=1704558287890;client-nonce=b7aa180359040f4f00c1a3945156a076;returning-chatter=0;user-type=;flags=;display-name=DontCagePlebs;first-msg=0;historical=1;mod=0;rm-received-ts=1704558288062;user-id=85837900;color=#DAA520 :dontcageplebs!dontcageplebs@dontcageplebs.tmi.twitch.tv PRIVMSG #nymn Aware","@room-id=62300805;id=f50c0981-789a-4edd-822a-9886c0e567b3;historical=1;user-id=45923155;user-type=;rm-received-ts=1704558288700;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,17-27,33-43,49-59,65-75,81-91,97-107,113-123,129-139,145-155,161-171,177-187,193-203;color=#FF0000;subscriber=0;badges=bits/1000;display-name=HajleSellasje;first-msg=0;returning-chatter=0;turbo=0;flags=;badge-info=;tmi-sent-ts=1704558288530;mod=0 :hajlesellasje!hajlesellasje@hajlesellasje.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@badges=subscriber/42,twitch-recap-2023/1;subscriber=1;mod=0;tmi-sent-ts=1704558289794;flags=;rm-received-ts=1704558289966;room-id=62300805;badge-info=subscriber/47;id=29741864-f340-4824-8029-b97c2699289f;returning-chatter=0;color=#FF0000;display-name=Mawsonator;user-id=92529125;turbo=0;first-msg=0;emotes=;user-type=;historical=1;client-nonce=0c5978125e8f24205eec7dff4d1f1c84 :mawsonator!mawsonator@mawsonator.tmi.twitch.tv PRIVMSG #nymn monkaOMEGA","@first-msg=0;badges=;client-nonce=0422710ffd837f0a5408bee46adc954a;subscriber=0;user-type=;room-id=62300805;tmi-sent-ts=1704558289821;badge-info=;id=f7282e2a-c521-4bab-9f24-70d0565e4d67;mod=0;turbo=0;display-name=Patixxl;rm-received-ts=1704558289986;emotes=;color=#FF0000;historical=1;flags=;returning-chatter=0;user-id=51967700 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn IVEGONEPASTHEPOINTOFINSANITY","@badges=;rm-received-ts=1704558290289;color=#FF69B4;emotes=;mod=0;user-type=;display-name=ehtia;returning-chatter=0;id=2078944f-9387-41f9-832e-a15a3bc39ea6;user-id=163155934;historical=1;client-nonce=4e9fbaf8ca8779175041f6474649cfd6;turbo=0;flags=;subscriber=0;badge-info=;first-msg=0;tmi-sent-ts=1704558290112;room-id=62300805 :ehtia!ehtia@ehtia.tmi.twitch.tv PRIVMSG #nymn Stare","@mod=0;subscriber=0;emotes=;tmi-sent-ts=1704558291192;badges=no_audio/1;display-name=sehtt_;client-nonce=6eeb17fc92819d809e121c4816a5723e;room-id=62300805;first-msg=0;historical=1;user-id=133344079;badge-info=;turbo=0;flags=;user-type=;id=d26cc055-2795-465f-9add-6e5058813cf0;returning-chatter=0;color=#5F9EA0;rm-received-ts=1704558291381 :sehtt_!sehtt_@sehtt_.tmi.twitch.tv PRIVMSG #nymn IVEGONEPASTHEPOINTOFINSANITY","@badge-info=subscriber/49;color=#FF2424;returning-chatter=0;subscriber=1;user-type=;mod=0;rm-received-ts=1704558291521;historical=1;first-msg=0;id=eeebe9e4-aedb-41a5-bcb4-2334f747c9df;room-id=62300805;emotes=;badges=subscriber/48,bits/25000;turbo=0;user-id=159210800;display-name=ME_ME;client-nonce=5ebb68c2d7b278d5765d93b450ba3a49;flags=;tmi-sent-ts=1704558291357 :me_me!me_me@me_me.tmi.twitch.tv PRIVMSG #nymn IVEGONEPASTHEPOINTOFINSANITY","@user-id=46199261;emotes=;badge-info=;first-msg=0;mod=0;turbo=0;display-name=Obiwun;tmi-sent-ts=1704558291347;user-type=;flags=;client-nonce=1d75c3b3086ff8f715f69160a3dbba75;id=b503aaff-abbe-4ea1-9759-7cf589a19f47;returning-chatter=0;room-id=62300805;subscriber=0;color=#8A2BE2;historical=1;rm-received-ts=1704558291526;badges=no_audio/1 :obiwun!obiwun@obiwun.tmi.twitch.tv PRIVMSG #nymn monkaGIGA","@display-name=h_h410;rm-received-ts=1704558291721;turbo=0;first-msg=0;badges=subscriber/54,chatter-cs-go-2022/1;user-type=;historical=1;tmi-sent-ts=1704558291548;emotes=;id=8e8f9090-b67e-430d-87cc-c52d429a0a82;user-id=117088592;subscriber=1;badge-info=subscriber/54;room-id=62300805;returning-chatter=0;flags=;color=#00FF7F;mod=0 :h_h410!h_h410@h_h410.tmi.twitch.tv PRIVMSG #nymn :FeelsAmazingMan PianoTime","@display-name=yikeyikers;first-msg=0;subscriber=0;user-type=;emotes=;badge-info=;returning-chatter=0;tmi-sent-ts=1704558292111;user-id=275131292;id=3aaf0bf0-f043-402c-9285-981ba10c7b39;turbo=0;badges=;client-nonce=13557a4951404e7c0f3793035eef17bb;mod=0;historical=1;room-id=62300805;color=#008000;rm-received-ts=1704558292327;flags= :yikeyikers!yikeyikers@yikeyikers.tmi.twitch.tv PRIVMSG #nymn Aware","@user-type=;mod=0;emotes=;user-id=87120320;color=#D52AFF;display-name=Joshlad;turbo=0;badges=vip/1,subscriber/72,rplace-2023/1;badge-info=subscriber/77;room-id=62300805;returning-chatter=0;tmi-sent-ts=1704558292430;vip=1;id=888f1d29-69bf-4c22-b76f-0f53cdc51281;flags=;rm-received-ts=1704558292602;first-msg=0;subscriber=1;historical=1 :joshlad!joshlad@joshlad.tmi.twitch.tv PRIVMSG #nymn monkaS","@subscriber=0;display-name=jonhycrack;flags=;tmi-sent-ts=1704558292872;badges=no_audio/1;first-msg=0;emotes=;rm-received-ts=1704558293040;id=ffe8e1e7-5729-4018-a1c4-85b5278bb831;user-id=431946171;historical=1;returning-chatter=0;badge-info=;color=#008000;room-id=62300805;user-type=;mod=0;turbo=0 :jonhycrack!jonhycrack@jonhycrack.tmi.twitch.tv PRIVMSG #nymn monkaOMEGA","@room-id=62300805;display-name=Patixxl;user-id=51967700;emotes=;tmi-sent-ts=1704558293378;historical=1;rm-received-ts=1704558293553;id=b6efde1c-3a26-4342-92f2-a43f48afa5b6;user-type=;badges=;returning-chatter=0;mod=0;flags=;subscriber=0;first-msg=0;turbo=0;client-nonce=c04b18ddf9106f07ec5f3378ace63b44;badge-info=;color=#FF0000 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn forsenUnpleased","@id=c1e5eedb-58d0-4495-8434-027ed58006db;first-msg=0;display-name=MaxThurian;returning-chatter=0;user-id=60181947;room-id=62300805;user-type=;badges=subscriber/42,twitch-recap-2023/1;badge-info=subscriber/43;color=#00ED2A;mod=0;turbo=0;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170;subscriber=1;flags=;rm-received-ts=1704558293566;tmi-sent-ts=1704558293381;client-nonce=e01c7d240e90975fd7bd49315703c5bb;historical=1 :maxthurian!maxthurian@maxthurian.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@user-id=40037186;subscriber=1;client-nonce=7761274e4fe5a05de70701b933463c5e;tmi-sent-ts=1704558293455;emotes=;badge-info=subscriber/9;mod=0;flags=;historical=1;id=917bd6fc-e240-4af7-a590-32b343771f0e;color=#FFFF00;badges=subscriber/9,turbo/1;returning-chatter=0;display-name=Kotzblitz20;turbo=1;rm-received-ts=1704558293629;room-id=62300805;first-msg=0;user-type= :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn Aware","@room-id=62300805;returning-chatter=0;client-nonce=efa8e9cab5adc49607cbd0aa21c19930;badges=subscriber/6,twitch-recap-2023/1;first-msg=0;color=#84FFD5;display-name=cyan_tide;turbo=0;user-id=904684680;user-type=;rm-received-ts=1704558294607;tmi-sent-ts=1704558294421;id=56560072-f0a0-4ed7-aa19-7999a34a2842;mod=0;subscriber=1;badge-info=subscriber/7;emotes=;flags=;historical=1 :cyan_tide!cyan_tide@cyan_tide.tmi.twitch.tv PRIVMSG #nymn monkaOMEGA","@color=#63BD68;rm-received-ts=1704558295039;display-name=jontEmillian;user-type=;turbo=0;mod=0;id=0efe97f2-3963-4ce7-8936-6e6c5f641f06;first-msg=0;user-id=433352132;subscriber=1;tmi-sent-ts=1704558294860;flags=;returning-chatter=0;badges=subscriber/36,twitch-recap-2023/1;room-id=62300805;emotes=;badge-info=subscriber/38;historical=1 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn Stare","@id=55ee9502-a3b5-4cf2-96e2-42e2fd0efeed;returning-chatter=0;rm-received-ts=1704558295665;mod=0;turbo=0;badge-info=;room-id=62300805;user-type=;flags=;emotes=;display-name=jonhycrack;badges=no_audio/1;subscriber=0;tmi-sent-ts=1704558295489;color=#008000;user-id=431946171;historical=1;first-msg=0 :jonhycrack!jonhycrack@jonhycrack.tmi.twitch.tv PRIVMSG #nymn :monkaOMEGA 󠀀","@historical=1;badges=subscriber/48,bits/25000;client-nonce=a91a93171c48c1e447f56655da6ea2c2;first-msg=0;display-name=ME_ME;id=92476813-f193-42a9-af09-bd069bb17c99;mod=0;tmi-sent-ts=1704558296225;user-id=159210800;emotes=;color=#FF2424;rm-received-ts=1704558296417;turbo=0;returning-chatter=0;room-id=62300805;user-type=;flags=;subscriber=1;badge-info=subscriber/49 :me_me!me_me@me_me.tmi.twitch.tv PRIVMSG #nymn Stare","@turbo=0;subscriber=0;tmi-sent-ts=1704558296714;badge-info=;user-type=;id=d17ef9b3-b88a-445f-8641-9012f4da88be;mod=0;flags=;user-id=998960046;badges=;color=;historical=1;client-nonce=21d36f0dbad51fe1629b14e90031b3b0;emotes=;returning-chatter=0;first-msg=0;rm-received-ts=1704558296891;display-name=jross1812;room-id=62300805 :jross1812!jross1812@jross1812.tmi.twitch.tv PRIVMSG #nymn Aware","@room-id=62300805;first-msg=0;color=#DAA520;id=6d673376-2c0d-4e1b-9c31-7475785effd5;user-type=;mod=0;badge-info=subscriber/37;rm-received-ts=1704558297057;historical=1;emotes=;flags=;returning-chatter=0;display-name=DontCagePlebs;user-id=85837900;tmi-sent-ts=1704558296881;turbo=0;badges=subscriber/36,no_audio/1;subscriber=1;client-nonce=a9fdf132331119198b31828bf73b3cfa :dontcageplebs!dontcageplebs@dontcageplebs.tmi.twitch.tv PRIVMSG #nymn Stare","@id=8eb422de-c2ed-437d-b020-60d6b91aac0a;mod=0;display-name=Empathos;turbo=0;badges=twitch-recap-2023/1;rm-received-ts=1704558297057;tmi-sent-ts=1704558296861;room-id=62300805;emotes=;client-nonce=db21bfdd170b6b9f6bc5dac6b04fcc8e;subscriber=0;user-type=;historical=1;color=#8A2BE2;first-msg=0;badge-info=;user-id=31363069;returning-chatter=0;flags= :empathos!empathos@empathos.tmi.twitch.tv PRIVMSG #nymn NOOO","@subscriber=1;returning-chatter=0;rm-received-ts=1704558297988;display-name=Kotzblitz20;room-id=62300805;first-msg=0;client-nonce=c94e87a98d83dc6ff369fbe7a9c2a44b;badge-info=subscriber/9;tmi-sent-ts=1704558297816;user-id=40037186;flags=;turbo=1;id=13000419-899c-44e7-b0df-856204c088af;emotes=;historical=1;user-type=;color=#FFFF00;mod=0;badges=subscriber/9,turbo/1 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn monkaOMEGA","@color=#FF2424;historical=1;user-id=159210800;emotes=;display-name=ME_ME;mod=0;badges=subscriber/48,bits/25000;room-id=62300805;client-nonce=bf2fecdca610aa30899f6af7aea85b08;rm-received-ts=1704558297993;user-type=;id=b84cc141-95a6-484a-8e0f-02687032dad0;returning-chatter=0;tmi-sent-ts=1704558297826;badge-info=subscriber/49;turbo=0;flags=;first-msg=0;subscriber=1 :me_me!me_me@me_me.tmi.twitch.tv PRIVMSG #nymn IVEGONEPASTHEPOINTOFINSANITY","@returning-chatter=0;turbo=0;id=1eae99a1-2811-45b3-a139-e38cc5e24c13;mod=0;historical=1;user-id=63372784;user-type=;tmi-sent-ts=1704558298206;badges=bits/100;subscriber=0;emotes=;display-name=DM8917;rm-received-ts=1704558298383;client-nonce=7fcb46bb8e5520ced438c68ac9095fbc;room-id=62300805;badge-info=;flags=;first-msg=0;color=#25E000 :dm8917!dm8917@dm8917.tmi.twitch.tv PRIVMSG #nymn :IVEGONEPASTHEPOINTOFINSANITY 󠀀","@subscriber=1;id=3e4e2c27-b273-4ff2-b08f-3ed646bbd656;emotes=;flags=;historical=1;turbo=0;room-id=62300805;user-type=;color=#1E90FF;display-name=mnqn18;returning-chatter=0;user-id=474204887;badges=subscriber/0,premium/1;tmi-sent-ts=1704558298344;client-nonce=445cba1e1156526dbd0c1c0c2040a492;badge-info=subscriber/2;first-msg=0;rm-received-ts=1704558298516;mod=0 :mnqn18!mnqn18@mnqn18.tmi.twitch.tv PRIVMSG #nymn monkaOMEGA","@mod=0;returning-chatter=0;emotes=;room-id=62300805;user-type=;tmi-sent-ts=1704558298525;flags=;display-name=jontEmillian;badges=subscriber/36,twitch-recap-2023/1;rm-received-ts=1704558298691;id=05a914b9-5bc5-4ddd-a3a5-089e8dd6b0dd;first-msg=0;color=#63BD68;user-id=433352132;turbo=0;badge-info=subscriber/38;subscriber=1;historical=1 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn ClueLookingAtYou","@emotes=;id=3bd0a24d-c75a-4c0a-b877-84beea8a12b5;display-name=Intel_power;room-id=62300805;tmi-sent-ts=1704558298936;color=#0000FF;badges=bits-charity/1;turbo=0;rm-received-ts=1704558299115;mod=0;user-id=103665668;subscriber=0;returning-chatter=0;badge-info=;historical=1;client-nonce=507cdafd820a3e86ba03357f6b6e82bb;first-msg=0;flags=;user-type= :intel_power!intel_power@intel_power.tmi.twitch.tv PRIVMSG #nymn Stare","@returning-chatter=0;room-id=62300805;rm-received-ts=1704558300570;color=#FF0000;flags=;mod=0;tmi-sent-ts=1704558300403;badges=;historical=1;turbo=0;display-name=Patixxl;badge-info=;client-nonce=82869a08183f7970ca50c81111420a97;user-id=51967700;emotes=;user-type=;subscriber=0;first-msg=0;id=85e15786-c65d-47aa-a4ab-ba47627d0ebc :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn IVEGONEPASTHEPOINTOFINSANITY","@returning-chatter=0;display-name=DontCagePlebs;mod=0;color=#DAA520;badge-info=subscriber/37;historical=1;rm-received-ts=1704558300805;emotes=;turbo=0;user-type=;user-id=85837900;client-nonce=69f422140f84b5e367a6aed2bba6548f;badges=subscriber/36,no_audio/1;tmi-sent-ts=1704558300634;subscriber=1;flags=;id=0cdb6d1f-fe0a-4227-87de-1c78218a0a31;first-msg=0;room-id=62300805 :dontcageplebs!dontcageplebs@dontcageplebs.tmi.twitch.tv PRIVMSG #nymn IVEGONEPASTHEPOINTOFINSANITY","@emotes=;subscriber=1;tmi-sent-ts=1704558300698;room-id=62300805;badges=subscriber/42,twitch-recap-2023/1;rm-received-ts=1704558300883;historical=1;mod=0;display-name=MaxThurian;color=#00ED2A;turbo=0;id=fa7e4ae8-bf54-49c8-af4f-87a65a06467d;client-nonce=435ff1d3b746bfeddeac3a8a28d8c0bc;returning-chatter=0;user-id=60181947;first-msg=0;flags=;badge-info=subscriber/43;user-type= :maxthurian!maxthurian@maxthurian.tmi.twitch.tv PRIVMSG #nymn :monkaOMEGA ForsenLookingAtYou","@client-nonce=a1a49fc74c1945e1fc38de3f21d4204b;user-type=;subscriber=1;id=055d85b4-8e13-486d-8535-e8ff6daecdba;room-id=62300805;tmi-sent-ts=1704558300749;flags=;badge-info=subscriber/9;emotes=;historical=1;turbo=1;mod=0;rm-received-ts=1704558300930;badges=subscriber/9,turbo/1;display-name=Kotzblitz20;color=#FFFF00;returning-chatter=0;first-msg=0;user-id=40037186 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :monkaOMEGA monkaOMEGA monkaOMEGA monkaOMEGA monkaOMEGA","@display-name=deever44;first-msg=0;turbo=0;color=;tmi-sent-ts=1704558301450;id=e35d6b0a-79e5-49e9-8dfc-299342b5b1c2;rm-received-ts=1704558301636;user-type=;user-id=37931493;badges=no_audio/1;historical=1;subscriber=0;returning-chatter=0;flags=;room-id=62300805;mod=0;emotes=;client-nonce=6f19b16eee728b787d4b2fe66e8e2274;badge-info= :deever44!deever44@deever44.tmi.twitch.tv PRIVMSG #nymn Stare","@rm-received-ts=1704558301861;display-name=123homo;badges=;returning-chatter=0;user-type=;mod=0;historical=1;badge-info=;color=#FF0000;id=71b0752b-770e-465d-9157-10577decf8c1;subscriber=0;room-id=62300805;flags=;tmi-sent-ts=1704558301692;user-id=133862911;client-nonce=86a6d1d82507591a80e3ff7285e2ac01;turbo=0;first-msg=0;emotes= :123homo!123homo@123homo.tmi.twitch.tv PRIVMSG #nymn :I LOVE APOLLO","@room-id=62300805;tmi-sent-ts=1704558301748;flags=;turbo=0;first-msg=0;returning-chatter=0;badge-info=;user-type=;badges=;emotes=115234:0-7;mod=0;user-id=275131292;id=285f3ad9-caff-40b6-8c34-a3cf34865749;subscriber=0;historical=1;rm-received-ts=1704558301957;client-nonce=e1828eee7a36bd14d38db1e79ad9fd4a;display-name=yikeyikers;color=#008000 :yikeyikers!yikeyikers@yikeyikers.tmi.twitch.tv PRIVMSG #nymn :BatChest AI","@emotes=;turbo=0;client-nonce=959fe0ca83a36a5b38b020ab9cf32958;color=#FF2424;first-msg=0;mod=0;badges=subscriber/48,bits/25000;historical=1;id=b17c2c76-a71f-4c28-a9d0-f2338434d1cb;room-id=62300805;user-id=159210800;flags=;tmi-sent-ts=1704558302194;returning-chatter=0;user-type=;subscriber=1;rm-received-ts=1704558302354;display-name=ME_ME;badge-info=subscriber/49 :me_me!me_me@me_me.tmi.twitch.tv PRIVMSG #nymn :IVEGONEPASTHEPOINTOFINSANITY 󠀀","@id=37c73d2c-c840-4879-b070-f10819de7320;mod=0;display-name=jontEmillian;tmi-sent-ts=1704558302391;flags=;badge-info=subscriber/38;emotes=;user-type=;subscriber=1;badges=subscriber/36,twitch-recap-2023/1;user-id=433352132;first-msg=0;room-id=62300805;color=#63BD68;rm-received-ts=1704558302545;turbo=0;historical=1;returning-chatter=0 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn :ClueLookingAtYou 󠀀","@client-nonce=6615fab12a3cbea682b6ef20b13e72b7;historical=1;turbo=0;rm-received-ts=1704558302651;returning-chatter=0;badges=twitch-recap-2023/1;color=#10E2E2;room-id=62300805;flags=;display-name=ALotOfChickens;tmi-sent-ts=1704558302464;id=c5ca4fbb-fd43-4a78-b3eb-65831de43a75;badge-info=;user-type=;first-msg=0;mod=0;emotes=;subscriber=0;user-id=167633177 :alotofchickens!alotofchickens@alotofchickens.tmi.twitch.tv PRIVMSG #nymn :pepeLaugh TeaTime","@emote-only=1;badge-info=subscriber/77;user-id=87120320;tmi-sent-ts=1704558302616;id=78c456ac-d05d-4d70-8c24-c5074bc703d1;flags=;historical=1;emotes=emotesv2_662cb46b3efe41b4ada2d6560fd06cac:0-17;first-msg=0;user-type=;rm-received-ts=1704558302788;badges=vip/1,subscriber/72,rplace-2023/1;returning-chatter=0;mod=0;display-name=Joshlad;turbo=0;subscriber=1;vip=1;room-id=62300805;color=#D52AFF :joshlad!joshlad@joshlad.tmi.twitch.tv PRIVMSG #nymn suntgiLookingAtYou","@mod=0;color=#8A2BE2;returning-chatter=0;tmi-sent-ts=1704558302753;badges=no_audio/1;room-id=62300805;user-type=;client-nonce=ea47ae3d799e07d8d2e09be2cebf8627;display-name=Obiwun;emotes=;turbo=0;historical=1;id=812a5797-9f6f-41c1-842d-94085d4dc6b1;flags=;first-msg=0;subscriber=0;badge-info=;rm-received-ts=1704558302932;user-id=46199261 :obiwun!obiwun@obiwun.tmi.twitch.tv PRIVMSG #nymn ForsenLookingAtYou","@emotes=;room-id=62300805;display-name=jonhycrack;historical=1;first-msg=0;color=#008000;flags=;rm-received-ts=1704558303454;user-id=431946171;tmi-sent-ts=1704558303273;badge-info=;returning-chatter=0;turbo=0;id=efd2f0d9-e5d3-4029-b79b-62851010b08a;badges=no_audio/1;subscriber=0;mod=0;user-type= :jonhycrack!jonhycrack@jonhycrack.tmi.twitch.tv PRIVMSG #nymn :monkaOMEGA BEHIND THE GREEN CURTAIN","@id=1a6308fa-c20e-431b-95ed-6e68a178c8c1;room-id=62300805;rm-received-ts=1704558303622;tmi-sent-ts=1704558303150;color=#00FF7F;client-nonce=68c270ed92b9ca46390e580bf13b7b36;user-id=83365099;emotes=;subscriber=0;historical=1;first-msg=0;returning-chatter=0;flags=0-12:P.3;turbo=0;badge-info=;badges=glitchcon2020/1;display-name=OmniValor;user-type=;mod=0 :omnivalor!omnivalor@omnivalor.tmi.twitch.tv PRIVMSG #nymn wtfffffffffff","@rm-received-ts=1704558303980;subscriber=1;id=1272038c-5fb9-47e9-a12e-259d030aa9c9;user-type=;display-name=Kotzblitz20;color=#FFFF00;user-id=40037186;first-msg=0;flags=;historical=1;mod=0;client-nonce=6e7880bb87efd70ace3c0200cf46475f;emotes=;badges=subscriber/9,turbo/1;turbo=1;returning-chatter=0;tmi-sent-ts=1704558303803;badge-info=subscriber/9;room-id=62300805 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn IVEGONEPASTHEPOINTOFINSANITY","@flags=;turbo=0;returning-chatter=0;user-id=431946171;tmi-sent-ts=1704558304774;badges=no_audio/1;color=#008000;badge-info=;subscriber=0;first-msg=0;mod=0;user-type=;id=aac65138-2265-42cc-800e-9e27ae53171b;display-name=jonhycrack;room-id=62300805;rm-received-ts=1704558304934;emotes=;historical=1 :jonhycrack!jonhycrack@jonhycrack.tmi.twitch.tv PRIVMSG #nymn :monkaOMEGA BEHIND THE GREEN CURTAIN","@color=#FF0000;turbo=0;user-id=133862911;client-nonce=3afd83cd50d36060a9387aee58e137b4;mod=0;rm-received-ts=1704558304965;emotes=;badge-info=;historical=1;room-id=62300805;subscriber=0;returning-chatter=0;flags=;badges=;id=5a3f2b89-056e-4114-995d-8570aee1dee3;user-type=;display-name=123homo;first-msg=0;tmi-sent-ts=1704558304798 :123homo!123homo@123homo.tmi.twitch.tv PRIVMSG #nymn :I HATE AI","@badge-info=subscriber/41;client-nonce=6252e0206b6474e8baf80fbcfd933f44;turbo=0;id=873d1ef6-617d-47ad-b657-3883259ce8f6;user-type=;first-msg=0;user-id=154079285;rm-received-ts=1704558305162;tmi-sent-ts=1704558304973;historical=1;room-id=62300805;emotes=;flags=;badges=subscriber/36;returning-chatter=0;mod=0;color=#00FF7F;subscriber=1;display-name=boogkitty :boogkitty!boogkitty@boogkitty.tmi.twitch.tv PRIVMSG #nymn :yes its randomised Nymn","@id=e4991da4-a52d-49cd-a82a-e20b84751423;historical=1;badges=;room-id=62300805;turbo=0;client-nonce=b9b1635fef4548fe66149bdeeb7f5c26;display-name=Patixxl;user-type=;subscriber=0;color=#FF0000;rm-received-ts=1704558305341;first-msg=0;tmi-sent-ts=1704558305183;mod=0;user-id=51967700;badge-info=;returning-chatter=0;flags=;emotes= :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :IVEGONEPASTHEPOINTOFINSANITY 󠀀","@turbo=0;mod=0;subscriber=1;rm-received-ts=1704558305350;badges=subscriber/9,chatter-cs-go-2022/1;display-name=Phant0mBlades;tmi-sent-ts=1704558305175;first-msg=0;color=#008000;id=efb0af5d-3b59-49ec-b3cf-dce5c7386214;returning-chatter=0;room-id=62300805;emotes=;flags=;historical=1;user-id=278896263;badge-info=subscriber/9;user-type= :phant0mblades!phant0mblades@phant0mblades.tmi.twitch.tv PRIVMSG #nymn IVEGONEPASTHEPOINTOFINSANITY","@rm-received-ts=1704558306205;display-name=jontEmillian;tmi-sent-ts=1704558306034;flags=;subscriber=1;returning-chatter=0;mod=0;badges=subscriber/36,twitch-recap-2023/1;first-msg=0;turbo=0;room-id=62300805;emotes=;user-type=;historical=1;badge-info=subscriber/38;color=#63BD68;id=bde0c098-922c-455d-a23a-bddc8b82b2eb;user-id=433352132 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn ClueLookingAtYou","@id=f9ae2b7d-29ce-415a-8c39-9e2b1ec71b93;room-id=62300805;badges=no_audio/1;color=#008000;emotes=;display-name=jonhycrack;flags=;first-msg=0;mod=0;rm-received-ts=1704558306589;returning-chatter=0;user-type=;historical=1;user-id=431946171;turbo=0;badge-info=;subscriber=0;tmi-sent-ts=1704558306408 :jonhycrack!jonhycrack@jonhycrack.tmi.twitch.tv PRIVMSG #nymn :monkaOMEGA BEHIND THE GREEN CURTAIN","@badges=subscriber/9,turbo/1;flags=;user-type=;badge-info=subscriber/9;rm-received-ts=1704558307542;subscriber=1;mod=0;turbo=1;room-id=62300805;returning-chatter=0;display-name=Kotzblitz20;id=7b3f8887-f3f0-4d28-b6ef-0254600d81e8;first-msg=0;tmi-sent-ts=1704558307350;historical=1;color=#FFFF00;emotes=;client-nonce=29567dd99ba1a611f2a3ae29636df9bf;user-id=40037186 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :IVEGONEPASTHEPOINTOFINSANITY IVEGONEPASTHEPOINTOFINSANITY IVEGONEPASTHEPOINTOFINSANITY IVEGONEPASTHEPOINTOFINSANITY IVEGONEPASTHEPOINTOFINSANITY","@user-id=222340799;turbo=0;badge-info=subscriber/7;rm-received-ts=1704558308109;room-id=62300805;color=#B22222;first-msg=0;tmi-sent-ts=1704558307914;flags=;emotes=emotesv2_e16d73fce3b840949d5474dfaca63ffd:12-22;subscriber=1;id=11c9190b-9a7b-4374-a5cb-16570adbca42;display-name=crazyjuni0r_;client-nonce=e68bbaedc41ee337e322b1a65d572745;user-type=;badges=subscriber/6,chatter-cs-go-2022/1;returning-chatter=0;mod=0;historical=1 :crazyjuni0r_!crazyjuni0r_@crazyjuni0r_.tmi.twitch.tv PRIVMSG #nymn :!#showemote spacea32HOM","@mod=0;badges=subscriber/42,twitch-recap-2023/1;returning-chatter=0;first-msg=0;flags=;turbo=0;subscriber=1;display-name=MaxThurian;client-nonce=5db1e38800091cd2bc5c0bc0b9ef418e;user-type=;emotes=;badge-info=subscriber/43;room-id=62300805;id=0241e30f-9407-4e93-a93a-d908c3dbccf1;user-id=60181947;rm-received-ts=1704558308214;historical=1;color=#00ED2A;tmi-sent-ts=1704558308025 :maxthurian!maxthurian@maxthurian.tmi.twitch.tv PRIVMSG #nymn :monkaOMEGA ForsenLookingAtYou 󠀀","@subscriber=1;historical=1;color=#8A2BE2;tmi-sent-ts=1704558308187;mod=0;returning-chatter=0;room-id=62300805;user-id=137782780;first-msg=0;user-type=;id=0f1f844f-8f10-4b8a-b4f5-af4645f28c06;turbo=0;badge-info=subscriber/4;flags=;client-nonce=97081ba776de2e36895e70769e3622e8;badges=subscriber/3,no_audio/1;rm-received-ts=1704558308541;emotes=;display-name=pleasekeepconnor6silly :pleasekeepconnor6silly!pleasekeepconnor6silly@pleasekeepconnor6silly.tmi.twitch.tv PRIVMSG #nymn IVEGONEPASTHEPOINTOFINSANITY","@badge-info=;room-id=62300805;user-type=;color=#008000;badges=no_audio/1;first-msg=0;emotes=;turbo=0;tmi-sent-ts=1704558308427;historical=1;mod=0;rm-received-ts=1704558308612;flags=;returning-chatter=0;id=9863f240-b6c6-414e-9a17-a47cc0905324;subscriber=0;display-name=jonhycrack;user-id=431946171 :jonhycrack!jonhycrack@jonhycrack.tmi.twitch.tv PRIVMSG #nymn :monkaOMEGA BEHIND THE GREEN CURTAIN","@turbo=0;id=e6929219-ce61-4295-bb6a-88b70ae4fa32;room-id=62300805;client-nonce=f1ba29c814860616774514a82ec5fa67;emotes=;returning-chatter=0;subscriber=1;badge-info=subscriber/49;mod=0;tmi-sent-ts=1704558309446;rm-received-ts=1704558309629;badges=subscriber/48,bits/25000;historical=1;display-name=ME_ME;color=#FF2424;flags=;user-id=159210800;first-msg=0;user-type= :me_me!me_me@me_me.tmi.twitch.tv PRIVMSG #nymn IVEGONEPASTHEPOINTOFINSANITY","@display-name=Patixxl;mod=0;user-id=51967700;flags=;color=#FF0000;id=deaff6db-6df2-40a6-9a48-8e88bcdbc9ec;subscriber=0;returning-chatter=0;client-nonce=4f22dd1d098b7ba2775f156496b3808f;user-type=;tmi-sent-ts=1704558309939;turbo=0;badges=;historical=1;first-msg=0;emotes=;rm-received-ts=1704558310113;room-id=62300805;badge-info= :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn IVEGONEPASTHEPOINTOFINSANITY","@user-id=28317294;display-name=ImDaxify;vip=1;flags=;badges=vip/1,subscriber/36,twitch-recap-2023/1;room-id=62300805;turbo=0;first-msg=0;returning-chatter=0;client-nonce=4c706ed3fe3fef6bd3dabce0f62a4d74;tmi-sent-ts=1704558310694;mod=0;badge-info=subscriber/36;user-type=;id=cd749df0-6847-4cb9-b64a-c347746661a5;color=#1F8FFF;emotes=;rm-received-ts=1704558310867;historical=1;subscriber=1 :imdaxify!imdaxify@imdaxify.tmi.twitch.tv PRIVMSG #nymn :ai generated game? @NymN","@room-id=62300805;first-msg=0;color=#63BD68;historical=1;rm-received-ts=1704558310930;tmi-sent-ts=1704558310766;emotes=;flags=;mod=0;badge-info=subscriber/38;user-id=433352132;badges=subscriber/36,twitch-recap-2023/1;turbo=0;id=8668e86b-2962-4524-b130-63b1b36e30c8;subscriber=1;display-name=jontEmillian;user-type=;returning-chatter=0 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn :ClueLookingAtYou 󠀀","@emotes=;badge-info=;badges=bits/100;subscriber=0;rm-received-ts=1704558312125;historical=1;user-id=63372784;user-type=;color=#25E000;room-id=62300805;id=c3857edd-89b0-4f4a-a829-73c3ce64ca40;client-nonce=cd54cf76802ccc9de6f676191e8a7261;display-name=DM8917;flags=;returning-chatter=0;tmi-sent-ts=1704558311940;first-msg=0;turbo=0;mod=0 :dm8917!dm8917@dm8917.tmi.twitch.tv PRIVMSG #nymn :IVEGONEPASTHEPOINTOFINSANITY IVEGONEPASTHEPOINTOFINSANITY","@tmi-sent-ts=1704558314190;room-id=62300805;display-name=Dankarop;mod=0;subscriber=0;color=#00FF7F;rm-received-ts=1704558314391;returning-chatter=0;user-type=;flags=5-7:P.3;emotes=;badge-info=;turbo=0;first-msg=0;id=eb7f11b7-dfd4-454d-a496-2ee41c2d84fb;client-nonce=18dcf3f8e3e26de01694b5b946569400;user-id=467106798;badges=rplace-2023/1;historical=1 :dankarop!dankarop@dankarop.tmi.twitch.tv PRIVMSG #nymn :monk ass BillyApprove","@display-name=miniwoffer;id=96a0d440-6225-4348-8533-09fc5bae3462;tmi-sent-ts=1704558314381;first-msg=0;user-type=;mod=0;color=#FF69B4;room-id=62300805;turbo=0;user-id=22733078;client-nonce=1ef4e4cf50663e8a47750ab6f0cf7e87;badges=subscriber/0,bits/1;badge-info=subscriber/2;subscriber=1;returning-chatter=0;emotes=;flags=;rm-received-ts=1704558314554;historical=1 :miniwoffer!miniwoffer@miniwoffer.tmi.twitch.tv PRIVMSG #nymn monkaS","@room-id=62300805;badges=subscriber/9,turbo/1;badge-info=subscriber/9;user-type=;display-name=Kotzblitz20;emotes=;historical=1;returning-chatter=0;flags=;client-nonce=5a8238db251becbbc27ef83899d9dafa;color=#FFFF00;user-id=40037186;rm-received-ts=1704558315337;turbo=1;subscriber=1;mod=0;tmi-sent-ts=1704558315167;first-msg=0;id=89b072ac-212a-4a7c-a4be-468b64ddd10f :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn monkaOMEGA","@color=#00FF7F;badge-info=;display-name=Dankarop;subscriber=0;first-msg=0;client-nonce=21261092ada5f0db291f91b83cc30025;id=9026a45e-a7fc-428d-b01e-7b9ee280fd38;flags=5-7:P.3;returning-chatter=0;emotes=;turbo=0;tmi-sent-ts=1704558315236;mod=0;badges=rplace-2023/1;user-id=467106798;historical=1;rm-received-ts=1704558315402;room-id=62300805;user-type= :dankarop!dankarop@dankarop.tmi.twitch.tv PRIVMSG #nymn :monk ass BillyApprove 󠀀","@user-type=;emotes=;badge-info=;returning-chatter=0;id=dbf58bc9-85c1-428b-b498-f9600e16c88a;mod=0;client-nonce=2054333e43b9720c33e2db1edc90bac3;display-name=Bonfiredes;first-msg=0;color=#A81C00;subscriber=0;flags=;badges=gold-pixel-heart/1;rm-received-ts=1704558315436;tmi-sent-ts=1704558315264;room-id=62300805;user-id=44312943;turbo=0;historical=1 :bonfiredes!bonfiredes@bonfiredes.tmi.twitch.tv PRIVMSG #nymn yes","@tmi-sent-ts=1704558315706;emotes=;color=#D2691E;id=0cec7379-5b38-409c-8eaf-9df82c4bd924;mod=0;display-name=Binfz;flags=;user-id=116793607;rm-received-ts=1704558315890;client-nonce=6ec93cc31adcde7767b6f6de2b07c556;badges=;turbo=0;returning-chatter=0;subscriber=0;room-id=62300805;badge-info=;first-msg=0;user-type=;historical=1 :binfz!binfz@binfz.tmi.twitch.tv PRIVMSG #nymn :AI GENERATED GAME LULW","@id=b3a2e2c4-8b37-4c85-9a35-dca479b2e963;badge-info=;rm-received-ts=1704558316431;user-id=467106798;color=#00FF7F;badges=rplace-2023/1;subscriber=0;returning-chatter=0;emotes=;mod=0;flags=5-7:P.3;room-id=62300805;historical=1;display-name=Dankarop;tmi-sent-ts=1704558316234;client-nonce=3b93fcb34e3f672c9d3dcca9f6dce9a9;first-msg=0;turbo=0;user-type= :dankarop!dankarop@dankarop.tmi.twitch.tv PRIVMSG #nymn :monk ass BillyApprove","@client-nonce=027f4619e7ccf6e01c79e3f3379ff3ae;badges=;id=335c87cd-3018-4be3-af9c-f04bc324d9dd;returning-chatter=0;display-name=Patixxl;turbo=0;emotes=;historical=1;subscriber=0;color=#FF0000;first-msg=0;flags=;user-type=;room-id=62300805;tmi-sent-ts=1704558316436;mod=0;badge-info=;rm-received-ts=1704558316592;user-id=51967700 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :IVEGONEPASTHEPOINTOFINSANITY 󠀀","@color=#008000;badges=twitch-recap-2023/1;emotes=;historical=1;first-msg=0;user-id=11654373;rm-received-ts=1704558316905;id=8f916376-5f46-422e-a1a6-c1c3617fe961;flags=;room-id=62300805;badge-info=;display-name=bovabova;tmi-sent-ts=1704558316719;returning-chatter=0;mod=0;client-nonce=60b377c83abc5cfd38c9005242b3989c;subscriber=0;user-type=;turbo=0 :bovabova!bovabova@bovabova.tmi.twitch.tv PRIVMSG #nymn :i love you nymn","@historical=1;badges=subscriber/9,turbo/1;returning-chatter=0;flags=;display-name=Kotzblitz20;turbo=1;rm-received-ts=1704558317627;user-type=;user-id=40037186;id=7199d335-d925-4278-8762-8486683ce9e4;mod=0;badge-info=subscriber/9;tmi-sent-ts=1704558317433;subscriber=1;first-msg=0;client-nonce=c7e654f8ff49f0f308c88b12d3130c4d;room-id=62300805;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74;color=#FFFF00 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@turbo=0;client-nonce=8efb2c564cdfd666b8b80b4d7cdb1dc1;id=6746ff61-6e78-4aac-8164-2f7ec4426a94;rm-received-ts=1704558317705;subscriber=0;mod=0;first-msg=0;badges=no_audio/1;emotes=;user-type=;returning-chatter=0;display-name=deever44;tmi-sent-ts=1704558317538;user-id=37931493;color=;badge-info=;room-id=62300805;historical=1;flags= :deever44!deever44@deever44.tmi.twitch.tv PRIVMSG #nymn yes","@room-id=62300805;returning-chatter=0;client-nonce=158ac9e8ca2a0a794338b2039c346637;tmi-sent-ts=1704558318322;color=#FF0000;emotes=;rm-received-ts=1704558318499;display-name=Patixxl;flags=;user-id=51967700;mod=0;user-type=;turbo=0;badges=;id=97e1ea95-2145-47f1-935c-ce8797d9902f;subscriber=0;badge-info=;historical=1;first-msg=0 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@badge-info=;turbo=0;client-nonce=8ecd8512e8860b6af6ae2a0058b51a7b;mod=0;color=#FF0000;returning-chatter=0;first-msg=0;historical=1;id=f3fe105f-d6b4-43d5-8859-a61b2fe090dc;user-type=;subscriber=0;tmi-sent-ts=1704558319513;flags=;room-id=62300805;user-id=86965943;emotes=;badges=premium/1;display-name=voyu1337;rm-received-ts=1704558319718 :voyu1337!voyu1337@voyu1337.tmi.twitch.tv PRIVMSG #nymn :WAYTOODANK WAYTOODANK WAYTOODANK WAYTOODANK WAYTOODANK WAYTOODANK WAYTOODANK WAYTOODANK WAYTOODANK WAYTOODANK WAYTOODANK WAYTOODANK WAYTOODANK WAYTOODANK WAYTOODANK WAYTOODANK WAYTOODANK WAYTOODANK WAYTOODANK WAYTOODANK WAYTOODANK","@first-msg=0;id=3ba84985-6097-4429-8b78-6af4b09afdbb;color=#D52AFF;rm-received-ts=1704558319749;room-id=62300805;historical=1;flags=;emotes=;subscriber=1;mod=0;turbo=0;tmi-sent-ts=1704558319556;user-type=;badges=vip/1,subscriber/72,rplace-2023/1;display-name=Joshlad;vip=1;user-id=87120320;returning-chatter=0;badge-info=subscriber/77 :joshlad!joshlad@joshlad.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@flags=;client-nonce=eb6645ae1c43a2aeb59278d81bfb2f85;user-id=64153307;badge-info=;id=24278ced-396d-48d0-8fbb-273c8e46d4f3;turbo=0;room-id=62300805;display-name=ZormiK;tmi-sent-ts=1704558320714;first-msg=0;rm-received-ts=1704558320914;emotes=;subscriber=0;mod=0;user-type=;badges=glitchcon2020/1;returning-chatter=0;color=#FF4500;historical=1 :zormik!zormik@zormik.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@tmi-sent-ts=1704558320918;badge-info=subscriber/43;flags=;first-msg=0;user-id=60181947;returning-chatter=0;rm-received-ts=1704558321090;emotes=;mod=0;display-name=MaxThurian;badges=subscriber/42,twitch-recap-2023/1;client-nonce=28215166443431512c53d815f3f47c3f;user-type=;id=7fcaf95b-1797-4ac5-9e98-333fe2b442f7;historical=1;room-id=62300805;color=#00ED2A;subscriber=1;turbo=0 :maxthurian!maxthurian@maxthurian.tmi.twitch.tv PRIVMSG #nymn WAYTOODANK","@tmi-sent-ts=1704558321101;mod=0;flags=;first-msg=0;display-name=DontCagePlebs;room-id=62300805;user-id=85837900;badge-info=subscriber/37;emotes=;client-nonce=05b0e54fdec0c1c4c039cf3256fd2161;badges=subscriber/36,no_audio/1;id=970e2afe-ee81-404f-ad4d-0455dede5194;historical=1;returning-chatter=0;turbo=0;subscriber=1;rm-received-ts=1704558321269;user-type=;color=#DAA520 :dontcageplebs!dontcageplebs@dontcageplebs.tmi.twitch.tv PRIVMSG #nymn WAYTOOBUH","@badges=subscriber/36,twitch-recap-2023/1;badge-info=subscriber/38;room-id=62300805;user-type=;rm-received-ts=1704558321374;first-msg=0;id=79cc43b9-c7ef-4959-8610-e6cb2d6af6f4;user-id=433352132;color=#63BD68;historical=1;flags=;turbo=0;display-name=jontEmillian;subscriber=1;mod=0;tmi-sent-ts=1704558321184;returning-chatter=0;emotes= :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn forsenParty","@room-id=62300805;mod=0;badge-info=subscriber/9;user-id=40037186;subscriber=1;id=82d847df-5e55-45ae-9b25-7b6618171ad9;client-nonce=1be7581e194abde88d6fc68763c6ed4d;first-msg=0;badges=subscriber/9,turbo/1;tmi-sent-ts=1704558321206;user-type=;rm-received-ts=1704558321387;flags=;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106;display-name=Kotzblitz20;returning-chatter=0;color=#FFFF00;historical=1;turbo=1 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@flags=;rm-received-ts=1704558322012;client-nonce=f5d6339f25a02b9a89daa738ce35b072;tmi-sent-ts=1704558321836;badge-info=;mod=0;emotes=;returning-chatter=0;turbo=0;user-type=;display-name=Patixxl;color=#FF0000;subscriber=0;room-id=62300805;user-id=51967700;id=12fd4a70-a7bc-465a-a484-72243726ea6e;historical=1;badges=;first-msg=0 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@returning-chatter=0;historical=1;display-name=123homo;reply-thread-parent-msg-id=cd749df0-6847-4cb9-b64a-c347746661a5;client-nonce=87d704664a3b935d00dfb5bffeb50ef8;reply-thread-parent-user-id=28317294;emotes=;user-type=;badges=;subscriber=0;reply-parent-user-id=28317294;badge-info=;reply-parent-display-name=ImDaxify;color=#FF0000;reply-parent-user-login=imdaxify;room-id=62300805;user-id=133862911;reply-parent-msg-id=cd749df0-6847-4cb9-b64a-c347746661a5;reply-parent-msg-body=ai\\sgenerated\\sgame?\\s@NymN;id=7d198fbe-6da9-47a4-8f5a-2a43e1c4f7fc;turbo=0;mod=0;reply-thread-parent-display-name=ImDaxify;first-msg=0;reply-thread-parent-user-login=imdaxify;flags=;rm-received-ts=1704558322863;tmi-sent-ts=1704558322694 :123homo!123homo@123homo.tmi.twitch.tv PRIVMSG #nymn :@ImDaxify NOOOOOOOOOOO","@id=7dac020c-1d98-42fa-839c-6c4bae3247d2;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10;flags=;returning-chatter=0;badge-info=subscriber/9;badges=subscriber/9,chatter-cs-go-2022/1;display-name=Phant0mBlades;user-type=;subscriber=1;room-id=62300805;mod=0;user-id=278896263;historical=1;rm-received-ts=1704558323183;tmi-sent-ts=1704558323001;color=#008000;turbo=0;first-msg=0 :phant0mblades!phant0mblades@phant0mblades.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM","@display-name=Kotzblitz20;user-type=;turbo=1;flags=;returning-chatter=0;tmi-sent-ts=1704558323184;badges=subscriber/9,turbo/1;mod=0;color=#FFFF00;id=ebfcef5f-564b-4afd-9bd5-71b7c9251919;subscriber=1;user-id=40037186;historical=1;rm-received-ts=1704558323349;badge-info=subscriber/9;client-nonce=fb892838f7b2355f2647bb94a0817bc7;room-id=62300805;first-msg=0;emotes= :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn WAYTOOBUH","@rm-received-ts=1704558323841;user-id=167633177;emotes=;mod=0;id=56025526-3beb-4adc-9d45-3bba873464fb;first-msg=0;subscriber=0;user-type=;color=#10E2E2;flags=;badges=twitch-recap-2023/1;client-nonce=58142ebdd499fec4f4ae021e9c58cca7;display-name=ALotOfChickens;room-id=62300805;tmi-sent-ts=1704558323647;returning-chatter=0;turbo=0;badge-info=;historical=1 :alotofchickens!alotofchickens@alotofchickens.tmi.twitch.tv PRIVMSG #nymn WAYTOODANK","@color=#00ED2A;badge-info=subscriber/43;user-type=;rm-received-ts=1704558323906;id=4e54fdf3-4432-439d-91f2-3676b73cdcb0;flags=;first-msg=0;emotes=;tmi-sent-ts=1704558323736;returning-chatter=0;turbo=0;display-name=MaxThurian;room-id=62300805;mod=0;client-nonce=d876f58bfb9c200c314771e02fd63525;subscriber=1;user-id=60181947;historical=1;badges=subscriber/42,twitch-recap-2023/1 :maxthurian!maxthurian@maxthurian.tmi.twitch.tv PRIVMSG #nymn :WAYTOODANK my head","@tmi-sent-ts=1704558323769;id=dc863ee7-9f5a-41c5-8a91-5da56b75187e;user-id=433352132;emotes=;badge-info=subscriber/38;subscriber=1;color=#63BD68;returning-chatter=0;display-name=jontEmillian;turbo=0;rm-received-ts=1704558323949;first-msg=0;flags=;room-id=62300805;user-type=;mod=0;badges=subscriber/36,twitch-recap-2023/1;historical=1 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn :forsenParty 󠀀","@badges=;display-name=forsenkkona_;returning-chatter=0;rm-received-ts=1704558324219;mod=0;user-id=151423066;flags=;badge-info=;tmi-sent-ts=1704558324036;turbo=0;historical=1;room-id=62300805;id=b1af35e4-7614-4b5c-bf16-d8383f53ee64;subscriber=0;emotes=;color=#FF69B4;user-type=;first-msg=0 :forsenkkona_!forsenkkona_@forsenkkona_.tmi.twitch.tv PRIVMSG #nymn AlienPls","@room-id=62300805;rm-received-ts=1704558324589;flags=;badge-info=;user-type=;emotes=;historical=1;user-id=163349624;id=edf97de2-7804-4e21-8055-8b7074d6299f;turbo=0;display-name=IllidanSF;tmi-sent-ts=1704558324388;mod=0;badges=no_video/1;first-msg=0;color=#00FF7F;returning-chatter=0;subscriber=0 :illidansf!illidansf@illidansf.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@room-id=62300805;color=#008000;client-nonce=c6a93a2d077a401bfcc0a1fa13bc7580;rm-received-ts=1704558324709;tmi-sent-ts=1704558324533;display-name=AikawaCaiman;first-msg=0;subscriber=0;emotes=;id=b7bbeb17-bad6-4b2b-84a5-a342092933cf;badge-info=;badges=no_audio/1;returning-chatter=0;turbo=0;flags=;mod=0;historical=1;user-id=157747813;user-type= :aikawacaiman!aikawacaiman@aikawacaiman.tmi.twitch.tv PRIVMSG #nymn WAYTOODANK","@id=6ff2fcf2-22a7-4ed8-8847-f21270e9a516;tmi-sent-ts=1704558324999;mod=0;emotes=;badge-info=;turbo=0;flags=;subscriber=0;first-msg=0;user-id=51967700;color=#FF0000;historical=1;user-type=;room-id=62300805;returning-chatter=0;client-nonce=ab3623b9ce719f9a99e4dde80add843d;display-name=Patixxl;badges=;rm-received-ts=1704558325178 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@tmi-sent-ts=1704558325455;badges=subscriber/0,bits/1;rm-received-ts=1704558325628;room-id=62300805;badge-info=subscriber/2;color=#FF69B4;user-id=22733078;display-name=miniwoffer;mod=0;emotes=;turbo=0;id=df51ecee-0194-4574-94f2-c62cc72f7382;first-msg=0;flags=;subscriber=1;historical=1;client-nonce=ce97fa5118e62ea6d35f4c01cfb629c8;returning-chatter=0;user-type= :miniwoffer!miniwoffer@miniwoffer.tmi.twitch.tv PRIVMSG #nymn :monkaS RUN","@id=4781f1ef-00ff-4e16-af2f-91620969570c;color=#FFFF00;mod=0;badges=subscriber/9,turbo/1;display-name=Kotzblitz20;client-nonce=e18e193d7de9217664758c7c389f63f6;returning-chatter=0;badge-info=subscriber/9;tmi-sent-ts=1704558325486;rm-received-ts=1704558325700;first-msg=0;subscriber=1;user-id=40037186;user-type=;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154;historical=1;turbo=1;room-id=62300805;flags= :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@historical=1;display-name=Joshlad;color=#D52AFF;badges=vip/1,subscriber/72,rplace-2023/1;subscriber=1;emotes=;returning-chatter=0;vip=1;badge-info=subscriber/77;tmi-sent-ts=1704558325613;first-msg=0;turbo=0;room-id=62300805;user-type=;rm-received-ts=1704558325787;id=0038971c-4ade-4266-9a8f-40170237375a;flags=;mod=0;user-id=87120320 :joshlad!joshlad@joshlad.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@flags=;rm-received-ts=1704558326207;room-id=62300805;id=05abf77d-7568-41b4-815b-2650c38ed1ed;turbo=0;historical=1;first-msg=0;user-id=36237730;emotes=;display-name=Raztheman;mod=0;color=#0000FF;badge-info=;client-nonce=e2c7aca43bd133717017f167193ddb5a;subscriber=0;returning-chatter=0;user-type=;tmi-sent-ts=1704558326027;badges=premium/1 :raztheman!raztheman@raztheman.tmi.twitch.tv PRIVMSG #nymn :@ImDaxify this was before AI","@historical=1;flags=;first-msg=0;rm-received-ts=1704558326768;display-name=jontEmillian;color=#63BD68;emotes=;id=67ec380b-2a79-4c4e-8fae-f75549560804;user-type=;badges=subscriber/36,twitch-recap-2023/1;mod=0;turbo=0;user-id=433352132;badge-info=subscriber/38;tmi-sent-ts=1704558326594;returning-chatter=0;subscriber=1;room-id=62300805 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn forsenParty","@rm-received-ts=1704558326993;user-id=85837900;mod=0;emotes=;id=53b0a831-68e3-4e86-ba22-e58eed4a67a6;user-type=;flags=;first-msg=0;color=#DAA520;turbo=0;subscriber=1;historical=1;returning-chatter=0;room-id=62300805;client-nonce=f0bae9acabec8a9e7e64c53104396faa;badge-info=subscriber/37;tmi-sent-ts=1704558326822;display-name=DontCagePlebs;badges=subscriber/36,no_audio/1 :dontcageplebs!dontcageplebs@dontcageplebs.tmi.twitch.tv PRIVMSG #nymn FEELSWAYTOOGOOD","@tmi-sent-ts=1704558327034;historical=1;flags=;badge-info=;user-type=;turbo=0;emotes=;user-id=431946171;mod=0;id=05ec2df1-5748-4d05-a4b9-37f3f8e9b0b7;first-msg=0;display-name=jonhycrack;color=#008000;room-id=62300805;badges=no_audio/1;subscriber=0;returning-chatter=0;rm-received-ts=1704558327202 :jonhycrack!jonhycrack@jonhycrack.tmi.twitch.tv PRIVMSG #nymn FEELSWAYTOOGOOD","@first-msg=0;user-id=179965969;subscriber=0;display-name=Dhreago;flags=;room-id=62300805;badge-info=;returning-chatter=0;emotes=;user-type=;color=#1E90FF;tmi-sent-ts=1704558327227;mod=0;id=2e5b7044-5b17-489f-a592-2468d9786e74;client-nonce=8ba5592e4e63f92f781610a42125f573;turbo=0;badges=;rm-received-ts=1704558327412;historical=1 :dhreago!dhreago@dhreago.tmi.twitch.tv PRIVMSG #nymn FEELSWAYTOOGOOD","@historical=1;badge-info=;first-msg=0;client-nonce=b7a36956bcd166dc622c3148685df8d6;subscriber=0;user-id=998960046;tmi-sent-ts=1704558327520;room-id=62300805;color=;rm-received-ts=1704558327690;display-name=jross1812;flags=;turbo=0;mod=0;returning-chatter=0;emotes=;id=14f1b73e-3620-4aba-97d1-9bb384a95b05;badges=;user-type= :jross1812!jross1812@jross1812.tmi.twitch.tv PRIVMSG #nymn docPls","@room-id=62300805;user-type=;badges=;id=b7757500-61d5-4001-8ba7-d7d7f0f9b825;returning-chatter=0;flags=;rm-received-ts=1704558328019;mod=0;badge-info=;historical=1;client-nonce=8d8e78bd5573ce8b1f59a81103bde050;emotes=;user-id=51967700;display-name=Patixxl;turbo=0;color=#FF0000;tmi-sent-ts=1704558327844;first-msg=0;subscriber=0 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@rm-received-ts=1704558328563;tmi-sent-ts=1704558328406;id=a512d728-5fa5-4648-ae93-d2861056290a;badge-info=;returning-chatter=0;display-name=jonhycrack;subscriber=0;emotes=;color=#008000;user-type=;user-id=431946171;mod=0;flags=;badges=no_audio/1;historical=1;turbo=0;room-id=62300805;first-msg=0 :jonhycrack!jonhycrack@jonhycrack.tmi.twitch.tv PRIVMSG #nymn :FEELSWAYTOOGOOD 󠀀","@tmi-sent-ts=1704558328665;user-id=157747813;turbo=0;user-type=;returning-chatter=0;client-nonce=25c4ddac4292049bf19080daf414878a;flags=16-19:P.6;id=fd1b5054-c8cc-42b9-b173-75ff3a05d5fe;rm-received-ts=1704558328836;subscriber=0;emotes=;room-id=62300805;color=#008000;badges=no_audio/1;historical=1;first-msg=0;mod=0;display-name=AikawaCaiman;badge-info= :aikawacaiman!aikawacaiman@aikawacaiman.tmi.twitch.tv PRIVMSG #nymn :WAYTOODANK HOLY FUCK","@tmi-sent-ts=1704558329150;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170,176-186,192-202,208-218,224-234,240-250,256-266;id=db3854e4-1d2f-4701-a791-a757fce273f1;first-msg=0;display-name=Kotzblitz20;returning-chatter=0;rm-received-ts=1704558329336;flags=;turbo=1;mod=0;historical=1;user-type=;room-id=62300805;color=#FFFF00;badges=subscriber/9,turbo/1;badge-info=subscriber/9;subscriber=1;user-id=40037186;client-nonce=08ee21f48a8801d7a40050f6c54dd7b6 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@historical=1;flags=;id=6ed41fe1-a803-4c51-a4e6-10d7fc68a46d;tmi-sent-ts=1704558329195;room-id=62300805;client-nonce=38080a601ba7c58cc3289bc61f44f40c;rm-received-ts=1704558329358;mod=0;user-id=474204887;emotes=;subscriber=1;badges=subscriber/0,premium/1;user-type=;returning-chatter=0;first-msg=0;display-name=mnqn18;color=#1E90FF;badge-info=subscriber/2;turbo=0 :mnqn18!mnqn18@mnqn18.tmi.twitch.tv PRIVMSG #nymn FEELSWAYTOOGOOD","@turbo=0;subscriber=0;badge-info=;first-msg=0;historical=1;rm-received-ts=1704558330327;display-name=jonhycrack;badges=no_audio/1;room-id=62300805;id=37ddbe79-8f91-4487-afaf-c53015c8e761;flags=;user-id=431946171;mod=0;tmi-sent-ts=1704558330159;returning-chatter=0;color=#008000;user-type=;emotes= :jonhycrack!jonhycrack@jonhycrack.tmi.twitch.tv PRIVMSG #nymn FEELSWAYTOOGOOD","@first-msg=0;room-id=62300805;subscriber=0;returning-chatter=0;user-type=;emotes=;flags=;color=#D2691E;badge-info=;mod=0;historical=1;rm-received-ts=1704558330379;user-id=38870532;turbo=0;tmi-sent-ts=1704558330217;display-name=Nopem8;badges=;id=37bf2d07-ee0c-4939-b1de-9605c77b8b00;client-nonce=83b9c6c63fc2cd78a7e2e2bd661e8d6e :nopem8!nopem8@nopem8.tmi.twitch.tv PRIVMSG #nymn :docPls ?","@mod=0;room-id=62300805;user-type=;tmi-sent-ts=1704558330830;color=#00FF7F;subscriber=0;flags=;badges=rplace-2023/1;display-name=Dankarop;historical=1;turbo=0;emotes=;id=0aaeb648-dd41-480f-903e-f4926d8f455d;client-nonce=2b26b1e2e466f12060390faeeeb399a6;first-msg=0;returning-chatter=0;rm-received-ts=1704558331026;user-id=467106798;badge-info= :dankarop!dankarop@dankarop.tmi.twitch.tv PRIVMSG #nymn forsenParty","@turbo=0;user-type=;returning-chatter=0;first-msg=0;badge-info=;user-id=37931493;display-name=deever44;subscriber=0;id=f00cc3b1-db07-4248-822e-3ed5daa3f2dc;mod=0;color=;flags=;client-nonce=76ce7498ac62f01d523af3744d0962de;emotes=;room-id=62300805;tmi-sent-ts=1704558331567;badges=no_audio/1;historical=1;rm-received-ts=1704558331754 :deever44!deever44@deever44.tmi.twitch.tv PRIVMSG #nymn forsenParty","@badge-info=subscriber/38;user-type=;turbo=0;emotes=;first-msg=0;color=#63BD68;id=7ea7ac00-50c7-436c-9935-d818900df7dd;user-id=433352132;returning-chatter=0;badges=subscriber/36,twitch-recap-2023/1;mod=0;tmi-sent-ts=1704558332090;historical=1;room-id=62300805;rm-received-ts=1704558332267;subscriber=1;display-name=jontEmillian;flags= :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn :forsenParty ACID","@rm-received-ts=1704558332628;user-type=;user-id=117033693;first-msg=0;badges=subscriber/3;mod=0;emotes=;historical=1;turbo=0;badge-info=subscriber/3;client-nonce=aecd4490775960d52f208338a036f354;flags=;room-id=62300805;tmi-sent-ts=1704558332440;returning-chatter=0;id=55632e15-3f0d-41ad-9cb4-cfc96365604c;display-name=NSAPartyVan;subscriber=1;color=#1E90FF :nsapartyvan!nsapartyvan@nsapartyvan.tmi.twitch.tv PRIVMSG #nymn WalterVibe","@rm-received-ts=1704558332904;user-type=;turbo=0;tmi-sent-ts=1704558332713;color=#FF2424;client-nonce=6fe3c74a3c08e3e2354716c6a0ad0b80;first-msg=0;emotes=;room-id=62300805;subscriber=1;badge-info=subscriber/49;badges=subscriber/48,bits/25000;historical=1;id=119f53e1-f41c-41fb-9db8-de06b238da2c;display-name=ME_ME;mod=0;returning-chatter=0;flags=;user-id=159210800 :me_me!me_me@me_me.tmi.twitch.tv PRIVMSG #nymn WAYTOODANK","@badges=subscriber/9,turbo/1;historical=1;room-id=62300805;mod=0;flags=;user-type=;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74;turbo=1;client-nonce=235b75636b5f8876bf06445c3d4c4040;first-msg=0;id=3f4ad233-6c34-42eb-a443-3200a5520f6b;rm-received-ts=1704558332936;subscriber=1;returning-chatter=0;badge-info=subscriber/9;display-name=Kotzblitz20;tmi-sent-ts=1704558332757;color=#FFFF00;user-id=40037186 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@display-name=Mawsonator;client-nonce=35dadbb6ecdc5c4222fc342eb82a4863;first-msg=0;badge-info=subscriber/47;id=9997af8f-cefd-46fc-a3c6-6c4ffa371775;rm-received-ts=1704558333303;color=#FF0000;user-type=;room-id=62300805;mod=0;flags=;user-id=92529125;badges=subscriber/42,twitch-recap-2023/1;subscriber=1;emotes=;historical=1;tmi-sent-ts=1704558333127;turbo=0;returning-chatter=0 :mawsonator!mawsonator@mawsonator.tmi.twitch.tv PRIVMSG #nymn FEELSWAYTOOGOOD","@room-id=62300805;rm-received-ts=1704558333647;badge-info=;color=#FF0000;client-nonce=d3607f59caba20c53029dcf1c4da8cab;returning-chatter=0;first-msg=0;user-id=86965943;user-type=;display-name=voyu1337;subscriber=0;id=5a67d1c7-12d0-4fc7-a4b8-4ee5f038259a;tmi-sent-ts=1704558333471;turbo=0;mod=0;flags=;historical=1;emotes=;badges=premium/1 :voyu1337!voyu1337@voyu1337.tmi.twitch.tv PRIVMSG #nymn :WAYTOODANK WAYTOODANK WAYTOODANK WAYTOODANK WAYTOODANK WAYTOODANK WAYTOODANK WAYTOODANK WAYTOODANK WAYTOODANK WAYTOODANK WAYTOODANK WAYTOODANK WAYTOODANK WAYTOODANK WAYTOODANK WAYTOODANK WAYTOODANK WAYTOODANK WAYTOODANK WAYTOODANK WAYTOODANK WAYTOODANK WAYTOODANK WAYTOODANK WAYTOODANK","@returning-chatter=0;room-id=62300805;user-type=;display-name=Joshlad;tmi-sent-ts=1704558333947;mod=0;badge-info=subscriber/77;historical=1;emotes=;first-msg=0;badges=vip/1,subscriber/72,rplace-2023/1;subscriber=1;color=#D52AFF;rm-received-ts=1704558334135;user-id=87120320;turbo=0;id=5a86c4fb-2d4a-4ae4-8b07-942382ebb38b;flags=;vip=1 :joshlad!joshlad@joshlad.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@tmi-sent-ts=1704558334578;user-id=431946171;rm-received-ts=1704558334748;badges=no_audio/1;historical=1;room-id=62300805;color=#008000;subscriber=0;badge-info=;first-msg=0;emotes=;turbo=0;user-type=;flags=;mod=0;returning-chatter=0;id=6319543e-fb08-4748-87fe-db77cf300e15;display-name=jonhycrack :jonhycrack!jonhycrack@jonhycrack.tmi.twitch.tv PRIVMSG #nymn :FEELSWAYTOOGOOD EDM","@badge-info=subscriber/38;subscriber=1;user-type=;color=#63BD68;flags=;room-id=62300805;badges=subscriber/36,twitch-recap-2023/1;id=3b276ed7-329b-49d0-800a-9a13a90e43a5;tmi-sent-ts=1704558334882;turbo=0;user-id=433352132;mod=0;display-name=jontEmillian;historical=1;returning-chatter=0;rm-received-ts=1704558335066;emotes=;first-msg=0 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn forsenParty","@rm-received-ts=1704558335346;user-id=29764188;room-id=62300805;client-nonce=145a8db4e58956c0ef5c22e32797af55;turbo=0;tmi-sent-ts=1704558335174;user-type=;emotes=30134:16-19;first-msg=0;id=e148a3ed-c424-41a5-a549-badb8bf13a5c;display-name=zzlint;flags=;badge-info=;returning-chatter=0;color=#1E90FF;subscriber=0;historical=1;badges=;mod=0 :zzlint!zzlint@zzlint.tmi.twitch.tv PRIVMSG #nymn :FEELSWAYTOOGOOD Mau5","@badge-info=;room-id=62300805;turbo=0;client-nonce=99b07f2e056e82ab76823ddb19329e84;first-msg=0;badges=bits/100;flags=;id=985500ad-15cf-44a7-8343-ce2c5a392e6c;returning-chatter=0;emotes=;tmi-sent-ts=1704558335289;rm-received-ts=1704558335472;display-name=DM8917;user-type=;user-id=63372784;historical=1;subscriber=0;color=#25E000;mod=0 :dm8917!dm8917@dm8917.tmi.twitch.tv PRIVMSG #nymn docJAMMER","@id=5770bafc-fb5c-49fb-9330-67b7c50f6471;badges=no_audio/1;turbo=0;emotes=;subscriber=0;historical=1;display-name=jonhycrack;room-id=62300805;mod=0;tmi-sent-ts=1704558336496;user-id=431946171;color=#008000;first-msg=0;rm-received-ts=1704558336666;user-type=;returning-chatter=0;flags=;badge-info= :jonhycrack!jonhycrack@jonhycrack.tmi.twitch.tv PRIVMSG #nymn :FEELSWAYTOOGOOD EDM","@display-name=Patixxl;returning-chatter=0;subscriber=0;tmi-sent-ts=1704558336633;historical=1;rm-received-ts=1704558336826;badge-info=;user-id=51967700;room-id=62300805;turbo=0;color=#FF0000;client-nonce=2de04a9fc39a1c420602361333c29548;user-type=;flags=;id=d040509a-cf15-49cd-94b4-e779a0899ba6;mod=0;badges=;emotes=;first-msg=0 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn FEELSWAYTOODONKMAN","@subscriber=1;user-type=;turbo=1;returning-chatter=0;user-id=40037186;client-nonce=6544d1f489cff2e0662da3e92f86a1ab;color=#FFFF00;mod=0;rm-received-ts=1704558336970;id=9054e699-2b56-4949-a852-07ad3688493b;emotes=;tmi-sent-ts=1704558336796;room-id=62300805;historical=1;flags=;badges=subscriber/9,turbo/1;display-name=Kotzblitz20;badge-info=subscriber/9;first-msg=0 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn FEELSWAYTOOGOOD","@emotes=;subscriber=1;mod=0;room-id=62300805;first-msg=0;rm-received-ts=1704558337489;id=1c22b735-dfe2-4915-92b6-b60fa347eacb;client-nonce=1b62955d200aac3c87cf87e90281a5b4;badge-info=subscriber/42;badges=subscriber/42,bits/1000;color=#9ACD32;turbo=0;display-name=wheeely;flags=;user-type=;user-id=68478828;tmi-sent-ts=1704558337312;returning-chatter=0;historical=1 :wheeely!wheeely@wheeely.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@user-type=;client-nonce=73b0314c295afe026f6f1e4627e77261;returning-chatter=0;flags=;user-id=85837900;turbo=0;tmi-sent-ts=1704558337339;historical=1;emotes=28:0-12;rm-received-ts=1704558337529;id=6cd76476-0425-4ebc-893a-ec12dd78d05a;badge-info=subscriber/37;room-id=62300805;display-name=DontCagePlebs;mod=0;subscriber=1;color=#DAA520;first-msg=0;badges=subscriber/36,no_audio/1 :dontcageplebs!dontcageplebs@dontcageplebs.tmi.twitch.tv PRIVMSG #nymn :MrDestructoid Clap","@emotes=;user-id=465513111;tmi-sent-ts=1704558337347;first-msg=0;subscriber=0;client-nonce=3f36dd46a3aa44625cd2c8b3b171d2f5;historical=1;id=ba3ab534-3e8c-4c92-a051-2c1d57f79ccf;rm-received-ts=1704558337537;mod=0;room-id=62300805;badge-info=;badges=;flags=;returning-chatter=0;color=;turbo=0;user-type=;display-name=mongushmengi :mongushmengi!mongushmengi@mongushmengi.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@display-name=liber7as;historical=1;turbo=0;tmi-sent-ts=1704558337987;returning-chatter=0;user-type=;badge-info=subscriber/41;user-id=92402102;subscriber=1;badges=subscriber/36;id=0daf3441-461e-4aaf-bf20-845faaf3bd4b;emotes=;room-id=62300805;client-nonce=b7b620ab04a5dd464871e5e256567180;color=#41FF00;mod=0;rm-received-ts=1704558338167;first-msg=0;flags= :liber7as!liber7as@liber7as.tmi.twitch.tv PRIVMSG #nymn :i think its just going to get weirder and weirder with each tunnel","@display-name=NSAPartyVan;badges=subscriber/3;first-msg=0;emotes=;historical=1;room-id=62300805;returning-chatter=0;tmi-sent-ts=1704558338599;color=#1E90FF;client-nonce=bbe7dba570c8812e9a1a68fb86c897ee;badge-info=subscriber/3;id=82c478fa-8ef3-4ddf-9970-48ba219fbda3;subscriber=1;user-type=;flags=;turbo=0;user-id=117033693;mod=0;rm-received-ts=1704558338754 :nsapartyvan!nsapartyvan@nsapartyvan.tmi.twitch.tv PRIVMSG #nymn :WalterVibe 󠀀","@flags=;returning-chatter=0;display-name=Kotzblitz20;historical=1;badge-info=subscriber/9;rm-received-ts=1704558339764;user-id=40037186;room-id=62300805;color=#FFFF00;user-type=;client-nonce=a28fab8314c98e5a8df581d6ec7c672b;tmi-sent-ts=1704558339587;id=caafdac3-0710-48b5-9801-0257c7d28b45;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42;first-msg=0;subscriber=1;badges=subscriber/9,turbo/1;turbo=1;mod=0 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM","@first-msg=0;display-name=jonhycrack;flags=;room-id=62300805;turbo=0;subscriber=0;tmi-sent-ts=1704558340475;rm-received-ts=1704558340649;color=#008000;badges=no_audio/1;returning-chatter=0;badge-info=;emotes=;mod=0;user-type=;id=0a26d8e4-a61e-4c97-81ce-d921f90f1835;historical=1;user-id=431946171 :jonhycrack!jonhycrack@jonhycrack.tmi.twitch.tv PRIVMSG #nymn :FEELSWAYTOOGOOD EDM","@flags=;historical=1;user-type=;subscriber=0;returning-chatter=0;user-id=151423066;room-id=62300805;color=#FF69B4;badge-info=;emotes=;first-msg=0;tmi-sent-ts=1704558340966;display-name=forsenkkona_;mod=0;badges=;id=fff8cab9-47aa-4390-97f2-696d8ca070b3;rm-received-ts=1704558341126;turbo=0 :forsenkkona_!forsenkkona_@forsenkkona_.tmi.twitch.tv PRIVMSG #nymn :AlienPls 󠀀","@flags=;turbo=0;mod=0;user-id=433352132;emotes=;user-type=;id=78a3ad68-fc6d-4d65-8b0d-399f1f181e40;first-msg=0;returning-chatter=0;room-id=62300805;display-name=jontEmillian;subscriber=1;color=#63BD68;badge-info=subscriber/38;badges=subscriber/36,twitch-recap-2023/1;historical=1;rm-received-ts=1704558341267;tmi-sent-ts=1704558341100 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn :forsenParty 󠀀","@returning-chatter=0;badge-info=;tmi-sent-ts=1704558342827;id=19e4769d-0334-45a9-b0ee-3175545595f2;historical=1;color=#25E000;user-type=;subscriber=0;client-nonce=9f65c668a1cdac9bfefcf1246309ad22;display-name=DM8917;emotes=;rm-received-ts=1704558343005;turbo=0;room-id=62300805;user-id=63372784;flags=;mod=0;first-msg=0;badges=bits/100 :dm8917!dm8917@dm8917.tmi.twitch.tv PRIVMSG #nymn FEELSWAYTOODONKMAN","@flags=;mod=0;historical=1;room-id=62300805;first-msg=0;tmi-sent-ts=1704558343772;user-type=;turbo=0;emotes=;subscriber=0;returning-chatter=0;client-nonce=db722ef1be2cc9b0a57747dbdac4894e;badge-info=;display-name=Patixxl;badges=;color=#FF0000;user-id=51967700;id=63e9a403-7b0a-46c3-9db5-0d50246e9cd2;rm-received-ts=1704558343936 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn FEELSWAYTOOGOOD","@badge-info=;id=673d33e0-fd71-4d4a-922d-5a6ff4015423;first-msg=0;user-type=;color=;display-name=jross1812;mod=0;flags=;historical=1;subscriber=0;client-nonce=773129302dba08d500aad5b30b76d74b;user-id=998960046;turbo=0;tmi-sent-ts=1704558343809;rm-received-ts=1704558343983;returning-chatter=0;emotes=;badges=;room-id=62300805 :jross1812!jross1812@jross1812.tmi.twitch.tv PRIVMSG #nymn :docPls docPls","@subscriber=1;room-id=62300805;returning-chatter=0;color=#FF69B4;rm-received-ts=1704558346671;badges=subscriber/0,bits/1;tmi-sent-ts=1704558346474;client-nonce=9660eb07d06b1617c16fa1f4a3f6f172;user-type=;badge-info=subscriber/2;first-msg=0;flags=;mod=0;user-id=22733078;display-name=miniwoffer;historical=1;id=3e3b48d5-5f79-42b9-b0cd-47390746da94;emotes=;turbo=0 :miniwoffer!miniwoffer@miniwoffer.tmi.twitch.tv PRIVMSG #nymn FEELSWAYTOODONKMAN","@returning-chatter=0;turbo=0;historical=1;mod=0;badges=premium/1;subscriber=0;first-msg=0;display-name=sikonic;room-id=62300805;flags=;tmi-sent-ts=1704558346461;client-nonce=5bbeec575baf8c4a4a545943cdd62009;rm-received-ts=1704558346681;badge-info=;user-id=147025589;emotes=;user-type=;color=#FFBB3C;id=9efed424-7285-4b90-91ec-cc68b1fad32c :sikonic!sikonic@sikonic.tmi.twitch.tv PRIVMSG #nymn :Ratge RaveTime EDM forsenParty Ratge RaveTime EDM forsenParty Ratge RaveTime EDM forsenParty Ratge RaveTime EDM forsenParty Ratge RaveTime EDM forsenParty Ratge RaveTime EDM forsenParty Ratge RaveTime EDM forsenParty Ratge RaveTime EDM forsenParty Ratge RaveTime EDM forsenParty Ratge RaveTime EDM forsenParty Ratge RaveTime EDM forsenParty","@subscriber=0;user-type=;badge-info=;tmi-sent-ts=1704558347073;rm-received-ts=1704558347238;turbo=0;user-id=837777908;color=#8A2BE2;room-id=62300805;badges=;first-msg=0;flags=;client-nonce=d9080b34d5ad87f041c5f1111560a4e6;mod=0;display-name=16oct22;id=f04d1db1-0c07-476e-9da3-c9d6a68958b3;returning-chatter=0;historical=1;emotes= :16oct22!16oct22@16oct22.tmi.twitch.tv PRIVMSG #nymn :This is like a Cruelty squad demo, not bad, and it is randomized it says","@first-msg=0;subscriber=0;room-id=62300805;display-name=Intel_power;id=e2f46fee-0f38-4f2d-973c-5180e9867de0;mod=0;user-type=;flags=;rm-received-ts=1704558347410;returning-chatter=0;color=#0000FF;client-nonce=752a3a1e24b43462fd355cdbaa4ff968;historical=1;turbo=0;emotes=;tmi-sent-ts=1704558347239;badge-info=;user-id=103665668;badges=bits-charity/1 :intel_power!intel_power@intel_power.tmi.twitch.tv PRIVMSG #nymn IVEGONEPASTHEPOINTOFINSANITY","@flags=;returning-chatter=0;first-msg=0;rm-received-ts=1704558347868;display-name=ME_ME;room-id=62300805;id=3bdb5102-f8eb-4a95-b70c-e9b9699b11f2;subscriber=1;emotes=;mod=0;turbo=0;user-type=;user-id=159210800;tmi-sent-ts=1704558347706;historical=1;badges=subscriber/48,bits/25000;client-nonce=507a6b36bc9a20bfd8e20a320cbceb9f;badge-info=subscriber/49;color=#FF2424 :me_me!me_me@me_me.tmi.twitch.tv PRIVMSG #nymn :WAYTOODANK 󠀀","@user-id=40037186;first-msg=0;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58;returning-chatter=0;rm-received-ts=1704558349387;id=6275aaa3-7679-462d-bdbe-3fd2c6360ca5;mod=0;historical=1;turbo=1;badge-info=subscriber/9;user-type=;room-id=62300805;tmi-sent-ts=1704558349190;badges=subscriber/9,turbo/1;client-nonce=57f18d039babda4daed6f1f6026ca5cd;flags=;display-name=Kotzblitz20;color=#FFFF00;subscriber=1 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@room-id=62300805;mod=0;badges=subscriber/36,twitch-recap-2023/1;first-msg=0;emotes=;flags=;turbo=0;historical=1;rm-received-ts=1704558350549;color=#63BD68;user-type=;display-name=jontEmillian;tmi-sent-ts=1704558350356;subscriber=1;user-id=433352132;id=fa74d11d-1bbe-4483-ad5d-284a3ca9cf59;returning-chatter=0;badge-info=subscriber/38 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn forsenParty","@flags=;badges=subscriber/9,turbo/1;id=9b469c92-eb93-4068-a720-715787559163;rm-received-ts=1704558351700;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170,176-186,192-202,208-218,224-234;first-msg=0;historical=1;room-id=62300805;returning-chatter=0;user-type=;color=#FFFF00;mod=0;user-id=40037186;badge-info=subscriber/9;display-name=Kotzblitz20;tmi-sent-ts=1704558351145;subscriber=1;turbo=1;client-nonce=7738a42f34f0f19da554581d6fad7e9f :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@display-name=Patixxl;badges=;mod=0;emotes=;user-type=;turbo=0;badge-info=;id=21f0cc2d-1008-4601-9908-0a538d8b4dc0;user-id=51967700;client-nonce=389ea1585c09bf5563d7f60bd8bfa20b;rm-received-ts=1704558352627;flags=;returning-chatter=0;tmi-sent-ts=1704558352439;room-id=62300805;subscriber=0;first-msg=0;color=#FF0000;historical=1 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@badge-info=subscriber/38;first-msg=0;room-id=62300805;turbo=0;tmi-sent-ts=1704558352479;color=#63BD68;subscriber=1;flags=;historical=1;rm-received-ts=1704558352645;returning-chatter=0;display-name=jontEmillian;mod=0;badges=subscriber/36,twitch-recap-2023/1;user-id=433352132;emotes=;id=edde475e-d2d9-4ed2-91a0-976ef81ba4ae;user-type= :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn :forsenParty 󠀀","@tmi-sent-ts=1704558352643;first-msg=0;returning-chatter=0;mod=0;id=5d3db884-ec47-4938-85fb-1dbc976a9c62;color=#D52AFF;display-name=Joshlad;turbo=0;emotes=;room-id=62300805;vip=1;user-id=87120320;subscriber=1;user-type=;flags=;rm-received-ts=1704558352816;historical=1;badge-info=subscriber/77;badges=vip/1,subscriber/72,rplace-2023/1 :joshlad!joshlad@joshlad.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@badge-info=subscriber/11;user-type=;color=#008000;id=d3f9d69e-808e-44d3-8df7-411addd9a878;mod=0;rm-received-ts=1704558353609;returning-chatter=0;subscriber=1;room-id=62300805;historical=1;client-nonce=7dcab2448d1d7b3b5b475c452c7b7d4e;emotes=;first-msg=0;turbo=0;user-id=135571016;flags=;display-name=terning;tmi-sent-ts=1704558353433;badges=subscriber/9,rplace-2023/1 :terning!terning@terning.tmi.twitch.tv PRIVMSG #nymn IVEGONEPASTHEPOINTOFINSANITY","@subscriber=0;flags=;mod=0;first-msg=0;turbo=0;rm-received-ts=1704558353926;user-type=;returning-chatter=0;id=dbd8d668-b130-4099-9a5e-40ddee5c53ba;user-id=38870532;tmi-sent-ts=1704558353761;badges=;client-nonce=a668c12be8fa44b736b11a28fdf4f05e;color=#D2691E;room-id=62300805;badge-info=;display-name=Nopem8;emotes=;historical=1 :nopem8!nopem8@nopem8.tmi.twitch.tv PRIVMSG #nymn docPls","@emotes=;subscriber=1;room-id=62300805;returning-chatter=0;display-name=MaxThurian;historical=1;badges=subscriber/42,twitch-recap-2023/1;mod=0;badge-info=subscriber/43;client-nonce=87da96cd27dd086fa057683379735851;color=#00ED2A;flags=;id=840187ff-3a78-4235-8520-1d1541a52ba4;rm-received-ts=1704558354069;turbo=0;user-id=60181947;user-type=;first-msg=0;tmi-sent-ts=1704558353902 :maxthurian!maxthurian@maxthurian.tmi.twitch.tv PRIVMSG #nymn :monkaOMEGA ForsenLookingAtYou","@user-type=;emotes=;turbo=0;historical=1;rm-received-ts=1704558354670;client-nonce=652c893de89a9822d3b007485ccb2939;mod=0;display-name=Patixxl;flags=;room-id=62300805;returning-chatter=0;badges=;user-id=51967700;tmi-sent-ts=1704558354490;id=bda06fa4-c856-438d-ac20-b0b2cd255944;first-msg=0;subscriber=0;color=#FF0000;badge-info= :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138;user-type=;badge-info=subscriber/9;id=e3af92dc-6ad6-4f14-bcf7-9f41880497e3;turbo=1;subscriber=1;rm-received-ts=1704558355115;room-id=62300805;color=#FFFF00;tmi-sent-ts=1704558354918;user-id=40037186;mod=0;badges=subscriber/9,turbo/1;historical=1;client-nonce=0493eecde5af5299cc960d66fa5dcb44;display-name=Kotzblitz20;first-msg=0;returning-chatter=0;flags= :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@display-name=ME_ME;flags=;user-id=159210800;room-id=62300805;historical=1;subscriber=1;badges=subscriber/48,bits/25000;emote-only=1;rm-received-ts=1704558355254;mod=0;tmi-sent-ts=1704558355052;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10;color=#FF2424;first-msg=0;returning-chatter=0;turbo=0;id=4c5ff458-0634-4b36-bf71-c7fd0d7f4772;badge-info=subscriber/49;user-type=;client-nonce=db77bb11b4dc7d5e87c27c23670f080a :me_me!me_me@me_me.tmi.twitch.tv PRIVMSG #nymn forsenParty","@subscriber=1;room-id=62300805;turbo=0;user-type=;flags=;color=#63BD68;user-id=433352132;badge-info=subscriber/38;rm-received-ts=1704558355789;returning-chatter=0;mod=0;display-name=jontEmillian;emotes=;tmi-sent-ts=1704558355600;badges=subscriber/36,twitch-recap-2023/1;first-msg=0;id=5db9223f-0cab-4d40-9c47-d10c0f0ad437;historical=1 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn forsenParty","@first-msg=0;user-id=103665668;emotes=;flags=;badges=bits-charity/1;id=f520782b-2c65-457c-bb21-f668f7c5227f;rm-received-ts=1704558355994;turbo=0;client-nonce=9ecee36f432861d92e849da044f52d50;room-id=62300805;user-type=;mod=0;display-name=Intel_power;historical=1;subscriber=0;badge-info=;tmi-sent-ts=1704558355826;returning-chatter=0;color=#0000FF :intel_power!intel_power@intel_power.tmi.twitch.tv PRIVMSG #nymn :RAT RAVE","@emotes=;mod=0;returning-chatter=0;badges=;user-type=;rm-received-ts=1704558356803;display-name=Patixxl;badge-info=;first-msg=0;subscriber=0;tmi-sent-ts=1704558356626;turbo=0;user-id=51967700;flags=;room-id=62300805;historical=1;id=2200ea1f-d969-4d21-835e-07be22d4ede8;color=#FF0000;client-nonce=94ae4306657275d452d9ebea26c97bfd :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@flags=;badges=subscriber/54,bits/1000;tmi-sent-ts=1704558356619;user-type=;room-id=62300805;client-nonce=88d2ba33098cb57b9056a88f99b815eb;first-msg=0;returning-chatter=0;badge-info=subscriber/55;id=813f93d8-731c-463f-ab81-e52cf52109f9;subscriber=1;user-id=103592036;historical=1;turbo=0;color=#00615C;display-name=SecretCarrot;rm-received-ts=1704558356812;emotes=;mod=0 :secretcarrot!secretcarrot@secretcarrot.tmi.twitch.tv PRIVMSG #nymn AlienDance","@room-id=62300805;badges=bits/100;badge-info=;client-nonce=7ac19d53974bf5449b9230cc2269e0ea;id=9d89000a-edab-4b19-a016-1daa18871134;color=#25E000;subscriber=0;mod=0;historical=1;user-id=63372784;display-name=DM8917;rm-received-ts=1704558357172;tmi-sent-ts=1704558356991;first-msg=0;emotes=;flags=;returning-chatter=0;turbo=0;user-type= :dm8917!dm8917@dm8917.tmi.twitch.tv PRIVMSG #nymn :FEELSWAYTOODONKMAN FEELSWAYTOODONKMAN","@client-nonce=9735f1c95a4d0b7c409aaeae2bddeec6;historical=1;tmi-sent-ts=1704558358704;badges=subscriber/48,bits/25000;badge-info=subscriber/49;emote-only=1;display-name=ME_ME;emotes=555555560:0-1;mod=0;flags=;returning-chatter=0;id=5c74b507-da41-4ffc-8ddd-7c321549f54f;rm-received-ts=1704558358889;user-type=;turbo=0;first-msg=0;user-id=159210800;color=#FF2424;subscriber=1;room-id=62300805 :me_me!me_me@me_me.tmi.twitch.tv PRIVMSG #nymn ::D","@returning-chatter=0;badges=;id=6ea7e5f1-eb19-41b6-905a-cbc270e8fdcb;subscriber=0;tmi-sent-ts=1704558359045;room-id=62300805;flags=;first-msg=0;badge-info=;color=#000000;turbo=0;rm-received-ts=1704558359228;mod=0;user-id=205837377;historical=1;user-type=;display-name=Duchene;emotes= :duchene!duchene@duchene.tmi.twitch.tv PRIVMSG #nymn :\u0001ACTION forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀\u0001","@client-nonce=ee76af044adf14fb9c9577c831f4e81f;flags=;badges=subscriber/42,twitch-recap-2023/1;display-name=MaxThurian;badge-info=subscriber/43;mod=0;user-id=60181947;rm-received-ts=1704558359607;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170;color=#00ED2A;room-id=62300805;historical=1;user-type=;id=6cfb94e0-008a-4f52-857e-35f380e0382d;turbo=0;returning-chatter=0;subscriber=1;first-msg=0;tmi-sent-ts=1704558359422 :maxthurian!maxthurian@maxthurian.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@user-type=;badge-info=;tmi-sent-ts=1704558359614;rm-received-ts=1704558359787;flags=;mod=0;historical=1;emotes=;client-nonce=0f7efa76c59c2b3533efc70f4c053222;color=#FF0000;subscriber=0;user-id=51967700;first-msg=0;id=173d2f93-6008-412f-b91a-da3557f36e62;returning-chatter=0;turbo=0;display-name=Patixxl;badges=;room-id=62300805 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170,176-186,192-202;historical=1;badges=bits/1000;subscriber=0;tmi-sent-ts=1704558359649;user-id=45923155;returning-chatter=0;color=#FF0000;id=082e71d0-d252-4906-aa04-423d1f34267a;user-type=;room-id=62300805;flags=;badge-info=;first-msg=0;rm-received-ts=1704558359840;turbo=0;display-name=HajleSellasje;mod=0 :hajlesellasje!hajlesellasje@hajlesellasje.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@badges=;user-type=;turbo=0;badge-info=;display-name=AAAAUUUUGGGHHHHH;id=fe86f573-63ee-4de5-9104-2c1e8eb348a0;tmi-sent-ts=1704558360129;client-nonce=5c2cb15b0fca07ae322374d60ccce7d5;subscriber=0;user-id=809558302;first-msg=1;emotes=;room-id=62300805;historical=1;color=;rm-received-ts=1704558360358;returning-chatter=0;mod=0;flags= :aaaauuuuggghhhhh!aaaauuuuggghhhhh@aaaauuuuggghhhhh.tmi.twitch.tv PRIVMSG #nymn forsenParty","@returning-chatter=0;badges=subscriber/36,twitch-recap-2023/1;subscriber=1;rm-received-ts=1704558360386;room-id=62300805;user-id=433352132;flags=;turbo=0;color=#63BD68;mod=0;display-name=jontEmillian;emotes=555555560:0-1;first-msg=0;id=72ac6810-3aba-4aa4-af35-f62deba5baa3;historical=1;badge-info=subscriber/38;user-type=;emote-only=1;tmi-sent-ts=1704558360209 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn ::D","@flags=;display-name=ThinMartin;historical=1;returning-chatter=0;rm-received-ts=1704558361281;badges=;tmi-sent-ts=1704558361098;id=ca6717f6-0fbe-4b4d-963b-ddf0ad2bfe14;client-nonce=bb27367ad56e4ba11ca5a27e289bec26;mod=0;turbo=0;subscriber=0;badge-info=;color=#1E90FF;user-type=;emotes=;user-id=108762203;first-msg=0;room-id=62300805 :thinmartin!thinmartin@thinmartin.tmi.twitch.tv PRIVMSG #nymn :what is this game about nymn","@flags=;badge-info=;mod=0;emotes=496:0-1;user-id=37931493;display-name=deever44;badges=no_audio/1;rm-received-ts=1704558361823;subscriber=0;room-id=62300805;user-type=;tmi-sent-ts=1704558361630;first-msg=0;client-nonce=ef857cbe124955a59b99ce514bc0d25f;historical=1;emote-only=1;turbo=0;id=53b18995-698f-45b2-849c-9b0e2fb8a0a0;returning-chatter=0;color= :deever44!deever44@deever44.tmi.twitch.tv PRIVMSG #nymn ::D","@emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170;tmi-sent-ts=1704558362519;id=c8da497d-ef13-4098-b37a-85901d5183df;room-id=62300805;turbo=0;badge-info=subscriber/43;display-name=MaxThurian;user-id=60181947;client-nonce=45392a88fa1b4e9476cac75e0661f80c;badges=subscriber/42,twitch-recap-2023/1;rm-received-ts=1704558362716;flags=;returning-chatter=0;mod=0;historical=1;first-msg=0;color=#00ED2A;subscriber=1;user-type= :maxthurian!maxthurian@maxthurian.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@user-type=;historical=1;display-name=Kotzblitz20;id=9ca12e75-ea6e-45be-89b0-7fab952a4a87;color=#FFFF00;client-nonce=dde3afea3ba6219ca78a92cdab8cdf11;turbo=1;user-id=40037186;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170,176-186;rm-received-ts=1704558363192;first-msg=0;room-id=62300805;flags=;tmi-sent-ts=1704558362999;badges=subscriber/9,turbo/1;subscriber=1;mod=0;returning-chatter=0;badge-info=subscriber/9 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@subscriber=0;returning-chatter=0;user-type=;first-msg=0;emotes=;id=c419a2fa-87db-4dce-98ca-5659c66847aa;tmi-sent-ts=1704558363045;turbo=0;user-id=431946171;historical=1;room-id=62300805;flags=;rm-received-ts=1704558363211;mod=0;badges=no_audio/1;display-name=jonhycrack;color=#008000;badge-info= :jonhycrack!jonhycrack@jonhycrack.tmi.twitch.tv PRIVMSG #nymn Alien360","@first-msg=0;display-name=zzlint;historical=1;id=a668efb4-d9e9-4632-8b5e-8c819ebfe694;returning-chatter=0;flags=;rm-received-ts=1704558364202;client-nonce=f2ea00474929e05b4febde034eb82119;subscriber=0;tmi-sent-ts=1704558364036;user-type=;emotes=emotesv2_2f9a36844b054423833c817b5f8d4225:0-8;badges=;room-id=62300805;emote-only=1;mod=0;turbo=0;user-id=29764188;color=#1E90FF;badge-info= :zzlint!zzlint@zzlint.tmi.twitch.tv PRIVMSG #nymn forsenPls","@tmi-sent-ts=1704558364433;historical=1;badges=;subscriber=0;mod=0;color=#FF0000;user-id=51967700;turbo=0;client-nonce=4f36558386640aa28dc3fb324a5c7d12;id=370efb92-8a63-4135-8f70-de229f6addf1;emotes=;room-id=62300805;display-name=Patixxl;rm-received-ts=1704558364612;user-type=;first-msg=0;flags=;badge-info=;returning-chatter=0 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@first-msg=0;flags=;color=#63BD68;display-name=jontEmillian;badges=subscriber/36,twitch-recap-2023/1;user-type=;tmi-sent-ts=1704558364524;returning-chatter=0;mod=0;id=05140eaf-529a-4cc6-8097-e1e2eba0d739;turbo=0;badge-info=subscriber/38;historical=1;emotes=;subscriber=1;user-id=433352132;room-id=62300805;rm-received-ts=1704558364697 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn FIRSTTIMECHATTER","@user-type=;vip=1;emotes=;room-id=62300805;historical=1;badge-info=subscriber/77;display-name=Joshlad;rm-received-ts=1704558364899;id=d73a3660-035e-404c-a047-60afb03ee8c2;badges=vip/1,subscriber/72,rplace-2023/1;first-msg=0;returning-chatter=0;flags=;user-id=87120320;color=#D52AFF;tmi-sent-ts=1704558364721;mod=0;turbo=0;subscriber=1 :joshlad!joshlad@joshlad.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@emotes=;subscriber=0;badge-info=;badges=;historical=1;turbo=0;color=;client-nonce=b7c80c98a61be823fc9e6cd52b318343;user-id=465513111;tmi-sent-ts=1704558365024;returning-chatter=0;rm-received-ts=1704558365199;id=21fe77af-bcad-434e-87f3-812734423be6;user-type=;mod=0;display-name=mongushmengi;first-msg=0;room-id=62300805;flags= :mongushmengi!mongushmengi@mongushmengi.tmi.twitch.tv PRIVMSG #nymn FeelsDankMan","@id=488b9a38-0cd6-4481-943c-9144ff6b8482;emotes=;returning-chatter=0;tmi-sent-ts=1704558365024;user-id=85837900;client-nonce=8f0a4d1d73ca7f5551cddd44436c98a4;badges=subscriber/36,no_audio/1;user-type=;flags=;badge-info=subscriber/37;historical=1;subscriber=1;display-name=DontCagePlebs;rm-received-ts=1704558365220;turbo=0;color=#DAA520;room-id=62300805;mod=0;first-msg=0 :dontcageplebs!dontcageplebs@dontcageplebs.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@emote-only=1;badge-info=subscriber/49;tmi-sent-ts=1704558365156;returning-chatter=0;badges=subscriber/48,bits/25000;color=#FF2424;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10;id=dea75169-4282-4ec8-a099-8a6b8ec94427;rm-received-ts=1704558365338;flags=;subscriber=1;user-type=;first-msg=0;client-nonce=1468b44e56ba92be4eb5125f843d10bb;historical=1;user-id=159210800;display-name=ME_ME;mod=0;turbo=0;room-id=62300805 :me_me!me_me@me_me.tmi.twitch.tv PRIVMSG #nymn forsenParty","@historical=1;color=#FFFF00;id=344fbcfb-f4ae-467a-84a8-f68fb6af1d30;user-id=40037186;room-id=62300805;subscriber=1;badges=subscriber/9,turbo/1;display-name=Kotzblitz20;turbo=1;rm-received-ts=1704558365404;first-msg=0;returning-chatter=0;flags=;mod=0;client-nonce=025122561a6c8c77c64df491835a1a04;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170,176-186,192-202,208-218,224-234,240-250,256-266,272-282,288-298;tmi-sent-ts=1704558365217;user-type=;badge-info=subscriber/9 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@id=17f02747-5338-411c-9f3c-f0643132cc76;color=#FF69B4;client-nonce=f674b35954ca7cdcb11aae5195bf02dd;subscriber=1;user-type=;turbo=0;rm-received-ts=1704558366319;user-id=22733078;room-id=62300805;display-name=miniwoffer;emotes=;badge-info=subscriber/2;flags=;mod=0;badges=subscriber/0,bits/1;returning-chatter=0;tmi-sent-ts=1704558366132;first-msg=0;historical=1 :miniwoffer!miniwoffer@miniwoffer.tmi.twitch.tv PRIVMSG #nymn FIRSTTIMECHATTER","@mod=0;returning-chatter=0;emotes=;historical=1;id=fa4e238d-9b22-47b2-863c-50b143f4b416;rm-received-ts=1704558366976;turbo=0;room-id=62300805;color=#00615C;flags=;tmi-sent-ts=1704558366792;badges=subscriber/54,bits/1000;user-type=;first-msg=0;user-id=103592036;badge-info=subscriber/55;display-name=SecretCarrot;subscriber=1;client-nonce=29bd60cf782ead864fb0eb764467ebf6 :secretcarrot!secretcarrot@secretcarrot.tmi.twitch.tv PRIVMSG #nymn forsenParty","@badges=;display-name=Patixxl;room-id=62300805;mod=0;historical=1;subscriber=0;badge-info=;id=1420090c-2493-4455-a424-b874c4d54279;user-id=51967700;flags=;client-nonce=36839e4e863025c3f264deb28326a77c;first-msg=0;emotes=;tmi-sent-ts=1704558367404;user-type=;rm-received-ts=1704558367576;turbo=0;returning-chatter=0;color=#FF0000 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@badge-info=;user-id=63372784;color=#25E000;id=c755d6d9-0016-477d-9ad2-d7d6a3d77e56;flags=;tmi-sent-ts=1704558367479;display-name=DM8917;user-type=;room-id=62300805;badges=bits/100;emotes=;turbo=0;historical=1;rm-received-ts=1704558367663;returning-chatter=0;first-msg=0;client-nonce=ccd1fce4d89e59cd82fae61dcf7e487b;mod=0;subscriber=0 :dm8917!dm8917@dm8917.tmi.twitch.tv PRIVMSG #nymn FEELSWAYTOODONKMAN","@id=fb6cc2ee-909e-44bf-ac74-a04ceb10b6f6;returning-chatter=0;user-id=433352132;badges=subscriber/36,twitch-recap-2023/1;tmi-sent-ts=1704558367804;user-type=;room-id=62300805;turbo=0;color=#63BD68;emotes=;badge-info=subscriber/38;mod=0;rm-received-ts=1704558367969;historical=1;first-msg=0;flags=;display-name=jontEmillian;subscriber=1 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn forsenParty","@rm-received-ts=1704558368112;display-name=kfms6741;id=ddaa5215-8178-48fd-bf60-1bb6f939c31d;first-msg=0;returning-chatter=0;badges=subscriber/6,premium/1;user-type=;emote-only=1;room-id=62300805;emotes=emotesv2_10304fc8867a4d3586aadf2c409b153a:0-14,16-30,32-46,48-62,64-78,80-94,96-110,112-126,128-142,144-158,160-174;flags=;badge-info=subscriber/8;tmi-sent-ts=1704558367884;historical=1;turbo=0;client-nonce=d32814a0b0477dc5fe6d6b912302bfe2;color=#DAA520;subscriber=1;user-id=42838116;mod=0 :kfms6741!kfms6741@kfms6741.tmi.twitch.tv PRIVMSG #nymn :forsenPossessed forsenPossessed forsenPossessed forsenPossessed forsenPossessed forsenPossessed forsenPossessed forsenPossessed forsenPossessed forsenPossessed forsenPossessed","@mod=0;room-id=62300805;subscriber=0;rm-received-ts=1704558368229;id=cd0ea73c-5e47-4a58-a399-5ee7c96d0ec4;user-type=;first-msg=0;badges=no_audio/1;tmi-sent-ts=1704558368035;color=#008000;emotes=;returning-chatter=0;badge-info=;user-id=431946171;historical=1;display-name=jonhycrack;turbo=0;flags= :jonhycrack!jonhycrack@jonhycrack.tmi.twitch.tv PRIVMSG #nymn :Alien360 󠀀","@rm-received-ts=1704558369438;badges=subscriber/9,turbo/1;user-id=40037186;turbo=1;tmi-sent-ts=1704558369248;flags=;room-id=62300805;historical=1;client-nonce=2fab6158ec3ac51d21432a431b93c00f;mod=0;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170,176-186,192-202,208-218,224-234;first-msg=0;returning-chatter=0;id=a7ba8c7e-1b69-429b-89b5-af1835e9b97a;color=#FFFF00;display-name=Kotzblitz20;user-type=;subscriber=1;badge-info=subscriber/9 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@flags=;color=#DAA520;display-name=DontCagePlebs;turbo=0;mod=0;user-type=;tmi-sent-ts=1704558369505;subscriber=1;client-nonce=9469406a90ad5f280f79b88f30dadc5c;user-id=85837900;returning-chatter=0;badges=subscriber/36,no_audio/1;room-id=62300805;historical=1;emotes=;rm-received-ts=1704558369684;id=ae6c744b-3d3d-478a-bcc0-96fccdc7d1da;badge-info=subscriber/37;first-msg=0 :dontcageplebs!dontcageplebs@dontcageplebs.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM WUB WUB","@tmi-sent-ts=1704558370015;color=;rm-received-ts=1704558370199;flags=;user-id=37931493;subscriber=0;first-msg=0;client-nonce=db7fff6489ffbc786da8deb99837aa3b;badges=no_audio/1;display-name=deever44;turbo=0;returning-chatter=0;emotes=;mod=0;historical=1;badge-info=;id=5ed675ed-23f7-4984-93a6-b7831b0c02f2;room-id=62300805;user-type= :deever44!deever44@deever44.tmi.twitch.tv PRIVMSG #nymn forsenParty","@flags=;returning-chatter=0;mod=0;turbo=0;color=#00FF7F;id=dc6782e6-a5a2-4c7b-83c5-2daaf93037c0;user-id=154079285;user-type=;tmi-sent-ts=1704558370116;display-name=boogkitty;emotes=;client-nonce=4fcde071cb0f334963a95e5ae5a0422c;badges=subscriber/36;badge-info=subscriber/41;room-id=62300805;rm-received-ts=1704558370283;historical=1;first-msg=0;subscriber=1 :boogkitty!boogkitty@boogkitty.tmi.twitch.tv PRIVMSG #nymn :Ratge RaveTime","@tmi-sent-ts=1704558370903;turbo=0;client-nonce=247098a52c2dac2cc3e94cb17a55006c;id=d3460fc8-92b6-4405-817e-45653c3a59c4;mod=0;badges=;historical=1;rm-received-ts=1704558371071;badge-info=;returning-chatter=0;flags=;user-type=;room-id=62300805;color=#000000;display-name=Duchene;emotes=;first-msg=0;subscriber=0;user-id=205837377 :duchene!duchene@duchene.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@user-id=51967700;room-id=62300805;first-msg=0;display-name=Patixxl;client-nonce=39441701b67f5ac0a09884eefba06273;user-type=;emotes=;tmi-sent-ts=1704558370970;mod=0;rm-received-ts=1704558371141;turbo=0;returning-chatter=0;flags=;color=#FF0000;historical=1;subscriber=0;badge-info=;id=322e2ee6-9c56-49e6-b0b4-a605138e1011;badges= :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@emotes=;badges=;subscriber=0;id=01568b13-f48a-4b51-a289-627211cd5eed;color=;user-id=809558302;room-id=62300805;turbo=0;user-type=;tmi-sent-ts=1704558371094;first-msg=0;returning-chatter=0;flags=;mod=0;display-name=AAAAUUUUGGGHHHHH;rm-received-ts=1704558371274;badge-info=;client-nonce=5c5c027d67228165d9ddb8a1ea194e02;historical=1 :aaaauuuuggghhhhh!aaaauuuuggghhhhh@aaaauuuuggghhhhh.tmi.twitch.tv PRIVMSG #nymn forsendiscosnake","@room-id=62300805;emotes=;user-type=;display-name=jontEmillian;returning-chatter=0;id=84c71c54-04a3-4d2c-89aa-b32c69ac6133;tmi-sent-ts=1704558371190;first-msg=0;user-id=433352132;turbo=0;subscriber=1;badges=subscriber/36,twitch-recap-2023/1;badge-info=subscriber/38;color=#63BD68;historical=1;rm-received-ts=1704558371350;mod=0;flags= :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn :forsenParty 󠀀","@mod=0;color=#008000;user-id=431946171;historical=1;turbo=0;tmi-sent-ts=1704558371652;badges=no_audio/1;first-msg=0;badge-info=;user-type=;emotes=;room-id=62300805;flags=;rm-received-ts=1704558371829;id=d72ba1a2-c593-4ef4-af0f-55d074c2990f;display-name=jonhycrack;returning-chatter=0;subscriber=0 :jonhycrack!jonhycrack@jonhycrack.tmi.twitch.tv PRIVMSG #nymn Alien360","@room-id=62300805;user-id=103592036;tmi-sent-ts=1704558371794;historical=1;badges=subscriber/54,bits/1000;rm-received-ts=1704558371962;user-type=;color=#00615C;emotes=;first-msg=0;id=ddc5934a-ae34-4abc-b08c-9726af73ed53;turbo=0;badge-info=subscriber/55;client-nonce=0bf6d12caa30061e488b645225be2356;flags=;mod=0;display-name=SecretCarrot;returning-chatter=0;subscriber=1 :secretcarrot!secretcarrot@secretcarrot.tmi.twitch.tv PRIVMSG #nymn :forsenParty okay","@id=3bbc0eaa-6b33-475b-8aba-11f6469c1ad1;subscriber=0;client-nonce=db2ffd62e8563204b733a15c9e9fa6d2;room-id=62300805;badge-info=;historical=1;first-msg=0;rm-received-ts=1704558372604;user-id=809558302;turbo=0;color=;tmi-sent-ts=1704558372448;user-type=;display-name=AAAAUUUUGGGHHHHH;flags=;badges=;mod=0;returning-chatter=0;emotes= :aaaauuuuggghhhhh!aaaauuuuggghhhhh@aaaauuuuggghhhhh.tmi.twitch.tv PRIVMSG #nymn :forsendiscosnake 󠀀","@emotes=;id=708911c5-ec5b-4852-942f-a7d0716f7274;flags=;first-msg=0;mod=0;badge-info=;turbo=0;user-id=51967700;room-id=62300805;user-type=;returning-chatter=0;client-nonce=409a64806b507f65db54694a4147e44e;rm-received-ts=1704558373326;subscriber=0;tmi-sent-ts=1704558373153;historical=1;display-name=Patixxl;badges=;color=#FF0000 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@vip=1;display-name=ImDaxify;subscriber=1;room-id=62300805;turbo=0;emotes=;color=#1F8FFF;client-nonce=199b15033cf2c3564c8ebf0c60066bb4;mod=0;flags=0-3:P.0;id=45d383d0-d9e3-4b47-88d7-406e2e5ce616;badges=vip/1,subscriber/36,twitch-recap-2023/1;user-id=28317294;badge-info=subscriber/36;tmi-sent-ts=1704558373446;returning-chatter=0;historical=1;user-type=;rm-received-ts=1704558373635;first-msg=0 :imdaxify!imdaxify@imdaxify.tmi.twitch.tv PRIVMSG #nymn :damn rats are having raves in the sewers? forsenParty","@turbo=0;flags=;mod=0;historical=1;badges=;user-type=;room-id=62300805;client-nonce=0643afa2de7aea8ebe1a38b0f6e07990;badge-info=;emotes=;rm-received-ts=1704558375049;user-id=38870532;first-msg=0;subscriber=0;id=49ec6e5d-6421-4d96-ba87-e980658fced2;returning-chatter=0;tmi-sent-ts=1704558374871;color=#D2691E;display-name=Nopem8 :nopem8!nopem8@nopem8.tmi.twitch.tv PRIVMSG #nymn :forsenParty sewer rave","@tmi-sent-ts=1704558375413;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10;returning-chatter=0;display-name=Kotzblitz20;subscriber=1;flags=;turbo=1;client-nonce=977a320ab81516105211894b8feba859;first-msg=0;rm-received-ts=1704558375586;color=#FFFF00;mod=0;badges=subscriber/9,turbo/1;user-type=;emote-only=1;historical=1;user-id=40037186;room-id=62300805;badge-info=subscriber/9;id=aaa3b65e-cfaf-415f-926c-1d18efbb73e5 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn forsenParty","@client-nonce=693d50d27c1b9a409aef6f90199f1e59;subscriber=1;returning-chatter=0;rm-received-ts=1704558376011;room-id=62300805;color=#00615C;tmi-sent-ts=1704558375845;first-msg=0;user-type=;emotes=;turbo=0;badge-info=subscriber/55;display-name=SecretCarrot;badges=subscriber/54,bits/1000;user-id=103592036;flags=;id=afa046c8-a267-4c18-bd74-96272de0930f;mod=0;historical=1 :secretcarrot!secretcarrot@secretcarrot.tmi.twitch.tv PRIVMSG #nymn forsenUnpleased","@turbo=0;mod=0;emotes=;subscriber=1;display-name=DontCagePlebs;rm-received-ts=1704558376248;badges=subscriber/36,no_audio/1;tmi-sent-ts=1704558376069;flags=;color=#DAA520;id=316b3840-627d-4b8d-ab34-150e3c1d9220;badge-info=subscriber/37;user-id=85837900;first-msg=0;room-id=62300805;user-type=;client-nonce=01dd8d81dc08f242212957ed986b3ad4;returning-chatter=0;historical=1 :dontcageplebs!dontcageplebs@dontcageplebs.tmi.twitch.tv PRIVMSG #nymn !","@flags=;rm-received-ts=1704558376981;tmi-sent-ts=1704558376822;badges=vip/1,subscriber/72,rplace-2023/1;display-name=Joshlad;historical=1;mod=0;user-type=;subscriber=1;user-id=87120320;emotes=;vip=1;id=6a192486-acf9-418b-8178-ddb5fef6bd9a;color=#D52AFF;turbo=0;room-id=62300805;first-msg=0;badge-info=subscriber/77;returning-chatter=0 :joshlad!joshlad@joshlad.tmi.twitch.tv PRIVMSG #nymn !","@first-msg=0;rm-received-ts=1704558377310;emotes=;display-name=Patixxl;user-id=51967700;mod=0;tmi-sent-ts=1704558377136;color=#FF0000;subscriber=0;user-type=;historical=1;id=959f1371-ce15-4aa8-a0ea-ee938f8825e3;badge-info=;flags=;turbo=0;room-id=62300805;badges=;client-nonce=e5cae5d7c13f5836a688e58c6eb5cce3;returning-chatter=0 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn forsenUnpleased","@rm-received-ts=1704558377798;tmi-sent-ts=1704558377630;user-id=29649547;color=#FF7F50;emotes=;room-id=62300805;badge-info=subscriber/53;mod=0;historical=1;returning-chatter=0;first-msg=0;user-type=;display-name=orange_bean;badges=subscriber/48;subscriber=1;id=fe19a439-fe4c-4ea5-a8c6-b9ac4842803e;flags=;turbo=0 :orange_bean!orange_bean@orange_bean.tmi.twitch.tv PRIVMSG #nymn SirO","@id=3c39c45c-6368-439d-b903-c94b90b669c5;badge-info=subscriber/37;historical=1;user-id=85837900;returning-chatter=0;user-type=;first-msg=0;client-nonce=0e7c769903d362944364dd51c6b3de1a;rm-received-ts=1704558379370;emotes=;badges=subscriber/36,no_audio/1;room-id=62300805;mod=0;display-name=DontCagePlebs;tmi-sent-ts=1704558379200;turbo=0;color=#DAA520;flags=;subscriber=1 :dontcageplebs!dontcageplebs@dontcageplebs.tmi.twitch.tv PRIVMSG #nymn ❗","@user-type=;color=#FF69B4;room-id=62300805;rm-received-ts=1704558380831;emotes=;subscriber=0;id=10e1772b-89ee-49ac-be18-4cee9267194d;turbo=0;first-msg=0;badge-info=;returning-chatter=0;badges=;mod=0;flags=;tmi-sent-ts=1704558380608;historical=1;display-name=ehtia;user-id=163155934;client-nonce=fb8f9e92b80977381fb46802fa727a73 :ehtia!ehtia@ehtia.tmi.twitch.tv PRIVMSG #nymn :PagMan quests","@room-id=62300805;subscriber=1;user-type=;mod=0;display-name=Kotzblitz20;client-nonce=cb07969de0f4c182a16accf1433a095f;badge-info=subscriber/9;color=#FFFF00;flags=;id=e4471904-9412-4312-a066-fdcd478754cf;first-msg=0;user-id=40037186;returning-chatter=0;turbo=1;emotes=;tmi-sent-ts=1704558380995;badges=subscriber/9,turbo/1;historical=1;rm-received-ts=1704558381200 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn AlienSilly","@turbo=0;user-id=63372784;emotes=;rm-received-ts=1704558381262;tmi-sent-ts=1704558381095;display-name=DM8917;flags=;first-msg=0;color=#25E000;badge-info=;historical=1;user-type=;client-nonce=9c19bcb5fc67194644456dfcb342aab5;subscriber=0;mod=0;id=c1361ed8-acea-4688-86bb-904fb3090956;badges=bits/100;room-id=62300805;returning-chatter=0 :dm8917!dm8917@dm8917.tmi.twitch.tv PRIVMSG #nymn docJAMMER","@flags=;mod=0;returning-chatter=0;user-id=431946171;tmi-sent-ts=1704558381594;badges=no_audio/1;room-id=62300805;rm-received-ts=1704558381765;first-msg=0;turbo=0;subscriber=0;badge-info=;display-name=jonhycrack;historical=1;color=#008000;emotes=;user-type=;id=02be0145-6e23-4f62-bd6a-2810bbf548d0 :jonhycrack!jonhycrack@jonhycrack.tmi.twitch.tv PRIVMSG #nymn AlienUnpleased","@tmi-sent-ts=1704558381879;room-id=62300805;historical=1;display-name=jontEmillian;first-msg=0;badges=subscriber/36,twitch-recap-2023/1;badge-info=subscriber/38;flags=;color=#63BD68;turbo=0;subscriber=1;user-id=433352132;id=bd423c09-7ee8-452f-b642-c12f5fe83bff;mod=0;emotes=;rm-received-ts=1704558382052;returning-chatter=0;user-type= :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn TooMuchWork","@subscriber=1;user-id=80542722;badge-info=subscriber/2;returning-chatter=0;user-type=;badges=subscriber/0,no_video/1;mod=0;historical=1;room-id=62300805;color=#00FF7F;display-name=jqxlol;flags=;emotes=;first-msg=0;tmi-sent-ts=1704558383507;client-nonce=ac69ce3c4618bd5b7e256dcda914ec69;rm-received-ts=1704558383692;turbo=0;id=410f2a70-2c0c-4441-814b-24ac9bf8d27f :jqxlol!jqxlol@jqxlol.tmi.twitch.tv PRIVMSG #nymn :Okayeg 💢 MUZIKA ? Okayeg 💢 MUZIKA ? Okayeg 💢 MUZIKA ? Okayeg 💢 MUZIKA ? Okayeg 💢 MUZIKA ? Okayeg 💢 MUZIKA ?","@user-type=;tmi-sent-ts=1704558384584;client-nonce=e87238020cdce65f7c6aff3abb3672f2;first-msg=0;rm-received-ts=1704558384776;returning-chatter=0;display-name=Kotzblitz20;user-id=40037186;emote-only=1;room-id=62300805;turbo=1;badge-info=subscriber/9;historical=1;color=#FFFF00;badges=subscriber/9,turbo/1;emotes=emotesv2_2f9a36844b054423833c817b5f8d4225:0-8;id=335b5b8c-5a03-4872-974a-0ddcb0952544;mod=0;subscriber=1;flags= :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn forsenPls","@turbo=0;color=#0000FF;rm-received-ts=1704558389403;emotes=1:20-21;badge-info=;room-id=62300805;first-msg=0;mod=0;flags=;tmi-sent-ts=1704558389190;user-type=;display-name=OfficialScrap;returning-chatter=0;user-id=85115603;subscriber=0;historical=1;badges=no_audio/1;client-nonce=e68344caf9846b4f5938496312f47ba0;id=f9cee817-2a5e-4443-8702-3be26c7af6e7 :officialscrap!officialscrap@officialscrap.tmi.twitch.tv PRIVMSG #nymn :stockholm simulator :)","@tmi-sent-ts=1704558390211;id=ef67b2bd-3b45-4608-b830-1a919fdfd8cb;first-msg=0;emotes=;turbo=0;badges=;badge-info=;user-id=465513111;color=;returning-chatter=0;display-name=mongushmengi;historical=1;user-type=;rm-received-ts=1704558390397;client-nonce=dd400c92f0ab3b3de975bd3481033bd0;mod=0;subscriber=0;flags=;room-id=62300805 :mongushmengi!mongushmengi@mongushmengi.tmi.twitch.tv PRIVMSG #nymn AlienPls","@room-id=62300805;subscriber=0;badge-info=;rm-received-ts=1704558397501;mod=0;tmi-sent-ts=1704558397328;first-msg=0;badges=no_audio/1;user-id=431946171;returning-chatter=0;turbo=0;user-type=;emotes=;color=#008000;historical=1;flags=;id=1c217b6a-f1da-4cb1-a209-5aa0bd364a8a;display-name=jonhycrack :jonhycrack!jonhycrack@jonhycrack.tmi.twitch.tv PRIVMSG #nymn Pepege","@id=f3ea2bda-0697-4cba-9163-732d04c2f707;client-nonce=774059e4111bb61a6cecc4f73ea521fd;tmi-sent-ts=1704558398455;subscriber=0;returning-chatter=0;display-name=deever44;historical=1;badge-info=;rm-received-ts=1704558398627;mod=0;room-id=62300805;user-type=;color=;emotes=;badges=no_audio/1;turbo=0;user-id=37931493;flags=;first-msg=0 :deever44!deever44@deever44.tmi.twitch.tv PRIVMSG #nymn whoooooos","@rm-received-ts=1704558398738;user-type=;turbo=1;badge-info=subscriber/9;mod=0;returning-chatter=0;emotes=;id=e9161872-1113-4b84-b6ba-d55a782df44d;user-id=40037186;client-nonce=51fade5e48119229d8db957bdb09628e;first-msg=0;tmi-sent-ts=1704558398564;display-name=Kotzblitz20;badges=subscriber/9,turbo/1;subscriber=1;room-id=62300805;historical=1;flags=;color=#FFFF00 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn AlienJam","@subscriber=1;display-name=SecretCarrot;historical=1;color=#00615C;user-id=103592036;mod=0;emotes=;badges=subscriber/54,bits/1000;turbo=0;client-nonce=4ff419686be13ef2f858704f2eaa62bd;id=839aee9d-ba6a-4cdc-9e5a-8d59772e26e0;flags=;rm-received-ts=1704558401080;first-msg=0;user-type=;tmi-sent-ts=1704558400912;returning-chatter=0;badge-info=subscriber/55;room-id=62300805 :secretcarrot!secretcarrot@secretcarrot.tmi.twitch.tv PRIVMSG #nymn Sludge","@badges=subscriber/36,no_audio/1;mod=0;id=7a791d78-b2ec-4b8c-b982-c3640f2bad71;display-name=DontCagePlebs;user-type=;flags=;room-id=62300805;first-msg=0;client-nonce=c039c0e152302e9ace32c39848d7fa97;emotes=;rm-received-ts=1704558402227;tmi-sent-ts=1704558402057;badge-info=subscriber/37;turbo=0;user-id=85837900;color=#DAA520;historical=1;returning-chatter=0;subscriber=1 :dontcageplebs!dontcageplebs@dontcageplebs.tmi.twitch.tv PRIVMSG #nymn Sludge","@turbo=0;id=d32c6052-2379-485a-8df9-192ccfdbbbcf;mod=0;returning-chatter=0;first-msg=0;flags=;subscriber=0;color=#008000;badges=glhf-pledge/1;display-name=elareldan;emotes=;user-type=;historical=1;room-id=62300805;tmi-sent-ts=1704558405669;user-id=551027178;badge-info=;rm-received-ts=1704558405857 :elareldan!elareldan@elareldan.tmi.twitch.tv PRIVMSG #nymn Sludge","@user-id=63372784;tmi-sent-ts=1704558405944;display-name=DM8917;room-id=62300805;client-nonce=6a9189de1aea2f00a96e12abd80d23ae;user-type=;flags=;emotes=;badge-info=;color=#25E000;id=e67fcc8f-706a-4ef6-8961-233253d17b5f;historical=1;mod=0;badges=bits/100;turbo=0;subscriber=0;rm-received-ts=1704558406134;returning-chatter=0;first-msg=0 :dm8917!dm8917@dm8917.tmi.twitch.tv PRIVMSG #nymn Sludge","@flags=;room-id=62300805;display-name=crunch_sack;rm-received-ts=1704558406247;id=6e127af0-fc9e-46df-a062-2b05473edd68;badges=;user-id=108648666;first-msg=0;subscriber=0;badge-info=;turbo=0;tmi-sent-ts=1704558406091;mod=0;user-type=;emotes=;color=#7AF4FA;returning-chatter=0;historical=1 :crunch_sack!crunch_sack@crunch_sack.tmi.twitch.tv PRIVMSG #nymn :Sludge 󠀀","@first-msg=0;display-name=Kotzblitz20;returning-chatter=0;badges=subscriber/9,turbo/1;mod=0;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10;subscriber=1;flags=;tmi-sent-ts=1704558410701;historical=1;turbo=1;user-id=40037186;room-id=62300805;rm-received-ts=1704558410880;client-nonce=3cbaf6ea472d69fdec6095e2923eca46;badge-info=subscriber/9;user-type=;color=#FFFF00;id=15b0b643-edce-4507-bdda-174c6fd288af :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM","@display-name=Joshlad;emotes=;returning-chatter=0;mod=0;subscriber=1;vip=1;first-msg=0;color=#D52AFF;badge-info=subscriber/77;flags=;rm-received-ts=1704558411416;tmi-sent-ts=1704558411233;user-id=87120320;id=0752c857-93c1-4c58-b99a-9b960016c92b;user-type=;historical=1;badges=vip/1,subscriber/72,rplace-2023/1;room-id=62300805;turbo=0 :joshlad!joshlad@joshlad.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@color=#00ED2A;turbo=0;room-id=62300805;flags=;user-type=;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170;rm-received-ts=1704558412297;id=730a63cf-c99b-4b8d-a74c-e9e4cb38be82;badge-info=subscriber/43;mod=0;user-id=60181947;tmi-sent-ts=1704558412097;client-nonce=0386c8d1fb756dd175acfce21445bd66;first-msg=0;badges=subscriber/42,twitch-recap-2023/1;display-name=MaxThurian;subscriber=1;returning-chatter=0;historical=1 :maxthurian!maxthurian@maxthurian.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@subscriber=1;flags=;color=#63BD68;tmi-sent-ts=1704558412718;emotes=;badges=subscriber/36,twitch-recap-2023/1;id=a3da0e60-8900-45bd-a7b2-4adfab4b89a9;returning-chatter=0;first-msg=0;display-name=jontEmillian;user-id=433352132;historical=1;mod=0;room-id=62300805;user-type=;badge-info=subscriber/38;turbo=0;rm-received-ts=1704558412896 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn forsenParty","@user-id=85837900;rm-received-ts=1704558413016;emotes=;badges=subscriber/36,no_audio/1;first-msg=0;user-type=;mod=0;flags=;color=#DAA520;badge-info=subscriber/37;display-name=DontCagePlebs;id=3ea2910e-b86b-4ca0-995f-572ef2cbef26;room-id=62300805;client-nonce=fd866f5cee610eb23d566141735eb289;tmi-sent-ts=1704558412815;turbo=0;returning-chatter=0;historical=1;subscriber=1 :dontcageplebs!dontcageplebs@dontcageplebs.tmi.twitch.tv PRIVMSG #nymn :frooit fly Okayeg","@user-id=51967700;display-name=Patixxl;mod=0;flags=;first-msg=0;room-id=62300805;returning-chatter=0;id=3b5fa772-aa5e-4ed9-ba09-f2f420ee5a68;turbo=0;emotes=;badges=;rm-received-ts=1704558413521;badge-info=;client-nonce=091e8189c0aae8021a40333b1153a69a;user-type=;historical=1;tmi-sent-ts=1704558413331;subscriber=0;color=#FF0000 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@room-id=62300805;mod=0;color=#FFFF00;badge-info=subscriber/9;tmi-sent-ts=1704558414933;user-id=40037186;display-name=Kotzblitz20;subscriber=1;badges=subscriber/9,turbo/1;id=767aa364-c94b-4f12-ad6f-689978277329;flags=;turbo=1;user-type=;historical=1;first-msg=0;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170,176-186;client-nonce=d0b2bbcc39604d7657b7a3001723269f;rm-received-ts=1704558415116;returning-chatter=0 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@id=e67887b3-a289-4993-8073-949d97b95772;first-msg=0;rm-received-ts=1704558415629;tmi-sent-ts=1704558415459;color=#FF0000;room-id=62300805;emotes=;returning-chatter=0;turbo=0;flags=;badges=;user-id=51967700;mod=0;client-nonce=530fd0a7fdc8ffdf0ccabb12a47143dd;subscriber=0;historical=1;display-name=Patixxl;user-type=;badge-info= :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@first-msg=0;color=#0000FF;mod=0;badges=bits-charity/1;user-type=;id=e9430551-8ab6-47c6-a06d-f3dc497ce053;flags=;turbo=0;tmi-sent-ts=1704558416737;badge-info=;historical=1;display-name=Intel_power;rm-received-ts=1704558416928;client-nonce=d16bda697071b21393ca96858bbdb314;subscriber=0;user-id=103665668;emotes=;room-id=62300805;returning-chatter=0 :intel_power!intel_power@intel_power.tmi.twitch.tv PRIVMSG #nymn RaveTime","@first-msg=0;rm-received-ts=1704558417101;historical=1;user-type=;room-id=62300805;returning-chatter=0;user-id=85837900;badge-info=subscriber/37;id=23b87d89-9310-42ea-ab76-1846fca913fc;color=#DAA520;mod=0;display-name=DontCagePlebs;emotes=;tmi-sent-ts=1704558416910;client-nonce=2ea6d32107a02bcb8ac136fbece3f214;turbo=0;flags=;badges=subscriber/36,no_audio/1;subscriber=1 :dontcageplebs!dontcageplebs@dontcageplebs.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@user-type=;turbo=0;historical=1;subscriber=1;rm-received-ts=1704558417121;display-name=e7om;user-id=423574282;color=#D2FFD6;first-msg=0;badges=subscriber/9,gold-pixel-heart/1;emotes=;id=64072860-b15c-49a2-9d4a-ad7ad48b33a1;tmi-sent-ts=1704558416904;badge-info=subscriber/9;returning-chatter=0;mod=0;room-id=62300805;flags= :e7om!e7om@e7om.tmi.twitch.tv PRIVMSG #nymn WaterIceSaltAyy","@id=57f13cad-a098-4a24-9689-7cef1b3644c1;mod=0;color=#000000;rm-received-ts=1704558417351;badge-info=;returning-chatter=0;user-type=;badges=;first-msg=0;tmi-sent-ts=1704558417177;emotes=;display-name=Duchene;historical=1;flags=;room-id=62300805;turbo=0;subscriber=0;user-id=205837377 :duchene!duchene@duchene.tmi.twitch.tv PRIVMSG #nymn :\u0001ACTION forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀\u0001","@user-id=51967700;flags=;tmi-sent-ts=1704558417249;historical=1;subscriber=0;id=9157f2fc-42df-48fa-87e6-9cdb3ebc1886;client-nonce=ca72fb80f4137e77af26078fcd88e6c2;emotes=;badge-info=;user-type=;display-name=Patixxl;room-id=62300805;rm-received-ts=1704558417426;color=#FF0000;badges=;first-msg=0;turbo=0;mod=0;returning-chatter=0 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@badges=vip/1,subscriber/72,rplace-2023/1;badge-info=subscriber/77;historical=1;user-id=87120320;tmi-sent-ts=1704558418358;room-id=62300805;returning-chatter=0;turbo=0;emotes=;color=#D52AFF;id=f5acf77f-3c03-4fe2-9778-82eff8b09f4e;user-type=;rm-received-ts=1704558418539;first-msg=0;vip=1;mod=0;flags=;display-name=Joshlad;subscriber=1 :joshlad!joshlad@joshlad.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@subscriber=0;returning-chatter=0;first-msg=0;turbo=0;room-id=62300805;historical=1;display-name=steviehem;badge-info=;user-type=;user-id=52076774;client-nonce=9e9135359bc631f279e71ec996e6c9e5;color=#FF4500;tmi-sent-ts=1704558418533;rm-received-ts=1704558418737;flags=;mod=0;emotes=;id=3786d624-2b8f-4524-8add-4a0d3947e1a8;badges=premium/1 :steviehem!steviehem@steviehem.tmi.twitch.tv PRIVMSG #nymn :bros sippin on that lean","@badges=;flags=;subscriber=0;id=de0dd8cb-404e-4b31-b6b7-0946e7c9c237;first-msg=0;turbo=0;user-id=51967700;badge-info=;rm-received-ts=1704558419340;tmi-sent-ts=1704558419159;mod=0;emotes=;client-nonce=7ea293fe9bf65c6f77e2214e65646e47;user-type=;color=#FF0000;room-id=62300805;display-name=Patixxl;historical=1;returning-chatter=0 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@color=#B22222;client-nonce=c7390d07e8a633322be7bc360e39d2f7;returning-chatter=0;display-name=crazyjuni0r_;mod=0;room-id=62300805;subscriber=1;rm-received-ts=1704558419366;turbo=0;historical=1;id=b2b871a9-92dc-4695-9eff-8e6489a260d4;user-id=222340799;emotes=;first-msg=0;tmi-sent-ts=1704558419165;flags=;badges=subscriber/6,chatter-cs-go-2022/1;badge-info=subscriber/7;user-type= :crazyjuni0r_!crazyjuni0r_@crazyjuni0r_.tmi.twitch.tv PRIVMSG #nymn :!#showemote Hypnime","@badges=subscriber/36,twitch-recap-2023/1;returning-chatter=0;color=#63BD68;tmi-sent-ts=1704558419663;room-id=62300805;emotes=;flags=;rm-received-ts=1704558419862;user-type=;id=b8a16ee8-b7b1-4aef-9d95-824fb77ccde3;mod=0;turbo=0;display-name=jontEmillian;badge-info=subscriber/38;subscriber=1;user-id=433352132;first-msg=0;historical=1 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn :forsenParty 󠀀","@tmi-sent-ts=1704558422722;badges=subscriber/9,turbo/1;id=1cf97cdf-766e-4b94-8bb9-cb48e31f8c6d;historical=1;rm-received-ts=1704558422911;badge-info=subscriber/9;subscriber=1;room-id=62300805;display-name=Kotzblitz20;mod=0;color=#FFFF00;client-nonce=750e7553dd45b005b130474c0cc6e63f;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170,176-186,192-202,208-218,224-234;turbo=1;user-id=40037186;first-msg=0;user-type=;flags=;returning-chatter=0 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@room-id=62300805;flags=;first-msg=0;mod=0;user-type=;display-name=h_h410;badges=subscriber/54,chatter-cs-go-2022/1;badge-info=subscriber/54;color=#00FF7F;subscriber=1;id=283e35d7-01c8-419c-9cb7-cd265391d480;tmi-sent-ts=1704558423061;historical=1;rm-received-ts=1704558423252;turbo=0;returning-chatter=0;emotes=;user-id=117088592 :h_h410!h_h410@h_h410.tmi.twitch.tv PRIVMSG #nymn RoyaltE","@badge-info=subscriber/43;flags=;color=#00ED2A;user-id=60181947;turbo=0;rm-received-ts=1704558423958;emotes=;mod=0;user-type=;badges=subscriber/42,twitch-recap-2023/1;historical=1;returning-chatter=0;first-msg=0;display-name=MaxThurian;room-id=62300805;subscriber=1;id=717da12d-1d6e-4821-aa3e-e5033d9e6e18;tmi-sent-ts=1704558423766;client-nonce=0c82f8884db705e2667f1273bdc8d6c1 :maxthurian!maxthurian@maxthurian.tmi.twitch.tv PRIVMSG #nymn monkaOMEGA","@returning-chatter=0;display-name=Joshlad;subscriber=1;turbo=0;badges=vip/1,subscriber/72,rplace-2023/1;tmi-sent-ts=1704558424024;color=#D52AFF;first-msg=0;user-type=;id=a8049204-1665-4931-863f-c8cac534fe3a;mod=0;user-id=87120320;emotes=;historical=1;rm-received-ts=1704558424178;flags=;room-id=62300805;badge-info=subscriber/77;vip=1 :joshlad!joshlad@joshlad.tmi.twitch.tv PRIVMSG #nymn monkaOMEGA","@flags=;turbo=1;user-id=40037186;tmi-sent-ts=1704558424859;historical=1;id=c4b8b5dd-6354-4953-a8cf-287df5e609e1;badges=subscriber/9,turbo/1;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170,176-186,192-202,208-218,224-234,240-250,256-266,272-282;rm-received-ts=1704558425068;mod=0;color=#FFFF00;returning-chatter=0;user-type=;first-msg=0;subscriber=1;room-id=62300805;display-name=Kotzblitz20;client-nonce=0f8a56f507129632f3ed2648089bedb3;badge-info=subscriber/9 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@turbo=0;historical=1;rm-received-ts=1704558425119;color=#00FF7F;badges=subscriber/36;display-name=boogkitty;client-nonce=25791c4eecb885bfaff45840ed83bfe6;flags=;tmi-sent-ts=1704558424954;first-msg=0;id=9c8bd5b7-317d-48fb-8380-fff872c6a6bb;returning-chatter=0;badge-info=subscriber/41;user-type=;room-id=62300805;mod=0;emotes=;user-id=154079285;subscriber=1 :boogkitty!boogkitty@boogkitty.tmi.twitch.tv PRIVMSG #nymn :steam page says there is Twitch Integration for this Nymn but it doesnt work atm","@client-nonce=8d6066f0ce273965822e8b5c66473755;returning-chatter=0;id=b1f04f97-02e1-4c3f-8c30-ba230361cf0e;flags=;display-name=Intel_power;emotes=;subscriber=0;first-msg=0;badges=bits-charity/1;rm-received-ts=1704558425157;historical=1;user-type=;badge-info=;room-id=62300805;mod=0;tmi-sent-ts=1704558424988;turbo=0;user-id=103665668;color=#0000FF :intel_power!intel_power@intel_power.tmi.twitch.tv PRIVMSG #nymn :RAT KING","@returning-chatter=0;flags=;user-id=63372784;room-id=62300805;subscriber=0;turbo=0;color=#25E000;tmi-sent-ts=1704558425088;display-name=DM8917;client-nonce=694e988f6340df19650c339a586933aa;badges=bits/100;first-msg=0;historical=1;id=bf26a00e-9057-4719-9d3b-718631c3c325;rm-received-ts=1704558425268;mod=0;user-type=;badge-info=;emotes= :dm8917!dm8917@dm8917.tmi.twitch.tv PRIVMSG #nymn FEELSWAYTOODONKMAN","@client-nonce=7bf94ad9a226eb86418e4822ebada0ce;emotes=;color=#DAA520;user-type=;flags=;badges=subscriber/36,no_audio/1;turbo=0;rm-received-ts=1704558425924;badge-info=subscriber/37;room-id=62300805;display-name=DontCagePlebs;first-msg=0;subscriber=1;mod=0;historical=1;id=b6a9fd22-acf0-4cbe-9b48-c85b0569be58;tmi-sent-ts=1704558425740;returning-chatter=0;user-id=85837900 :dontcageplebs!dontcageplebs@dontcageplebs.tmi.twitch.tv PRIVMSG #nymn headBang","@mod=0;flags=;emotes=;badges=;room-id=62300805;rm-received-ts=1704558426204;badge-info=;user-type=;first-msg=0;historical=1;color=#FF0000;tmi-sent-ts=1704558426032;id=f84c9dfd-9b46-42e7-9992-eedc8fad5d05;subscriber=0;user-id=51967700;turbo=0;returning-chatter=0;client-nonce=fdf40be9235f4c0a490b546e4e23f896;display-name=Patixxl :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn OOOO","@historical=1;room-id=62300805;subscriber=1;user-type=;flags=;id=ca6f57b8-8fce-46ad-b165-fe518b0df503;returning-chatter=0;emotes=;first-msg=0;mod=0;turbo=0;user-id=433352132;display-name=jontEmillian;color=#63BD68;tmi-sent-ts=1704558426038;badges=subscriber/36,twitch-recap-2023/1;rm-received-ts=1704558426212;badge-info=subscriber/38 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn headBang","@subscriber=0;flags=;client-nonce=6d590cea6f146a932d6fbf694f0c5428;returning-chatter=0;id=b9e2e91b-f558-44f3-8092-7d6d928b44c7;historical=1;user-id=167633177;badge-info=;tmi-sent-ts=1704558426532;display-name=ALotOfChickens;room-id=62300805;user-type=;mod=0;emotes=;first-msg=0;turbo=0;badges=twitch-recap-2023/1;rm-received-ts=1704558426712;color=#10E2E2 :alotofchickens!alotofchickens@alotofchickens.tmi.twitch.tv PRIVMSG #nymn monkaOMEGA","@emotes=;color=#FF2424;subscriber=1;client-nonce=5ea1a329a2599f327bc1aa8aa75590bf;badges=subscriber/48,bits/25000;tmi-sent-ts=1704558427278;room-id=62300805;returning-chatter=0;user-id=159210800;badge-info=subscriber/49;id=f16ec133-6105-4561-b782-c0f6a1e64b2a;historical=1;first-msg=0;turbo=0;rm-received-ts=1704558427465;mod=0;display-name=ME_ME;flags=;user-type= :me_me!me_me@me_me.tmi.twitch.tv PRIVMSG #nymn Ratge","@id=bab41921-40de-47a1-9cc5-bb1dfaa62bc1;tmi-sent-ts=1704558427583;client-nonce=599cc4f7fca1b4f668c3821c171f0b4c;returning-chatter=0;badges=;color=#000000;mod=0;emotes=;subscriber=0;first-msg=0;display-name=Duchene;rm-received-ts=1704558427760;user-type=;user-id=205837377;turbo=0;badge-info=;room-id=62300805;flags=;historical=1 :duchene!duchene@duchene.tmi.twitch.tv PRIVMSG #nymn monkaOMEGA","@subscriber=1;rm-received-ts=1704558427952;color=#00ED2A;client-nonce=d485bfb69a1dd0bb845ca96cf59eb90d;id=2db108d3-886e-4b9e-bb30-41a6a62591cb;badges=subscriber/42,twitch-recap-2023/1;tmi-sent-ts=1704558427776;user-type=;first-msg=0;mod=0;room-id=62300805;emotes=;user-id=60181947;badge-info=subscriber/43;flags=;display-name=MaxThurian;historical=1;turbo=0;returning-chatter=0 :maxthurian!maxthurian@maxthurian.tmi.twitch.tv PRIVMSG #nymn :monkaOMEGA he makes all the rules","@client-nonce=25f894511b804503b35a7d20b9ff8ada;id=bcbba0b7-c32c-479a-83d1-3cf7cbc740d3;user-id=38870532;flags=;badge-info=;historical=1;first-msg=0;rm-received-ts=1704558428105;emotes=;room-id=62300805;mod=0;subscriber=0;display-name=Nopem8;color=#D2691E;tmi-sent-ts=1704558427920;user-type=;badges=;turbo=0;returning-chatter=0 :nopem8!nopem8@nopem8.tmi.twitch.tv PRIVMSG #nymn docPls","@rm-received-ts=1704558428531;id=e6610cff-7d97-439e-91fe-ba654cfc5e7e;user-type=;historical=1;first-msg=0;tmi-sent-ts=1704558428330;room-id=62300805;flags=;client-nonce=7159cd630ec3d2b7f46c37813cfb9796;subscriber=1;badge-info=subscriber/9;returning-chatter=0;user-id=40037186;mod=0;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170,176-186,192-202,208-218,224-234,240-250,256-266,272-282,288-298;display-name=Kotzblitz20;color=#FFFF00;turbo=1;badges=subscriber/9,turbo/1 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@first-msg=0;subscriber=0;user-type=;turbo=0;color=#25E000;client-nonce=83a6399c68ba947d0ca5a56dfae7342d;returning-chatter=0;room-id=62300805;display-name=DM8917;user-id=63372784;historical=1;id=c9caafce-0fb9-4b56-b350-f43a5ff42df3;badges=bits/100;mod=0;badge-info=;emotes=;tmi-sent-ts=1704558428905;rm-received-ts=1704558429109;flags= :dm8917!dm8917@dm8917.tmi.twitch.tv PRIVMSG #nymn :FEELSWAYTOODONKMAN FEELSWAYTOODONKMAN FEELSWAYTOODONKMAN FEELSWAYTOODONKMAN","@color=#63BD68;rm-received-ts=1704558429428;tmi-sent-ts=1704558429273;historical=1;room-id=62300805;user-id=433352132;id=4408b7f2-56f8-4c9b-b84a-4f8f3b48e5cf;display-name=jontEmillian;turbo=0;subscriber=1;first-msg=0;flags=;emotes=;mod=0;user-type=;returning-chatter=0;badge-info=subscriber/38;badges=subscriber/36,twitch-recap-2023/1 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn :headBang 󠀀","@client-nonce=cdf08621359979e40b3e353536fca118;badge-info=;historical=1;turbo=0;user-id=51967700;emotes=;flags=;color=#FF0000;id=ffdda8e5-670c-4b56-93f5-3cee6b61453e;badges=;returning-chatter=0;display-name=Patixxl;first-msg=0;rm-received-ts=1704558429444;subscriber=0;mod=0;room-id=62300805;user-type=;tmi-sent-ts=1704558429258 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@subscriber=0;user-id=163155934;color=#FF69B4;tmi-sent-ts=1704558429961;badges=;rm-received-ts=1704558430134;room-id=62300805;badge-info=;id=87a2822c-09de-4d40-88b5-be0ca8882a4f;first-msg=0;mod=0;flags=;historical=1;turbo=0;user-type=;returning-chatter=0;display-name=ehtia;emotes=;client-nonce=011296f5ccb7a4442058a42bae7747f4 :ehtia!ehtia@ehtia.tmi.twitch.tv PRIVMSG #nymn headBang","@emotes=;display-name=FollowProtoBuddy;flags=;first-msg=0;mod=0;room-id=62300805;color=#00FF7F;user-id=216144449;historical=1;returning-chatter=0;user-type=;client-nonce=7a50bed26a926634e3620e6b8b89c7d9;turbo=1;subscriber=0;rm-received-ts=1704558430351;tmi-sent-ts=1704558430181;id=0e92ae94-5c5c-4cfc-bbb8-d6c5f6a6b297;badge-info=;badges=turbo/1 :followprotobuddy!followprotobuddy@followprotobuddy.tmi.twitch.tv PRIVMSG #nymn OOOO","@emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170,176-186,192-202,208-218,224-234;user-id=40037186;flags=;id=868b4a17-3b32-49b3-8fa1-4cf6efab8618;color=#FFFF00;turbo=1;display-name=Kotzblitz20;badges=subscriber/9,turbo/1;rm-received-ts=1704558430684;room-id=62300805;user-type=;returning-chatter=0;tmi-sent-ts=1704558430496;historical=1;first-msg=0;client-nonce=1cbfa1119a40d6a5481a1e4372c758ab;subscriber=1;mod=0;badge-info=subscriber/9 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@tmi-sent-ts=1704558430617;badges=vip/1,subscriber/72,rplace-2023/1;mod=0;returning-chatter=0;turbo=0;flags=;badge-info=subscriber/77;rm-received-ts=1704558430784;user-id=87120320;user-type=;color=#D52AFF;subscriber=1;historical=1;display-name=Joshlad;first-msg=0;room-id=62300805;emote-only=1;id=c8aacf6d-264b-4065-a83a-ba0f68ed9209;vip=1;emotes=emotesv2_67cfc3d84f244644a6891e57215cf79d:0-9 :joshlad!joshlad@joshlad.tmi.twitch.tv PRIVMSG #nymn elisRockin","@room-id=62300805;user-type=;tmi-sent-ts=1704558431457;badges=;flags=;rm-received-ts=1704558431646;returning-chatter=0;emotes=;id=fd58d35b-edbb-4287-b480-d52db56def72;badge-info=;turbo=0;user-id=51967700;client-nonce=1b8470946467f785a8d16b37a55cac8f;mod=0;first-msg=0;display-name=Patixxl;subscriber=0;color=#FF0000;historical=1 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@mod=0;id=c4e6d6b1-4d2f-4d69-8425-204abea6eb53;historical=1;first-msg=0;badges=subscriber/36,twitch-recap-2023/1;rm-received-ts=1704558432481;display-name=jontEmillian;user-id=433352132;flags=;room-id=62300805;user-type=;returning-chatter=0;color=#63BD68;turbo=0;emotes=;badge-info=subscriber/38;tmi-sent-ts=1704558432316;subscriber=1 :jontemillian!jontemillian@jontemillian.tmi.twitch.tv PRIVMSG #nymn headBang","@rm-received-ts=1704558432998;room-id=62300805;mod=0;first-msg=0;badges=subscriber/9,turbo/1;tmi-sent-ts=1704558432809;badge-info=subscriber/9;id=de0bf00b-2011-4c03-af83-370e869be277;returning-chatter=0;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170,176-186,192-202,208-218,224-234,240-250,256-266,272-282,288-298,304-314,320-330,336-346,352-362,368-378,384-394,400-410;subscriber=1;display-name=Kotzblitz20;client-nonce=0b6bc2649923d92d3f7e79473756eee6;user-id=40037186;color=#FFFF00;historical=1;flags=;user-type=;turbo=1 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@mod=0;room-id=62300805;client-nonce=68a7ec8a6f39e9ea183b44b0108f56a5;first-msg=0;returning-chatter=0;badge-info=;tmi-sent-ts=1704558433270;user-type=;emotes=;user-id=51967700;subscriber=0;id=f7c44bff-c544-4fae-8b0b-03125067cd06;display-name=Patixxl;badges=;turbo=0;flags=;historical=1;color=#FF0000;rm-received-ts=1704558433442 :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM","@subscriber=0;emotes=;mod=0;badges=bits/100;first-msg=0;color=#25E000;tmi-sent-ts=1704558433256;flags=;rm-received-ts=1704558433450;turbo=0;user-type=;historical=1;user-id=63372784;display-name=DM8917;id=3506ebf3-398b-4eb7-96aa-5824f2a86e69;returning-chatter=0;badge-info=;client-nonce=e1048a1ee30b40a1d9c5b8b03287d003;room-id=62300805 :dm8917!dm8917@dm8917.tmi.twitch.tv PRIVMSG #nymn :FEELSWAYTOODONKMAN FEELSWAYTOODONKMAN FEELSWAYTOODONKMAN FEELSWAYTOODONKMAN FEELSWAYTOODONKMAN FEELSWAYTOODONKMAN","@returning-chatter=0;id=aedfc38a-f3cb-463d-9257-c0e298a6909b;turbo=0;first-msg=0;rm-received-ts=1704558434507;badge-info=subscriber/37;emotes=;room-id=62300805;mod=0;subscriber=1;badges=subscriber/36,no_audio/1;client-nonce=d92bb36df137745dc27c5d1425c9a4d5;display-name=DontCagePlebs;color=#DAA520;user-type=;user-id=85837900;tmi-sent-ts=1704558434329;historical=1;flags= :dontcageplebs!dontcageplebs@dontcageplebs.tmi.twitch.tv PRIVMSG #nymn :headBang headBang","@badges=subscriber/36;subscriber=1;id=7fdb6244-f85e-46e4-9293-58427784ced3;badge-info=subscriber/41;user-type=;flags=;mod=0;historical=1;tmi-sent-ts=1704558434394;first-msg=0;client-nonce=a90f7915863d1c2b8c5be9cf56736a7c;turbo=0;color=#41FF00;user-id=92402102;room-id=62300805;rm-received-ts=1704558434576;display-name=liber7as;emotes=;returning-chatter=0 :liber7as!liber7as@liber7as.tmi.twitch.tv PRIVMSG #nymn :Ratge peepoKing","@emotes=;user-id=51967700;rm-received-ts=1704558434996;tmi-sent-ts=1704558434830;badges=;display-name=Patixxl;badge-info=;flags=;subscriber=0;id=c5a4344b-9096-4829-ae60-c3e75638ee44;mod=0;first-msg=0;returning-chatter=0;historical=1;user-type=;turbo=0;color=#FF0000;room-id=62300805;client-nonce=30ed60fd117908450d4c9e52d44010df :patixxl!patixxl@patixxl.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM 󠀀","@mod=0;badges=no_audio/1;emotes=;display-name=jonhycrack;color=#008000;flags=;first-msg=0;user-type=;subscriber=0;badge-info=;id=c377622f-ca90-4801-9042-6083e32bc05a;tmi-sent-ts=1704558435079;returning-chatter=0;room-id=62300805;user-id=431946171;rm-received-ts=1704558435281;turbo=0;historical=1 :jonhycrack!jonhycrack@jonhycrack.tmi.twitch.tv PRIVMSG #nymn docCum","@user-type=;first-msg=0;badge-info=subscriber/9;mod=0;historical=1;rm-received-ts=1704558435444;display-name=Kotzblitz20;turbo=1;badges=subscriber/9,turbo/1;emotes=emotesv2_ee52e9d2f1344320bad5e93dd2a6e570:0-10,16-26,32-42,48-58,64-74,80-90,96-106,112-122,128-138,144-154,160-170,176-186,192-202,208-218;color=#FFFF00;user-id=40037186;returning-chatter=0;client-nonce=d3b1db3cb35a737476cecb87aa26bfaf;subscriber=1;id=c7f2229e-b8e2-4790-9c0a-5cadd649814a;flags=;tmi-sent-ts=1704558435255;room-id=62300805 :kotzblitz20!kotzblitz20@kotzblitz20.tmi.twitch.tv PRIVMSG #nymn :forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM forsenParty EDM"],"error":null,"error_code":null} \ No newline at end of file diff --git a/benchmarks/resources/seventvemotes-nymn.json b/benchmarks/resources/seventvemotes-nymn.json new file mode 100644 index 000000000..0b61afbf0 --- /dev/null +++ b/benchmarks/resources/seventvemotes-nymn.json @@ -0,0 +1 @@ +{"id":"62300805","platform":"TWITCH","username":"nymn","display_name":"NymN","linked_at":1622031401000,"emote_capacity":42069,"emote_set_id":null,"emote_set":{"id":"63b02874ad025a672cb4969f","name":"Tabula rasa","flags":0,"tags":[],"immutable":false,"privileged":false,"emotes":[{"id":"60ae9a57ac03cad60771b2d8","name":"PagMan","flags":0,"timestamp":1672506026185,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60ae9a57ac03cad60771b2d8","name":"PagMan","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60cbf0275348df16a156b329","username":"chubbss_","display_name":"Chubbss_","avatar_url":"//cdn.7tv.app/user/60cbf0275348df16a156b329/av_65024752bf154a9913688c69/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60ae9a57ac03cad60771b2d8","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1334,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":970,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2318,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2472,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4072,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3620,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4915,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":5104,"format":"WEBP"}]}}},{"id":"60b4fd124eb0019aa6ed4ec7","name":"POGPLANT","flags":0,"timestamp":1672506026442,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60b4fd124eb0019aa6ed4ec7","name":"POGPLANT","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"609fccd64c18609a1dba6c2c","username":"askhp","display_name":"askhp","avatar_url":"//cdn.7tv.app/user/609fccd64c18609a1dba6c2c/av_64a196dc0cd4eb14e55ee103/3x_static.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60b4fd124eb0019aa6ed4ec7","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1178,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1513,"format":"AVIF"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2896,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2920,"format":"WEBP"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":5048,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4407,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5931,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":6258,"format":"WEBP"}]}}},{"id":"60a1de4aac2bcb20efc751fb","name":"pokiDance","flags":0,"timestamp":1672506026675,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60a1de4aac2bcb20efc751fb","name":"pokiDance","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60a1dde6ac2bcb20efc7445e","username":"fiakes","display_name":"fiakes","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60a1de4aac2bcb20efc751fb","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":45,"size":29265,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":45,"size":44820,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":45,"size":64860,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":45,"size":101074,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":45,"size":108125,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":45,"size":167886,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":45,"size":147428,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":45,"size":179198,"format":"WEBP"}]}}},{"id":"6162d21ef7b7a929341244dd","name":"nymn123","flags":0,"timestamp":1672506026912,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"6162d21ef7b7a929341244dd","name":"nymn123","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae8fc0ea50f43c9e3ae255","username":"agenttud","display_name":"agenttud","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/273db808-d42f-4dab-9b39-9780ef2777b0-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6162d21ef7b7a929341244dd","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":87,"height":32,"frame_count":1,"size":2531,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":87,"height":32,"frame_count":1,"size":2570,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":174,"height":64,"frame_count":1,"size":5423,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":174,"height":64,"frame_count":1,"size":6426,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":261,"height":96,"frame_count":1,"size":8100,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":261,"height":96,"frame_count":1,"size":10920,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":348,"height":128,"frame_count":1,"size":11234,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":348,"height":128,"frame_count":1,"size":16142,"format":"WEBP"}]}}},{"id":"60b5917d22b0373436c28ac0","name":"Cat","flags":0,"timestamp":1672506027143,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60b5917d22b0373436c28ac0","name":"Cat","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60b1d205e67fbb9dd75bfb6c","username":"frocasso","display_name":"frocasso","avatar_url":"//cdn.7tv.app/pp/60b1d205e67fbb9dd75bfb6c/80ff55dac0e944469e435c85fd3806fc","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60b5917d22b0373436c28ac0","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":150,"size":39063,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":150,"size":99640,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":150,"size":112648,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":150,"size":234300,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":150,"size":195257,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":150,"size":383216,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":150,"size":284925,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":150,"size":281196,"format":"WEBP"}]}}},{"id":"605305868c870a000de38b6f","name":"LULE","flags":0,"timestamp":1672593293030,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"605305868c870a000de38b6f","name":"LULE","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"6042160396832ffa786fbd8a","username":"noctum2k","display_name":"noctum2k","avatar_url":"//cdn.7tv.app/pp/6042160396832ffa786fbd8a/b6787ae92e55401aa651d208be7563e5","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/605305868c870a000de38b6f","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":912,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1206,"format":"AVIF"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2535,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2466,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3921,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4338,"format":"WEBP"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":6856,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5731,"format":"AVIF"}]}}},{"id":"6145e8b10969108b671957ec","name":"Aware","flags":0,"timestamp":1672593293292,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"6145e8b10969108b671957ec","name":"Aware","flags":0,"tags":["clueless"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"603bb6a596832ffa78e7b27b","username":"megakill3","display_name":"MegaKill3","avatar_url":"//cdn.7tv.app/pp/603bb6a596832ffa78e7b27b/add3acfec2fb4256816e944d79b94a0c","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6145e8b10969108b671957ec","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":35,"height":32,"frame_count":61,"size":13635,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":35,"height":32,"frame_count":61,"size":71976,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":70,"height":64,"frame_count":61,"size":29174,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":70,"height":64,"frame_count":61,"size":164190,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":105,"height":96,"frame_count":61,"size":44686,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":105,"height":96,"frame_count":61,"size":268170,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":140,"height":128,"frame_count":61,"size":66758,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":140,"height":128,"frame_count":61,"size":355046,"format":"WEBP"}]}}},{"id":"6154ecd36251d7e000db18a0","name":"Clueless","flags":0,"timestamp":1672593293523,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"6154ecd36251d7e000db18a0","name":"Clueless","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"6053853cb4d31e459fdaa2dc","username":"laden","display_name":"Laden","avatar_url":"//cdn.7tv.app/pp/6053853cb4d31e459fdaa2dc/a94c67d7736940feb543e42024b740ef","style":{"color":849892095},"roles":["60724f65e93d828bf8858789","6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6154ecd36251d7e000db18a0","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":35,"height":32,"frame_count":1,"size":1206,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":35,"height":32,"frame_count":1,"size":1138,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":70,"height":64,"frame_count":1,"size":2072,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":70,"height":64,"frame_count":1,"size":2480,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":105,"height":96,"frame_count":1,"size":3177,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":105,"height":96,"frame_count":1,"size":3818,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":140,"height":128,"frame_count":1,"size":4182,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":140,"height":128,"frame_count":1,"size":5372,"format":"WEBP"}]}}},{"id":"60af0116a564afa26e3a7e86","name":"FloppaJAM","flags":0,"timestamp":1672593653882,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"60af0116a564afa26e3a7e86","name":"FloppaJAM","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"603ccf8b96832ffa78f80acd","username":"ook_3d","display_name":"ook_3D","avatar_url":"//cdn.7tv.app/pp/603ccf8b96832ffa78f80acd/fd41b01caad0452e96d247c7009580ab","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60af0116a564afa26e3a7e86","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":10,"size":4812,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":10,"size":7454,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":10,"size":7899,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":10,"size":16102,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":10,"size":12234,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":10,"size":23620,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":10,"size":17564,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":10,"size":26176,"format":"WEBP"}]}}},{"id":"603cd0152c7b4500143b46db","name":"DOCING","flags":0,"timestamp":1672672534103,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"603cd0152c7b4500143b46db","name":"DOCING","flags":0,"tags":["docing","docpls","forsencd","doc"],"lifecycle":3,"state":["NO_PERSONAL","LISTED"],"listed":true,"animated":true,"owner":{"id":"60a7c5c1ad0a5c63ef23bf5d","username":"was1max","display_name":"was1max","avatar_url":"//cdn.7tv.app/pp/60a7c5c1ad0a5c63ef23bf5d/72a8daff0f3744c0aa5184eb424ac711","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/603cd0152c7b4500143b46db","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":101,"size":38657,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":101,"size":81300,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":101,"size":82711,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":101,"size":189874,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":101,"size":136307,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":101,"size":308808,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":101,"size":420589,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":101,"size":542906,"format":"WEBP"}]}}},{"id":"60e8677677b18d5dd3800410","name":"AlienPls","flags":0,"timestamp":1672672534388,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60e8677677b18d5dd3800410","name":"AlienPls","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae4b445d3fdae583d20e9a","username":"ethantp","display_name":"ethantp","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/617473d7-7627-4bd6-befa-a2ff489d8daa-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60e8677677b18d5dd3800410","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":256,"size":142250,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":256,"size":203518,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":256,"size":329152,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":256,"size":494008,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":256,"size":571690,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":256,"size":880470,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":256,"size":855284,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":256,"size":1159480,"format":"WEBP"}]}}},{"id":"603caa69faf3a00014dff0b1","name":"Okayeg","flags":0,"timestamp":1672672847172,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"603caa69faf3a00014dff0b1","name":"Okayeg","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"603caa3396832ffa78c1aa0d","username":"no_title24","display_name":"no_title24","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/8bc0c15d-f2a5-457f-9c52-04bcbda0b806-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/603caa69faf3a00014dff0b1","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1571,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1138,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":3043,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2664,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4419,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4338,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":6242,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":6204,"format":"WEBP"}]}}},{"id":"60ae546c9986a00349ea35d5","name":"NymN","flags":0,"timestamp":1672702606816,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"60ae546c9986a00349ea35d5","name":"NymN","flags":1,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae3c29b2ecb015051f8f9a","username":"nymn","display_name":"NymN","avatar_url":"//cdn.7tv.app/pp/60ae3c29b2ecb015051f8f9a/71f269555aeb44c29100cae8aa59b56b","style":{"color":-1857617921},"roles":["6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60ae546c9986a00349ea35d5","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1102,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":742,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1931,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":1780,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":2802,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":2872,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4383,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":4306,"format":"WEBP"}]}}},{"id":"6042089e77137b000de9e669","name":"OMEGALUL","flags":0,"timestamp":1672758964245,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"6042089e77137b000de9e669","name":"OMEGALUL","flags":0,"tags":["xdddd"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"603ca8f696832ffa78c01eb4","username":"mauriplss","display_name":"Mauriplss","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/a428f0f0-bdd4-4c93-ac4b-ed174244cb66-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6042089e77137b000de9e669","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1293,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1046,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2437,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2652,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3734,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4586,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5702,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":7340,"format":"WEBP"}]}}},{"id":"6186d2e94ea2f24e50099f35","name":"sadE","flags":0,"timestamp":1672758964495,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"6186d2e94ea2f24e50099f35","name":"sadE","flags":0,"tags":["forsen","sad","sadge","lule"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"host":{"url":"//cdn.7tv.app/emote/6186d2e94ea2f24e50099f35","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":972,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":708,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1823,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":1836,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":2584,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":3018,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":3493,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":4506,"format":"WEBP"}]}}},{"id":"6329beb61c85cd937753ec61","name":"TimeToNime","flags":0,"timestamp":1672759315382,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"6329beb61c85cd937753ec61","name":"Nidea","flags":0,"tags":["idea","nymn"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60421e5596832ffa787cfa62","username":"sauha_","display_name":"Sauha_","avatar_url":"//static-cdn.jtvnw.net/user-default-pictures-uv/75305d54-c7cc-40d1-bb9c-91fbe85943c7-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6329beb61c85cd937753ec61","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":18,"size":6337,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":14,"size":7828,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":18,"size":9931,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":18,"size":16092,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":18,"size":14116,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":18,"size":26006,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":18,"size":19668,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":18,"size":34324,"format":"WEBP"}]}}},{"id":"60aeff0411a994a4acdd36b6","name":"docnotL","flags":0,"timestamp":1672845413213,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60aeff0411a994a4acdd36b6","name":"docnotL","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae653c9627f9aff4f5ccd1","username":"xoo_6119","display_name":"xoo_6119","avatar_url":"//cdn.7tv.app/user/60ae653c9627f9aff4f5ccd1/av_63ca0eccdedb49b24383ae5c/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60aeff0411a994a4acdd36b6","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":60,"size":22137,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":60,"size":41474,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":60,"size":44532,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":60,"size":79840,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":60,"size":70270,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":60,"size":123148,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":60,"size":94376,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":60,"size":137488,"format":"WEBP"}]}}},{"id":"60ae997698f4291470d407a8","name":"MODS","flags":0,"timestamp":1672845413728,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60ae997698f4291470d407a8","name":"MODS","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae8fc0ea50f43c9e3ae255","username":"agenttud","display_name":"agenttud","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/273db808-d42f-4dab-9b39-9780ef2777b0-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60ae997698f4291470d407a8","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":156,"size":28522,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":156,"size":117268,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":156,"size":64943,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":156,"size":243580,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":156,"size":114327,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":156,"size":377968,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":156,"size":194242,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":156,"size":415022,"format":"WEBP"}]}}},{"id":"63072162942ffb69e13d703f","name":"pepeW","flags":0,"timestamp":1672860000593,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"63072162942ffb69e13d703f","name":"pepeW","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60b04a7fad7fb4b50bd3a982","username":"brian6932","display_name":"brian6932","avatar_url":"//cdn.7tv.app/user/60b04a7fad7fb4b50bd3a982/av_64f8035f8bef730969094d7a/3x_static.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63072162942ffb69e13d703f","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":181,"size":16593,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":177,"size":47362,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":181,"size":28488,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":181,"size":155684,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":181,"size":46691,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":181,"size":247212,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":181,"size":73886,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":181,"size":344758,"format":"WEBP"}]}}},{"id":"60e787dd375879d78fc6b25e","name":"SNIFFA","flags":0,"timestamp":1672922706116,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60e787dd375879d78fc6b25e","name":"SNIFFA","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"6042058896832ffa785800fe","username":"zhark","display_name":"Zhark","avatar_url":"//cdn.7tv.app/pp/6042058896832ffa785800fe/37ee95ffaa9846b286cb5554ff0716c5","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60e787dd375879d78fc6b25e","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":139,"size":16776,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":139,"size":137452,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":139,"size":42320,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":139,"size":299940,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":139,"size":84421,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":139,"size":495752,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":139,"size":175672,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":139,"size":591066,"format":"WEBP"}]}}},{"id":"618fd73e17e4d50afc0d4e3f","name":"docPls","flags":0,"timestamp":1672931580819,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"618fd73e17e4d50afc0d4e3f","name":"docPls","flags":0,"tags":["doc","drdisrespect","doctordance"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60edbe6890b3667a8a3cfb66","username":"flushedjulian","display_name":"flushedjulian","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/ede97175-eb3e-4656-967d-1fc0da391dac-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/618fd73e17e4d50afc0d4e3f","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":126,"size":61366,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":126,"size":102754,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":126,"size":138229,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":126,"size":235070,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":126,"size":231065,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":126,"size":400984,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":126,"size":335953,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":126,"size":491700,"format":"WEBP"}]}}},{"id":"603cbda573d7a5001441f9d5","name":"flushE","flags":0,"timestamp":1672931818368,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"603cbda573d7a5001441f9d5","name":"flushE","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"603cb87696832ffa78d57767","username":"obscurelambda","display_name":"obscurelambda","avatar_url":"//cdn.7tv.app/user/603cb87696832ffa78d57767/av_647a68a9804ca09ebf23e890/3x.webp","style":{"color":849892095},"roles":["60724f65e93d828bf8858789","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/603cbda573d7a5001441f9d5","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1335,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":906,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2528,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2276,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3593,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":3574,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4834,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":5360,"format":"WEBP"}]}}},{"id":"60fd57134653f5d6c1b10d86","name":"Nime","flags":0,"timestamp":1672931818641,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60fd57134653f5d6c1b10d86","name":"Nime","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60ae3e98b2ecb0150535c6b7","username":"gempir","display_name":"gempir","avatar_url":"//cdn.7tv.app/pp/60ae3e98b2ecb0150535c6b7/4aa1786cec024098be20d7b0683bae72","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60fd57134653f5d6c1b10d86","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1153,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":802,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1953,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":1920,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3068,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":3480,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4554,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":5248,"format":"WEBP"}]}}},{"id":"603cb3b4c20d020014423c44","name":"pepeLaugh","flags":0,"timestamp":1673018783171,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"603cb3b4c20d020014423c44","name":"pepeLaugh","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"603ca8f696832ffa78c01eb4","username":"mauriplss","display_name":"Mauriplss","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/a428f0f0-bdd4-4c93-ac4b-ed174244cb66-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/603cb3b4c20d020014423c44","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1400,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1076,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2786,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2688,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4311,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4546,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":6117,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":6698,"format":"WEBP"}]}}},{"id":"616edc115ff09767de29919b","name":"dankHug","flags":0,"timestamp":1673018789436,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"616edc115ff09767de29919b","name":"dankHug","flags":0,"tags":["dank","hug"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae7643b351b8d1c09294b9","username":"snz_____","display_name":"snz_____","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/e441ab99-5ab9-4a54-8967-31efa3fd5e96-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/616edc115ff09767de29919b","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1309,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1078,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2377,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2638,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3791,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4482,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5563,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":6626,"format":"WEBP"}]}}},{"id":"603cb56bc20d020014423c60","name":"FeelsDonkMan","flags":0,"timestamp":1673020529104,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"603cb56bc20d020014423c60","name":"FeelsDonkMan","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"647e1211804ca09ebf249957","username":"aisenyeeecs","display_name":"AisenYeeeCS","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/6039818f-d868-48ec-ab02-b79125eeb894-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/603cb56bc20d020014423c60","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1570,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1038,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":3210,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2636,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":5008,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4414,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":7271,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":6684,"format":"WEBP"}]}}},{"id":"60ae4cd10e3547763479fc83","name":"Lamonting","flags":0,"timestamp":1673104655687,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60ae4cd10e3547763479fc83","name":"Lamonting","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae4b445d3fdae583d20e9a","username":"ethantp","display_name":"ethantp","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/617473d7-7627-4bd6-befa-a2ff489d8daa-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60ae4cd10e3547763479fc83","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":118,"size":17571,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":118,"size":77488,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":118,"size":68298,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":118,"size":187318,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":118,"size":139079,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":118,"size":319382,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":118,"size":224792,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":118,"size":229782,"format":"WEBP"}]}}},{"id":"62f9cabd00630d5b2acd66f0","name":"DinkDonk","flags":0,"timestamp":1673104655945,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"62f9cabd00630d5b2acd66f0","name":"DinkDonk","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"603c7fca96832ffa788a5f14","username":"hyruverse","display_name":"hyruverse","avatar_url":"//cdn.7tv.app/pp/603c7fca96832ffa788a5f14/2ed3bd237882444ebccf38ae918e8df6","style":{"color":849892095},"roles":["60724f65e93d828bf8858789","6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/62f9cabd00630d5b2acd66f0","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":2,"size":2986,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":2,"size":1960,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":2,"size":4566,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":2,"size":4152,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":2,"size":6451,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":2,"size":6996,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":2,"size":8261,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":2,"size":9868,"format":"WEBP"}]}}},{"id":"60ad8c93c7188f3be2332566","name":"NOIDONTTHINKSO","flags":0,"timestamp":1673104656191,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60ad8c93c7188f3be2332566","name":"NOIDONTTHINKSO","flags":0,"lifecycle":3,"state":["PERSONAL","LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae8fc0ea50f43c9e3ae255","username":"agenttud","display_name":"agenttud","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/273db808-d42f-4dab-9b39-9780ef2777b0-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60ad8c93c7188f3be2332566","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":124,"size":18896,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":124,"size":91376,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":124,"size":42469,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":124,"size":197032,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":124,"size":92230,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":124,"size":316230,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":124,"size":161369,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":124,"size":336336,"format":"WEBP"}]}}},{"id":"6063d9f8f4dc10001426b946","name":"GoodTake","flags":0,"timestamp":1673158455736,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"6063d9f8f4dc10001426b946","name":"GoodTake","flags":0,"tags":["muted","boring","volume"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"603c624796832ffa78678fd7","username":"juanjunki_6969","display_name":"JuanJunki_6969","avatar_url":"//cdn.7tv.app/user/603c624796832ffa78678fd7/av_64df2a212356a20967ed54d9/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6063d9f8f4dc10001426b946","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":93,"height":32,"frame_count":81,"size":12106,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":93,"height":32,"frame_count":81,"size":50910,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":186,"height":64,"frame_count":81,"size":24839,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":186,"height":64,"frame_count":81,"size":187294,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":279,"height":96,"frame_count":81,"size":47468,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":279,"height":96,"frame_count":81,"size":400330,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":372,"height":128,"frame_count":81,"size":234651,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":372,"height":128,"frame_count":81,"size":1003370,"format":"WEBP"}]}}},{"id":"60e857ca401af27eed2f6a4e","name":"Wokege","flags":0,"timestamp":1673190125994,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60e857ca401af27eed2f6a4e","name":"Wokege","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"6053853cb4d31e459fdaa2dc","username":"laden","display_name":"Laden","avatar_url":"//cdn.7tv.app/pp/6053853cb4d31e459fdaa2dc/a94c67d7736940feb543e42024b740ef","style":{"color":849892095},"roles":["60724f65e93d828bf8858789","6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60e857ca401af27eed2f6a4e","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":34,"height":32,"frame_count":1,"size":1579,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":34,"height":32,"frame_count":1,"size":1280,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":68,"height":64,"frame_count":1,"size":3186,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":68,"height":64,"frame_count":1,"size":3254,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":102,"height":96,"frame_count":1,"size":4969,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":102,"height":96,"frame_count":1,"size":5682,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":136,"height":128,"frame_count":1,"size":6693,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":136,"height":128,"frame_count":1,"size":7990,"format":"WEBP"}]}}},{"id":"619fb59915b3ff4a5bb7a90a","name":"AlienUnpleased","flags":0,"timestamp":1673190126255,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"619fb59915b3ff4a5bb7a90a","name":"AlienUnpleased","flags":0,"tags":["alien","alienpls","aliens","jam","pls"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"610320e899197254381e176a","username":"pallaslol","display_name":"pallaslol","avatar_url":"//cdn.7tv.app/user/610320e899197254381e176a/av_648247a3b4ddf6f60d18b83c/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/619fb59915b3ff4a5bb7a90a","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":792,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1178,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":1958,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1966,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":3368,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":2898,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":5022,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4112,"format":"AVIF"}]}}},{"id":"61d679c83d52bb5c33c4f9a6","name":"Buhh","flags":0,"timestamp":1673191602063,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"61d679c83d52bb5c33c4f9a6","name":"Buhh","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"613f0d960969108b6718ab36","username":"suspiciousmonkey","display_name":"SUSpiciouSmonkeY","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/ae6fdabf-8049-47d9-86d6-0ec9eabf9a26-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61d679c83d52bb5c33c4f9a6","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":936,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1291,"format":"AVIF"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2139,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":1968,"format":"WEBP"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":3308,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3021,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":3864,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":4710,"format":"WEBP"}]}}},{"id":"62af82f5454b0130fba333ba","name":"ok","flags":0,"timestamp":1673206059533,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"62af82f5454b0130fba333ba","name":"ok","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae759bdf5735e04acb69d9","username":"hotbear1110","display_name":"HotBear1110","avatar_url":"//cdn.7tv.app/pp/60ae759bdf5735e04acb69d9/80e2b49378c14dc6914fde8cb72fa673","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/62af82f5454b0130fba333ba","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":856,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1105,"format":"AVIF"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2130,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2096,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3054,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":3478,"format":"WEBP"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":5214,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4334,"format":"AVIF"}]}}},{"id":"605304ed8c870a000de38b6d","name":"Sadeg","flags":0,"timestamp":1673276592474,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"605304ed8c870a000de38b6d","name":"Sadeg","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"6042160396832ffa786fbd8a","username":"noctum2k","display_name":"noctum2k","avatar_url":"//cdn.7tv.app/pp/6042160396832ffa786fbd8a/b6787ae92e55401aa651d208be7563e5","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/605304ed8c870a000de38b6d","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1496,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1094,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":3153,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2818,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4839,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4692,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":7317,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":6820,"format":"WEBP"}]}}},{"id":"60ae3853b2ecb015050540d2","name":"WTFFF","flags":0,"timestamp":1673276592999,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60ae3853b2ecb015050540d2","name":"WTFF","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60635b50452cea4685f26b34","username":"hecrzy","display_name":"heCrzy","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/583dd5ac-2fe8-4ead-a20d-e10770118c5f-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60ae3853b2ecb015050540d2","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":847,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":598,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1371,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":1398,"format":"WEBP"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":2128,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":1852,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":2513,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":2138,"format":"WEBP"}]}}},{"id":"61a7bac1e9684edbbc37d009","name":"eShrug","flags":0,"timestamp":1673295807076,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"61a7bac1e9684edbbc37d009","name":"eShrug","flags":0,"tags":["forsen"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60aebf0ae90f445e43b37fe5","username":"prog0ldfish","display_name":"ProG0ldfish","avatar_url":"//cdn.7tv.app/user/60aebf0ae90f445e43b37fe5/av_6353ef0d7f642dee0e30c1f4/3x_static.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61a7bac1e9684edbbc37d009","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":55,"height":32,"frame_count":1,"size":1069,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":55,"height":32,"frame_count":1,"size":1004,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":110,"height":64,"frame_count":1,"size":1787,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":110,"height":64,"frame_count":1,"size":2172,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":165,"height":96,"frame_count":1,"size":2799,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":165,"height":96,"frame_count":1,"size":3654,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":220,"height":128,"frame_count":1,"size":3622,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":220,"height":128,"frame_count":1,"size":5202,"format":"WEBP"}]}}},{"id":"603ccedf2c7b4500143b46d7","name":"forsenCD","flags":0,"timestamp":1673362992445,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"603ccedf2c7b4500143b46d7","name":"forsenCD","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"603ccea596832ffa78f6ad89","username":"hmoodybins","display_name":"HMOODYBINS","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/5de62438-58d6-4cd1-840c-7690cf22e4f8-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/603ccedf2c7b4500143b46d7","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":838,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1148,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2180,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2305,"format":"AVIF"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3375,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":3596,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4655,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":5570,"format":"WEBP"}]}}},{"id":"603c89cbbb69c00014bed23e","name":"ZULUL","flags":0,"timestamp":1673362992934,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"603c89cbbb69c00014bed23e","name":"ZULUL","flags":0,"lifecycle":3,"state":["NO_PERSONAL"],"listed":false,"animated":false,"owner":{"id":"000000000000000000000000","username":"","display_name":"","style":{}},"host":{"url":"//cdn.7tv.app/emote/603c89cbbb69c00014bed23e","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":27,"height":32,"frame_count":1,"size":1073,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":27,"height":32,"frame_count":1,"size":886,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":54,"height":64,"frame_count":1,"size":1898,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":54,"height":64,"frame_count":1,"size":2064,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":81,"height":96,"frame_count":1,"size":2875,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":81,"height":96,"frame_count":1,"size":3422,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":108,"height":128,"frame_count":1,"size":3916,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":108,"height":128,"frame_count":1,"size":5098,"format":"WEBP"}]}}},{"id":"61630205c1ff9a17cc396522","name":"Sadge","flags":0,"timestamp":1673363399204,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"61630205c1ff9a17cc396522","name":"Sadge","flags":0,"tags":["sad"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"61562bfd6251d7e000db34c8","username":"mavii","display_name":"Mavii","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/64746afb-68cf-4dfb-98f7-9d4589bfb18b-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61630205c1ff9a17cc396522","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1284,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":952,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2475,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2406,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3787,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4156,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5426,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":6624,"format":"WEBP"}]}}},{"id":"63438a743d1bc89e0ff9e400","name":"peepoChat","flags":0,"timestamp":1673365629446,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"63438a743d1bc89e0ff9e400","name":"peepoChat","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"6053853cb4d31e459fdaa2dc","username":"laden","display_name":"Laden","avatar_url":"//cdn.7tv.app/pp/6053853cb4d31e459fdaa2dc/a94c67d7736940feb543e42024b740ef","style":{"color":849892095},"roles":["60724f65e93d828bf8858789","6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63438a743d1bc89e0ff9e400","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":2,"size":3044,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":2,"size":1546,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":2,"size":3708,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":2,"size":5589,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":2,"size":6386,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":2,"size":8212,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":2,"size":11058,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":2,"size":9258,"format":"WEBP"}]}}},{"id":"61fe824dd771ca5bf0379bb2","name":"Excel","flags":0,"timestamp":1673446825105,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"61fe824dd771ca5bf0379bb2","name":"Excel","flags":0,"tags":["excel","nymn"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae3e3eb2ecb01505346ae9","username":"fawcan","display_name":"Fawcan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38051bdb-83c6-4716-95e0-731462e02b45-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61fe824dd771ca5bf0379bb2","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":993,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":696,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1436,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":1356,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":1978,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":2662,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":2391,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":3408,"format":"WEBP"}]}}},{"id":"6053abc29d9e96000d24503d","name":"amongE","flags":0,"timestamp":1673449422562,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"6053abc29d9e96000d24503d","name":"amongE","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"608bd85b99dcd148faf02644","username":"tehargi_","display_name":"tehargi_","avatar_url":"//static-cdn.jtvnw.net/user-default-pictures-uv/294c98b5-e34d-42cd-a8f0-140b72fba9b0-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6053abc29d9e96000d24503d","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1382,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1096,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2718,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2508,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3902,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4338,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5187,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":5954,"format":"WEBP"}]}}},{"id":"6042091777137b000de9e66b","name":"monkaOMEGA","flags":0,"timestamp":1673449422786,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"6042091777137b000de9e66b","name":"monkaOMEGA","flags":0,"lifecycle":3,"state":["PERSONAL","LISTED"],"listed":true,"animated":false,"owner":{"id":"603ca8f696832ffa78c01eb4","username":"mauriplss","display_name":"Mauriplss","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/a428f0f0-bdd4-4c93-ac4b-ed174244cb66-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6042091777137b000de9e66b","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1224,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1066,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2189,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2334,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3289,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":3862,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4548,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":5322,"format":"WEBP"}]}}},{"id":"60afc290ebfcf7562ee8bab5","name":"Pepege","flags":0,"timestamp":1673449457426,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60afc290ebfcf7562ee8bab5","name":"Pepege","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60afb513a3648f409a305485","username":"mistrzu__","display_name":"mistrzu__","avatar_url":"//cdn.7tv.app/pp/60afb513a3648f409a305485/efa75abba8be42929a6178b381804a6a","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60afc290ebfcf7562ee8bab5","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":956,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1361,"format":"AVIF"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2644,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2368,"format":"WEBP"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4070,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4086,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5497,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":5532,"format":"WEBP"}]}}},{"id":"6351b23d321db49a66a2429f","name":"MEMONEY","flags":0,"timestamp":1673535852634,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"6351b23d321db49a66a2429f","name":"MEMONEY","flags":0,"tags":["nime","lime","spongebob","krabs","money","scam"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60b6746f64faf92496330d3c","username":"littlescampi","display_name":"LittleScampi","avatar_url":"//cdn.7tv.app/pp/60b6746f64faf92496330d3c/a1ff043123944a119d69cf9a7062a442","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6351b23d321db49a66a2429f","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":74,"height":32,"frame_count":1,"size":3110,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":74,"height":32,"frame_count":1,"size":3742,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":148,"height":64,"frame_count":1,"size":6771,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":148,"height":64,"frame_count":1,"size":10444,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":222,"height":96,"frame_count":1,"size":11017,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":222,"height":96,"frame_count":1,"size":19618,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":296,"height":128,"frame_count":1,"size":14982,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":296,"height":128,"frame_count":1,"size":30874,"format":"WEBP"}]}}},{"id":"6329da94345c8855a28db877","name":"AINTNOWAY","flags":0,"timestamp":1673535852914,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"6329da94345c8855a28db877","name":"AINTNOWAY","flags":0,"tags":["naw","deadass","skull","nah"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60d37adc205cf63c7eef6871","username":"eazylemnsqeezy","display_name":"eazylemnsqeezy","avatar_url":"//cdn.7tv.app/user/60d37adc205cf63c7eef6871/av_64200b330ef35e7ab89f2db4/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6329da94345c8855a28db877","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":39,"height":32,"frame_count":200,"size":32648,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":39,"height":32,"frame_count":200,"size":53182,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":78,"height":64,"frame_count":200,"size":355465,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":78,"height":64,"frame_count":200,"size":133396,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":117,"height":96,"frame_count":200,"size":1278863,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":117,"height":96,"frame_count":200,"size":377592,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":156,"height":128,"frame_count":200,"size":3226360,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":156,"height":128,"frame_count":200,"size":1273268,"format":"WEBP"}]}}},{"id":"62f80d745a8981e4c792ca1c","name":"RAGEY","flags":0,"timestamp":1673542285985,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"62f80d745a8981e4c792ca1c","name":"RAGEY","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60e8c8686d77836414234d0d","username":"pattiiiiiiii","display_name":"PATTIIIIIIII","avatar_url":"//cdn.7tv.app/user/60e8c8686d77836414234d0d/av_64b096dbb1a061d079b25a75/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/62f80d745a8981e4c792ca1c","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":38,"size":14018,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":38,"size":25518,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":38,"size":28460,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":38,"size":51096,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":38,"size":48701,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":38,"size":79202,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":38,"size":62604,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":38,"size":73942,"format":"WEBP"}]}}},{"id":"62c02c2cc2b63d1e2f3d8782","name":"hmmMeeting","flags":0,"timestamp":1673622252419,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"62c02c2cc2b63d1e2f3d8782","name":"hmmMeeting","flags":0,"tags":["reunion","group","hmm","discussion","talk","council"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"61541c3c20eaf897465ad48b","username":"andreimonty","display_name":"AndreiMonty","avatar_url":"//cdn.7tv.app/user/61541c3c20eaf897465ad48b/av_6458e293d3b4256e12d830a9/3x.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/62c02c2cc2b63d1e2f3d8782","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":79,"height":32,"frame_count":29,"size":10921,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":79,"height":32,"frame_count":29,"size":51390,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":158,"height":64,"frame_count":29,"size":28695,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":158,"height":64,"frame_count":29,"size":133652,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":237,"height":96,"frame_count":29,"size":56672,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":237,"height":96,"frame_count":29,"size":224218,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":316,"height":128,"frame_count":29,"size":113458,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":316,"height":128,"frame_count":29,"size":345852,"format":"WEBP"}]}}},{"id":"60ae958e229664e8667aea38","name":"GIGACHAD","flags":0,"timestamp":1673622252685,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60ae958e229664e8667aea38","name":"GIGACHAD","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae264eaee2aa55389c4164","username":"beutelino","display_name":"Beutelino","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/1dfb4e5c-3a50-4e41-9f79-887157c18f36-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60ae958e229664e8667aea38","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":198,"size":19845,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":198,"size":99794,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":198,"size":61265,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":198,"size":252100,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":198,"size":137527,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":198,"size":414348,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":198,"size":263741,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":198,"size":308490,"format":"WEBP"}]}}},{"id":"6305c255d4b348f08e833c90","name":"GymN","flags":0,"timestamp":1673630581712,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"6305c255d4b348f08e833c90","name":"GymN","flags":0,"tags":["nymn"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae3e98b2ecb0150535c6b7","username":"gempir","display_name":"gempir","avatar_url":"//cdn.7tv.app/pp/60ae3e98b2ecb0150535c6b7/4aa1786cec024098be20d7b0683bae72","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6305c255d4b348f08e833c90","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":68,"height":32,"frame_count":1,"size":2480,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":68,"height":32,"frame_count":1,"size":3752,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":136,"height":64,"frame_count":1,"size":5167,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":136,"height":64,"frame_count":1,"size":10610,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":204,"height":96,"frame_count":1,"size":7617,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":204,"height":96,"frame_count":1,"size":20062,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":272,"height":128,"frame_count":1,"size":10343,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":272,"height":128,"frame_count":1,"size":30922,"format":"WEBP"}]}}},{"id":"63695fc399efe5867cd0d4a5","name":"monkaE","flags":0,"timestamp":1673635463409,"actor_id":"60ae8fc0ea50f43c9e3ae255","data":{"id":"63695fc399efe5867cd0d4a5","name":"monkaE","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"6042058896832ffa785800fe","username":"zhark","display_name":"Zhark","avatar_url":"//cdn.7tv.app/pp/6042058896832ffa785800fe/37ee95ffaa9846b286cb5554ff0716c5","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63695fc399efe5867cd0d4a5","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":8,"size":3905,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":8,"size":3650,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":8,"size":6754,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":8,"size":8000,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":8,"size":9843,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":8,"size":11620,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":8,"size":12931,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":8,"size":16320,"format":"WEBP"}]}}},{"id":"61c71adaef5a587a07458f83","name":"Ogre","flags":0,"timestamp":1673636145247,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"61c71adaef5a587a07458f83","name":"Ogre","flags":0,"tags":["erobb221","ugly"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"61c7196612987d64d6ae7fc6","username":"quaxi13","display_name":"Quaxi13","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/39135e01-6d49-40ad-afe5-2973e6176848-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61c71adaef5a587a07458f83","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":53,"height":32,"frame_count":1,"size":1523,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":53,"height":32,"frame_count":1,"size":1370,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":106,"height":64,"frame_count":1,"size":2919,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":106,"height":64,"frame_count":1,"size":3268,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":159,"height":96,"frame_count":1,"size":4489,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":159,"height":96,"frame_count":1,"size":5584,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":212,"height":128,"frame_count":1,"size":6591,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":212,"height":128,"frame_count":1,"size":8572,"format":"WEBP"}]}}},{"id":"618a368217e4d50afc0cb2a8","name":"forsenWiggle","flags":0,"timestamp":1673636704456,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"618a368217e4d50afc0cb2a8","name":"forsenWiggle","flags":0,"tags":["forsen","wiggle","zulul","cute","scuffed","lidl"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"61661e03ce26ef6063881c94","username":"sidetrvcked","display_name":"sidetrvcked","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/3dc090a3-28b7-4707-b087-8641e5755201-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/618a368217e4d50afc0cb2a8","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":12,"size":8398,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":12,"size":13368,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":12,"size":17785,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":12,"size":34300,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":12,"size":33488,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":12,"size":58862,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":12,"size":49403,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":12,"size":72840,"format":"WEBP"}]}}},{"id":"6218ad877cc2d4e1953802e9","name":"Listening","flags":0,"timestamp":1673708657421,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"6218ad877cc2d4e1953802e9","name":"Listening","flags":0,"tags":["patrickbateman","americanpsycho","christianbale","lolw","music"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60b132f3bdea6753986d0208","username":"alastorkunn","display_name":"alastorkunn","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/c7d7cd45-054f-4dfd-ba8d-12a3347030d8-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6218ad877cc2d4e1953802e9","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":56,"height":32,"frame_count":162,"size":35541,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":56,"height":32,"frame_count":162,"size":160110,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":112,"height":64,"frame_count":162,"size":125123,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":112,"height":64,"frame_count":162,"size":403360,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":168,"height":96,"frame_count":162,"size":274685,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":168,"height":96,"frame_count":162,"size":721338,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":224,"height":128,"frame_count":162,"size":529864,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":224,"height":128,"frame_count":162,"size":921242,"format":"WEBP"}]}}},{"id":"60e8573f3c5b87437a3bac1f","name":"Bedge","flags":0,"timestamp":1673708657711,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60e8573f3c5b87437a3bac1f","name":"Bedge","flags":0,"lifecycle":3,"state":["NO_PERSONAL","LISTED"],"listed":true,"animated":false,"owner":{"id":"6053853cb4d31e459fdaa2dc","username":"laden","display_name":"Laden","avatar_url":"//cdn.7tv.app/pp/6053853cb4d31e459fdaa2dc/a94c67d7736940feb543e42024b740ef","style":{"color":849892095},"roles":["60724f65e93d828bf8858789","6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60e8573f3c5b87437a3bac1f","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":34,"height":32,"frame_count":1,"size":1546,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":34,"height":32,"frame_count":1,"size":1260,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":68,"height":64,"frame_count":1,"size":3125,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":68,"height":64,"frame_count":1,"size":3242,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":102,"height":96,"frame_count":1,"size":4760,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":102,"height":96,"frame_count":1,"size":5438,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":136,"height":128,"frame_count":1,"size":6393,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":136,"height":128,"frame_count":1,"size":7762,"format":"WEBP"}]}}},{"id":"63c1e2d7182ccc7de666c18b","name":"FeelsNymNge","flags":0,"timestamp":1673712270114,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"63c1e2d7182ccc7de666c18b","name":"FeelsNymNge","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae434b5d3fdae58382926a","username":"ayyybubu","display_name":"ayyybubu","avatar_url":"//cdn.7tv.app/user/60ae434b5d3fdae58382926a/av_636fa88735a18fd8b17e7399/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","62d86a8419fdcf401421c5ae","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63c1e2d7182ccc7de666c18b","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":42,"height":32,"frame_count":13,"size":12034,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":42,"height":32,"frame_count":13,"size":12778,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":84,"height":64,"frame_count":13,"size":26572,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":84,"height":64,"frame_count":13,"size":29200,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":126,"height":96,"frame_count":13,"size":40648,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":126,"height":96,"frame_count":13,"size":44452,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":168,"height":128,"frame_count":13,"size":60273,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":168,"height":128,"frame_count":13,"size":60834,"format":"WEBP"}]}}},{"id":"603caea243b9e100141caf4f","name":"TrollDespair","flags":0,"timestamp":1673795060685,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"603caea243b9e100141caf4f","name":"TrollDespair","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"603cae2496832ffa78c758cd","username":"swyfty_","display_name":"swyfty_","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/93a53aa0-4f02-4834-82e5-602f39d14ddc-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/603caea243b9e100141caf4f","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":943,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":716,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1615,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":1706,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":2364,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":2516,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":3084,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":3702,"format":"WEBP"}]}}},{"id":"610435e3fb49175e996f016b","name":"apolloJAM","flags":0,"timestamp":1673795060939,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"610435e3fb49175e996f016b","name":"apolloJAM","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"6042058896832ffa785800fe","username":"zhark","display_name":"Zhark","avatar_url":"//cdn.7tv.app/pp/6042058896832ffa785800fe/37ee95ffaa9846b286cb5554ff0716c5","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/610435e3fb49175e996f016b","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":216,"size":66008,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":216,"size":180824,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":216,"size":188821,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":216,"size":429512,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":216,"size":349160,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":216,"size":747160,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":216,"size":583714,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":216,"size":854150,"format":"WEBP"}]}}},{"id":"603cbdbc73d7a5001441f9d7","name":"Copesen","flags":0,"timestamp":1673795184010,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"603cbdbc73d7a5001441f9d7","name":"Copesen","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"603cb87696832ffa78d57767","username":"obscurelambda","display_name":"obscurelambda","avatar_url":"//cdn.7tv.app/user/603cb87696832ffa78d57767/av_647a68a9804ca09ebf23e890/3x.webp","style":{"color":849892095},"roles":["60724f65e93d828bf8858789","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/603cbdbc73d7a5001441f9d7","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1332,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1146,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2970,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2694,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":5148,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4106,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5850,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":7446,"format":"WEBP"}]}}},{"id":"61485c0e1eb7078240526fd9","name":"PirateJam","flags":0,"timestamp":1673802627267,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"61485c0e1eb7078240526fd9","name":"PirateJam","flags":0,"tags":["pirate","jam","piratejam"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60e2f07aa940e09428233bf8","username":"largaas_","display_name":"Largaas_","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/eb90faf6-01da-427c-b869-8e4dffef997f-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61485c0e1eb7078240526fd9","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":4,"size":3752,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":4,"size":4198,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":4,"size":6582,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":4,"size":9414,"format":"WEBP"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":4,"size":15896,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":4,"size":10048,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":4,"size":14136,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":4,"size":19124,"format":"WEBP"}]}}},{"id":"60ae6c1386fc40d488e1ebf9","name":"BRUH","flags":0,"timestamp":1673881461362,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60ae6c1386fc40d488e1ebf9","name":"BRUH","flags":0,"lifecycle":3,"state":["NO_PERSONAL","LISTED"],"listed":true,"animated":false,"owner":{"id":"6068a623452cea46853f9bba","username":"telvann","display_name":"telvann","avatar_url":"//cdn.7tv.app/pp/6068a623452cea46853f9bba/c1926e040ac0414c949438242dd77d4a","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60ae6c1386fc40d488e1ebf9","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1383,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1100,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2761,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2786,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4118,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4834,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5415,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":5364,"format":"WEBP"}]}}},{"id":"60b6e6d45d373afbd69c2d45","name":"HEWILLNEVER","flags":0,"timestamp":1673881515389,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60b6e6d45d373afbd69c2d45","name":"HEWILLNEVER","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60a2ed47ac2bcb20ef076038","username":"bandy_j","display_name":"bandy_j","avatar_url":"//cdn.7tv.app/user/60a2ed47ac2bcb20ef076038/av_63d1b23e9db7d93a1a304fba/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60b6e6d45d373afbd69c2d45","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":2,"size":2591,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":2,"size":1430,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":2,"size":4077,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":2,"size":3424,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":2,"size":5670,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":2,"size":5404,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":2,"size":7488,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":2,"size":6690,"format":"WEBP"}]}}},{"id":"621e1cdfb027edd02c8b6f09","name":"forsenPossessed","flags":0,"timestamp":1673967880711,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"621e1cdfb027edd02c8b6f09","name":"forsenPossessed","flags":0,"tags":["forsen","insane","asylum","possessed"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"613f3cbd7b14fdf700b8ba35","username":"gyoubu_masataka_oniwaaaaa","display_name":"gyoubu_masataka_oniwaaaaa","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/8f4d5ea6-6b18-4670-badd-b22dc463f21d-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/621e1cdfb027edd02c8b6f09","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":51,"size":22614,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":51,"size":32478,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":51,"size":42154,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":51,"size":67724,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":51,"size":66767,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":51,"size":110434,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":51,"size":89593,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":51,"size":130390,"format":"WEBP"}]}}},{"id":"60afcde452a13d1adba73d29","name":"FeelsLagMan","flags":0,"timestamp":1673971753978,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60afcde452a13d1adba73d29","name":"FeelsLagMan","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae6d202a346b32e302acdb","username":"defyyy_","display_name":"Defyyy_","avatar_url":"//cdn.7tv.app/pp/60ae6d202a346b32e302acdb/9bd147d3e37a4700961c86ab8a07958a","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60afcde452a13d1adba73d29","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":179,"size":50937,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":179,"size":159250,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":179,"size":133032,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":179,"size":370112,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":179,"size":237184,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":179,"size":631060,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":179,"size":408068,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":179,"size":723108,"format":"WEBP"}]}}},{"id":"603cb2dbc20d020014423c3c","name":"MEGALUL","flags":0,"timestamp":1674054310926,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"603cb2dbc20d020014423c3c","name":"MEGALUL","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"6394d00e986cf8ade3a7b82e","username":"ydyote","display_name":"ydyote","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/b6617b5f-a688-4f2c-bee2-6f7fbc01b917-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/603cb2dbc20d020014423c3c","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":34,"height":32,"frame_count":1,"size":1613,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":34,"height":32,"frame_count":1,"size":1218,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":68,"height":64,"frame_count":1,"size":3014,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":68,"height":64,"frame_count":1,"size":3210,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":102,"height":96,"frame_count":1,"size":5256,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":102,"height":96,"frame_count":1,"size":4938,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":136,"height":128,"frame_count":1,"size":6714,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":136,"height":128,"frame_count":1,"size":7740,"format":"WEBP"}]}}},{"id":"61a1368ce9684edbbc36f4ad","name":"SOCCER","flags":0,"timestamp":1674054311179,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"61a1368ce9684edbbc36f4ad","name":"SOCCER","flags":0,"tags":["nmyn","soccer","football","ball"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60b6746f64faf92496330d3c","username":"littlescampi","display_name":"LittleScampi","avatar_url":"//cdn.7tv.app/pp/60b6746f64faf92496330d3c/a1ff043123944a119d69cf9a7062a442","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61a1368ce9684edbbc36f4ad","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1205,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1058,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2542,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2884,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3943,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4948,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5762,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":7534,"format":"WEBP"}]}}},{"id":"60e7328e484ebd628b556b3e","name":"Okayge","flags":0,"timestamp":1674132897811,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60e7328e484ebd628b556b3e","name":"Okayge","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60b2a8d9be695c536f66179e","username":"venceslavsquare","display_name":"VenceslavSquare","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/9bce07d2-d3f9-4977-8e17-028ede768a35-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60e7328e484ebd628b556b3e","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1320,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":966,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2480,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2398,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3774,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4174,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5227,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":6236,"format":"WEBP"}]}}},{"id":"629fa7bb2b24f7ba48b6e6c4","name":"WHAT","flags":0,"timestamp":1674140710609,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"629fa7bb2b24f7ba48b6e6c4","name":"WHAT","flags":0,"tags":["what","emotiguy","shocked","shock"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"612d2ba3cffebf58c0aa8bfb","username":"kalorxd","display_name":"kalorxd","avatar_url":"//cdn.7tv.app/pp/612d2ba3cffebf58c0aa8bfb/68960057576f4b72a9e3d943819eaaf1","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/629fa7bb2b24f7ba48b6e6c4","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":47,"height":32,"frame_count":55,"size":13622,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":47,"height":32,"frame_count":55,"size":80062,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":94,"height":64,"frame_count":55,"size":31727,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":94,"height":64,"frame_count":55,"size":189190,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":141,"height":96,"frame_count":55,"size":54806,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":141,"height":96,"frame_count":55,"size":334118,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":188,"height":128,"frame_count":55,"size":86488,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":188,"height":128,"frame_count":55,"size":505904,"format":"WEBP"}]}}},{"id":"60aea8c73c27a8b79cc0c510","name":"YABBECANYOUBRINGMESOMEFOODPLEASE","flags":0,"timestamp":1674140710876,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60aea8c73c27a8b79cc0c510","name":"YABBECANYOUBRINGMESOMEFOODPLEASE","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae518c0e35477634c151f1","username":"fabulouspotato69","display_name":"FabulousPotato69","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60aea8c73c27a8b79cc0c510","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":23,"size":15698,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":23,"size":28668,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":23,"size":34031,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":23,"size":63172,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":23,"size":62526,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":23,"size":103594,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":23,"size":94302,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":23,"size":115934,"format":"WEBP"}]}}},{"id":"603cb2d7c20d020014423c3b","name":"MegaLUL","flags":0,"timestamp":1674140711208,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"603cb2d7c20d020014423c3b","name":"MegaLUL","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"603cb1c696832ffa78cc3bc2","username":"clyvere","display_name":"clyverE","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/3ff40972-0188-4cfc-adbf-8db119d7cf2a-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/603cb2d7c20d020014423c3b","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1678,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1252,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":3870,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":3190,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":5891,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":5646,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":8784,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":8298,"format":"WEBP"}]}}},{"id":"60fdd36e5ab6dc5bc4b1a7f8","name":"walterSmile","flags":0,"timestamp":1674144470885,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"60fdd36e5ab6dc5bc4b1a7f8","name":"walterSmile","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60b5481a2551fd3c8c239622","username":"isabelcoolaf","display_name":"isabelcoolaf","avatar_url":"//cdn.7tv.app/user/60b5481a2551fd3c8c239622/av_639243dbd1b0228466bcefae/3x_static.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60fdd36e5ab6dc5bc4b1a7f8","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":33,"height":32,"frame_count":1,"size":1286,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":33,"height":32,"frame_count":1,"size":1128,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":66,"height":64,"frame_count":1,"size":2330,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":66,"height":64,"frame_count":1,"size":2846,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":99,"height":96,"frame_count":1,"size":3389,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":99,"height":96,"frame_count":1,"size":4682,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":132,"height":128,"frame_count":1,"size":4635,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":132,"height":128,"frame_count":1,"size":6592,"format":"WEBP"}]}}},{"id":"62ac06d1604faf8634221574","name":"PotFaint","flags":0,"timestamp":1674226756142,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"62ac06d1604faf8634221574","name":"PotFaint","flags":0,"tags":["potfriend","pot","eldenring","faint","bruhfaint","brorbesvimer"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60b132f3bdea6753986d0208","username":"alastorkunn","display_name":"alastorkunn","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/c7d7cd45-054f-4dfd-ba8d-12a3347030d8-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/62ac06d1604faf8634221574","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":6,"size":5465,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":6,"size":5440,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":6,"size":9378,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":6,"size":11288,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":6,"size":13909,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":6,"size":18838,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":6,"size":18305,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":6,"size":25010,"format":"WEBP"}]}}},{"id":"637ce74f58f8ed425904bd51","name":"lookUp","flags":0,"timestamp":1674227244378,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"637ce74f58f8ed425904bd51","name":"lookUp","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60afa8c899923bbe7f6e5a33","username":"trippycolour","display_name":"TrippyColour","avatar_url":"//cdn.7tv.app/user/60afa8c899923bbe7f6e5a33/av_6592e20b64e6ee62744a436c/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/637ce74f58f8ed425904bd51","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1323,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1212,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2423,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2710,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3494,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4426,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4576,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":6454,"format":"WEBP"}]}}},{"id":"635206405029098c3d6e1c6c","name":"TriKool","flags":0,"timestamp":1674227271735,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"635206405029098c3d6e1c6c","name":"TriKool","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"603c7fca96832ffa788a5f14","username":"hyruverse","display_name":"hyruverse","avatar_url":"//cdn.7tv.app/pp/603c7fca96832ffa788a5f14/2ed3bd237882444ebccf38ae918e8df6","style":{"color":849892095},"roles":["60724f65e93d828bf8858789","6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/635206405029098c3d6e1c6c","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":14,"size":5955,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":14,"size":9694,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":14,"size":17278,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":14,"size":8895,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":14,"size":24940,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":14,"size":12899,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":14,"size":16801,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":14,"size":32434,"format":"WEBP"}]}}},{"id":"61a7c0e5e9684edbbc37d13a","name":"forsenGa","flags":0,"timestamp":1674227475843,"actor_id":"60ae8fc0ea50f43c9e3ae255","data":{"id":"61a7c0e5e9684edbbc37d13a","name":"forsenGa","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60aebf0ae90f445e43b37fe5","username":"prog0ldfish","display_name":"ProG0ldfish","avatar_url":"//cdn.7tv.app/user/60aebf0ae90f445e43b37fe5/av_6353ef0d7f642dee0e30c1f4/3x_static.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61a7c0e5e9684edbbc37d13a","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":28,"height":32,"frame_count":1,"size":1036,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":28,"height":32,"frame_count":1,"size":704,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":56,"height":64,"frame_count":1,"size":1588,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":56,"height":64,"frame_count":1,"size":1600,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":84,"height":96,"frame_count":1,"size":2342,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":84,"height":96,"frame_count":1,"size":2602,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":112,"height":128,"frame_count":1,"size":3038,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":112,"height":128,"frame_count":1,"size":3570,"format":"WEBP"}]}}},{"id":"60f11a7d7affbddfe71361ed","name":"forsenLaughingAtYou","flags":0,"timestamp":1674313510772,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60f11a7d7affbddfe71361ed","name":"forsenLaughingAtYou","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60aeaf804b1ea4526d77e0f9","username":"wdeweisheim","display_name":"WDEWEISHEIM","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/55de63ea-64ab-48b4-89e2-1864ce194b88-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60f11a7d7affbddfe71361ed","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":72,"size":22427,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":72,"size":50854,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":72,"size":46130,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":72,"size":109892,"format":"WEBP"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":72,"size":186096,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":72,"size":83298,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":72,"size":216466,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":72,"size":133543,"format":"AVIF"}]}}},{"id":"60ae89c64b1ea4526d9244b5","name":"ABDULpls","flags":0,"timestamp":1674313511041,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60ae89c64b1ea4526d9244b5","name":"ABDULpls","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"6330b76d8c374fc092ced2be","username":"abdulhd","display_name":"AbdulHD","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/ceb3037e-d440-46c0-9f6e-6278000a7383-profile_image-70x70.png","style":{"color":-1857617921},"roles":["6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60ae89c64b1ea4526d9244b5","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":10,"size":7784,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":10,"size":9624,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":10,"size":15891,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":10,"size":21610,"format":"WEBP"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":10,"size":35388,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":10,"size":25019,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":10,"size":34382,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":10,"size":36342,"format":"WEBP"}]}}},{"id":"603ea168284626000d068881","name":"KKrikey","flags":0,"timestamp":1674326030726,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"603ea168284626000d068881","name":"KKrikey","flags":0,"lifecycle":3,"state":["PERSONAL","LISTED"],"listed":true,"animated":false,"owner":{"id":"603cac0896832ffa78c463e1","username":"rupusen","display_name":"rupusen","avatar_url":"//static-cdn.jtvnw.net/user-default-pictures-uv/998f01ae-def8-11e9-b95c-784f43822e80-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/603ea168284626000d068881","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1437,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1134,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":3038,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2922,"format":"WEBP"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":5300,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4733,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":7864,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":7037,"format":"AVIF"}]}}},{"id":"6108e194569a3002abab0223","name":"forsenLevel","flags":0,"timestamp":1674399940611,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"6108e194569a3002abab0223","name":"forsenLevel","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"6118deac50795b4ba00cee36","username":"seloxyyz","display_name":"seloxyyz","avatar_url":"//static-cdn.jtvnw.net/user-default-pictures-uv/cdd517fe-def4-11e9-948e-784f43822e80-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6108e194569a3002abab0223","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1139,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":870,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1892,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":1858,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":2743,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":2984,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":3414,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":4116,"format":"WEBP"}]}}},{"id":"603cb9fd73d7a5001441f9b4","name":"Pepepains","flags":0,"timestamp":1674399940881,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"603cb9fd73d7a5001441f9b4","name":"Pepepains","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"603ca8f696832ffa78c01eb4","username":"mauriplss","display_name":"Mauriplss","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/a428f0f0-bdd4-4c93-ac4b-ed174244cb66-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/603cb9fd73d7a5001441f9b4","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1274,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1110,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2460,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2620,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3780,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4456,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5114,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":6542,"format":"WEBP"}]}}},{"id":"60ae31deaee2aa5538d2971c","name":"ppHop","flags":0,"timestamp":1674399941176,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60ae31deaee2aa5538d2971c","name":"ppHop","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60635b50452cea4685f26b34","username":"hecrzy","display_name":"heCrzy","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/583dd5ac-2fe8-4ead-a20d-e10770118c5f-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60ae31deaee2aa5538d2971c","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":39,"size":7809,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":39,"size":11774,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":39,"size":9466,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":39,"size":19834,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":39,"size":14131,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":39,"size":28090,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":39,"size":16044,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":39,"size":28138,"format":"WEBP"}]}}},{"id":"60b0e3cb7500a64f7c0ba32d","name":"peepoPizza","flags":0,"timestamp":1674477087372,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60b0e3cb7500a64f7c0ba32d","name":"peepoPizza","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60b0ca28b254a5e16b9e2db4","username":"khorsow","display_name":"khorsow","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/557100ee-5aa2-4b00-960c-8e734d471c5c-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60b0e3cb7500a64f7c0ba32d","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":6,"size":3509,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":6,"size":4680,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":6,"size":5808,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":6,"size":10354,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":6,"size":8747,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":6,"size":16606,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":6,"size":12610,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":6,"size":18720,"format":"WEBP"}]}}},{"id":"61ebede31a1b2a6e7324d897","name":"Life","flags":0,"timestamp":1674486348937,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"61ebede31a1b2a6e7324d897","name":"Life","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60ae518c0e35477634c151f1","username":"fabulouspotato69","display_name":"FabulousPotato69","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61ebede31a1b2a6e7324d897","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1131,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":686,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2311,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":1888,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3629,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":3400,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5306,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":5834,"format":"WEBP"}]}}},{"id":"62ce92d97025c7defe8c9fd2","name":"DankL","flags":0,"timestamp":1674486349486,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"62ce92d97025c7defe8c9fd2","name":"DankL","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"611d40d4e8715718f9917602","username":"the_balla_koala","display_name":"The_Balla_Koala","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/the_balla_koala-profile_image-1ffb68433553158f-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/62ce92d97025c7defe8c9fd2","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":138,"size":24919,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":138,"size":128898,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":138,"size":46671,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":138,"size":291590,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":138,"size":74908,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":138,"size":467016,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":138,"size":110255,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":138,"size":630368,"format":"WEBP"}]}}},{"id":"6169fc05c52da56cd4908d79","name":"weirdPaper","flags":0,"timestamp":1674572779250,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"6169fc05c52da56cd4908d79","name":"weirdPaper","flags":0,"tags":["weird","paper","monka","pepe","pepega","weirdchamp"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"61465bf80969108b671966fd","username":"eiti3","display_name":"Eiti3","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/9c658e7a-a6b0-4efc-be37-c67816639f15-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6169fc05c52da56cd4908d79","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":31,"height":32,"frame_count":1,"size":1432,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":31,"height":32,"frame_count":1,"size":1122,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":62,"height":64,"frame_count":1,"size":2948,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":62,"height":64,"frame_count":1,"size":2986,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":93,"height":96,"frame_count":1,"size":4468,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":93,"height":96,"frame_count":1,"size":5270,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":124,"height":128,"frame_count":1,"size":6148,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":124,"height":128,"frame_count":1,"size":8192,"format":"WEBP"}]}}},{"id":"60ae50320e35477634a5b5a0","name":"PauseMan","flags":0,"timestamp":1674572779522,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60ae50320e35477634a5b5a0","name":"PauseMan","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60ae5fb65d6c7fc0fa5b1e2c","username":"leekroom","display_name":"leekroom","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/fb847501-0636-4928-a577-3a7ea501b87c-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60ae50320e35477634a5b5a0","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1174,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":870,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1980,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":1914,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":2954,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":3158,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":3624,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":3758,"format":"WEBP"}]}}},{"id":"638359acbf3af4e79c91c521","name":"HUH","flags":0,"timestamp":1674659178792,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"638359acbf3af4e79c91c521","name":"apolloHUH","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60ae8fc0ea50f43c9e3ae255","username":"agenttud","display_name":"agenttud","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/273db808-d42f-4dab-9b39-9780ef2777b0-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/638359acbf3af4e79c91c521","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":33,"height":32,"frame_count":1,"size":1954,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":33,"height":32,"frame_count":1,"size":1242,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":66,"height":64,"frame_count":1,"size":5744,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":66,"height":64,"frame_count":1,"size":2240,"format":"AVIF"},{"name":"3x.avif","static_name":"3x_static.avif","width":99,"height":96,"frame_count":1,"size":3314,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":99,"height":96,"frame_count":1,"size":10866,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":132,"height":128,"frame_count":1,"size":4497,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":132,"height":128,"frame_count":1,"size":17574,"format":"WEBP"}]}}},{"id":"60b248477e6072867ba50758","name":"UHM","flags":0,"timestamp":1674659179061,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60b248477e6072867ba50758","name":"Uhm","flags":0,"tags":["uhm","awkward","huh","bruh"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60a7c5c1ad0a5c63ef23bf5d","username":"was1max","display_name":"was1max","avatar_url":"//cdn.7tv.app/pp/60a7c5c1ad0a5c63ef23bf5d/72a8daff0f3744c0aa5184eb424ac711","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60b248477e6072867ba50758","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":128,"size":23451,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":128,"size":85412,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":128,"size":46658,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":128,"size":171628,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":128,"size":90571,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":128,"size":263398,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":128,"size":129145,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":128,"size":286200,"format":"WEBP"}]}}},{"id":"60420e7677137b000de9e677","name":"PepeS","flags":0,"timestamp":1674659179352,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60420e7677137b000de9e677","name":"pepeS","flags":0,"lifecycle":3,"state":["PERSONAL","LISTED"],"listed":true,"animated":true,"owner":{"id":"603ca8f696832ffa78c01eb4","username":"mauriplss","display_name":"Mauriplss","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/a428f0f0-bdd4-4c93-ac4b-ed174244cb66-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60420e7677137b000de9e677","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":10,"size":4882,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":10,"size":9210,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":10,"size":6684,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":10,"size":14982,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":10,"size":9059,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":10,"size":21934,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":10,"size":9078,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":10,"size":22910,"format":"WEBP"}]}}},{"id":"60a7c6d74d83ca509fc737a2","name":"docArrive","flags":0,"timestamp":1674745579169,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60a7c6d74d83ca509fc737a2","name":"docArrive","flags":0,"tags":["docing","docarrive","docleave","doc"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60a7c5c1ad0a5c63ef23bf5d","username":"was1max","display_name":"was1max","avatar_url":"//cdn.7tv.app/pp/60a7c5c1ad0a5c63ef23bf5d/72a8daff0f3744c0aa5184eb424ac711","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60a7c6d74d83ca509fc737a2","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":153,"size":51855,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":153,"size":112968,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":153,"size":139033,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":153,"size":257016,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":153,"size":224032,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":153,"size":417376,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":153,"size":335565,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":153,"size":456610,"format":"WEBP"}]}}},{"id":"62da8d42c9fe72853564b4f8","name":"DIESOFCRINGE","flags":0,"timestamp":1674745579443,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"62da8d42c9fe72853564b4f8","name":"DIESOFCRINGE","flags":0,"tags":["meow","cat","diesofcringe","kitten"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae3e3eb2ecb01505346ae9","username":"fawcan","display_name":"Fawcan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38051bdb-83c6-4716-95e0-731462e02b45-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/62da8d42c9fe72853564b4f8","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":55,"height":32,"frame_count":120,"size":44573,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":55,"height":32,"frame_count":120,"size":145042,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":110,"height":64,"frame_count":120,"size":99668,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":110,"height":64,"frame_count":120,"size":313876,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":165,"height":96,"frame_count":120,"size":160871,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":165,"height":96,"frame_count":120,"size":493794,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":220,"height":128,"frame_count":120,"size":229565,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":220,"height":128,"frame_count":120,"size":692496,"format":"WEBP"}]}}},{"id":"60af206912d77014919c5ba6","name":"Gondola2","flags":0,"timestamp":1674747679380,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"60af206912d77014919c5ba6","name":"gondola2","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60aebde86cfcffe15fea447c","username":"s4tisfaction_","display_name":"s4tisfaction_","avatar_url":"//cdn.7tv.app/pp/60aebde86cfcffe15fea447c/f42ff54ebf7a47f5912b7aaa39af7287","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60af206912d77014919c5ba6","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":36,"size":12110,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":36,"size":23506,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":36,"size":24315,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":36,"size":49578,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":36,"size":36969,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":36,"size":77974,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":36,"size":56414,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":36,"size":90862,"format":"WEBP"}]}}},{"id":"60a551839485e7cf2f683cd2","name":"DonkPls","flags":0,"timestamp":1674832009007,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60a551839485e7cf2f683cd2","name":"DonkPls","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"609e2e159aa3ab64eb6a5129","username":"bigmeg_","display_name":"Bigmeg_","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/70ffe2a1-8246-4328-9fde-4e5463d63616-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60a551839485e7cf2f683cd2","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":29,"height":32,"frame_count":10,"size":10976,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":29,"height":32,"frame_count":10,"size":10504,"format":"AVIF"},{"name":"2x.avif","static_name":"2x_static.avif","width":58,"height":64,"frame_count":10,"size":20927,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":58,"height":64,"frame_count":10,"size":24554,"format":"WEBP"},{"name":"3x.webp","static_name":"3x_static.webp","width":87,"height":96,"frame_count":10,"size":40340,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":87,"height":96,"frame_count":10,"size":34399,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":116,"height":128,"frame_count":10,"size":46778,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":116,"height":128,"frame_count":10,"size":55060,"format":"WEBP"}]}}},{"id":"6230f826fe73af690d656eac","name":"Plotge","flags":0,"timestamp":1674832009265,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"6230f826fe73af690d656eac","name":"Plotge","flags":0,"tags":["gambage","suskage","sus","plotting","plot"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"614944771eb7078240528ea8","username":"esuesuesuuu","display_name":"EsuEsuEsuuu","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/67daa8bb-e4a8-4cbe-8c36-4fb4b43b9728-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6230f826fe73af690d656eac","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":10,"size":7214,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":10,"size":4834,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":10,"size":16332,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":10,"size":8867,"format":"AVIF"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":10,"size":15138,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":10,"size":26800,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":10,"size":24553,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":10,"size":30886,"format":"WEBP"}]}}},{"id":"619209bc17e4d50afc0d9619","name":"HandsUp","flags":0,"timestamp":1674834140721,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"619209bc17e4d50afc0d9619","name":"HandsUp","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60aebf0ae90f445e43b37fe5","username":"prog0ldfish","display_name":"ProG0ldfish","avatar_url":"//cdn.7tv.app/user/60aebf0ae90f445e43b37fe5/av_6353ef0d7f642dee0e30c1f4/3x_static.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/619209bc17e4d50afc0d9619","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":38,"height":32,"frame_count":1,"size":1496,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":38,"height":32,"frame_count":1,"size":1216,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":76,"height":64,"frame_count":1,"size":3178,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":76,"height":64,"frame_count":1,"size":2920,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":114,"height":96,"frame_count":1,"size":5016,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":114,"height":96,"frame_count":1,"size":5324,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":152,"height":128,"frame_count":1,"size":7122,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":152,"height":128,"frame_count":1,"size":8520,"format":"WEBP"}]}}},{"id":"61808bf4c632476d20d0c7c0","name":"bruhSit","flags":0,"timestamp":1674843292115,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"61808bf4c632476d20d0c7c0","name":"BRUHSIT","flags":0,"tags":["sit","bruh"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60f1507dd8c44ac3f3f511c8","username":"igor_mec","display_name":"Igor_mec","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/60e7d159-c116-4ce8-b245-2456b1a66554-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61808bf4c632476d20d0c7c0","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1472,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1148,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2813,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2686,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4309,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4552,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5642,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":6524,"format":"WEBP"}]}}},{"id":"6133b422d6b0df560a6525b2","name":"TakingNotes","flags":1,"timestamp":1674918408777,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"6133b422d6b0df560a6525b2","name":"TakingNotes","flags":256,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae81ff0bf2ee96aea05247","username":"snortexx","display_name":"snortexx","avatar_url":"//cdn.7tv.app/pp/60ae81ff0bf2ee96aea05247/183b9b6ab7624a53966fb782ec0963e0","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6133b422d6b0df560a6525b2","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":10,"size":3863,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":10,"size":6504,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":10,"size":5813,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":10,"size":13114,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":10,"size":9337,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":10,"size":20634,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":10,"size":12915,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":10,"size":22472,"format":"WEBP"}]}}},{"id":"638767f24cc489ef45239272","name":"peepoShy","flags":0,"timestamp":1674918409164,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"638767f24cc489ef45239272","name":"peepoShy","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"6053853cb4d31e459fdaa2dc","username":"laden","display_name":"Laden","avatar_url":"//cdn.7tv.app/pp/6053853cb4d31e459fdaa2dc/a94c67d7736940feb543e42024b740ef","style":{"color":849892095},"roles":["60724f65e93d828bf8858789","6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/638767f24cc489ef45239272","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":25,"size":4347,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":6,"size":1906,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":25,"size":6219,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":25,"size":28146,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":25,"size":8577,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":25,"size":52762,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":25,"size":10997,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":25,"size":74752,"format":"WEBP"}]}}},{"id":"615fc15e03c9e8ba70eb7d61","name":"GachiPls","flags":0,"timestamp":1674918409425,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"615fc15e03c9e8ba70eb7d61","name":"GachiPls","flags":0,"tags":["gachi","kazuya","gachipls"],"lifecycle":3,"state":["PERSONAL","LISTED"],"listed":true,"animated":true,"owner":{"id":"60afe7defd9839f62d6c87ea","username":"onyxclockwork","display_name":"onyxclockwork","avatar_url":"//cdn.7tv.app/pp/60afe7defd9839f62d6c87ea/cf5aa2f1219646fabb37425c0eeefd96","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/615fc15e03c9e8ba70eb7d61","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":29,"height":32,"frame_count":30,"size":18603,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":29,"height":32,"frame_count":30,"size":28226,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":58,"height":64,"frame_count":30,"size":41377,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":58,"height":64,"frame_count":30,"size":67690,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":87,"height":96,"frame_count":30,"size":70686,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":87,"height":96,"frame_count":30,"size":117788,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":116,"height":128,"frame_count":30,"size":104365,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":116,"height":128,"frame_count":30,"size":151586,"format":"WEBP"}]}}},{"id":"62dbd0fa0a430aad0143c1f4","name":"Vacatime","flags":0,"timestamp":1674936736757,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"62dbd0fa0a430aad0143c1f4","name":"Vacatime","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae518c0e35477634c151f1","username":"fabulouspotato69","display_name":"FabulousPotato69","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/62dbd0fa0a430aad0143c1f4","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1484,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1072,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":3090,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2654,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4618,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4482,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":6681,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":6812,"format":"WEBP"}]}}},{"id":"6192d816d34608492cc36ef6","name":"Smadging","flags":0,"timestamp":1675004839676,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"6192d816d34608492cc36ef6","name":"Smadging","flags":0,"tags":["smadge","madge","cry","chatting"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60b69a59fb0dd24047d466cc","username":"wokaa","display_name":"woKaa","avatar_url":"//cdn.7tv.app/pp/60b69a59fb0dd24047d466cc/88c94384e4404d329a68cf5f54245a0c","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6192d816d34608492cc36ef6","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":10,"size":4103,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":10,"size":5362,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":10,"size":7561,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":10,"size":12872,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":10,"size":11870,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":10,"size":20282,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":10,"size":20109,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":10,"size":24066,"format":"WEBP"}]}}},{"id":"60bc8bb7824feec0de8f94cc","name":"stopbeingMean","flags":0,"timestamp":1675004842346,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60bc8bb7824feec0de8f94cc","name":"stopbeingMean","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60b64822f48dd943d523a741","username":"bubbles_2","display_name":"bubbles_2","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38be6581-853f-4c3c-ae3a-efd27f6101e5-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60bc8bb7824feec0de8f94cc","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1286,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":894,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2334,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2122,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3486,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":3448,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4400,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":3926,"format":"WEBP"}]}}},{"id":"61e2c53f095be332e3475ad4","name":"EatTheMinus","flags":0,"timestamp":1675004843753,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"61e2c53f095be332e3475ad4","name":"EatTheMinus","flags":0,"tags":["nymn","apollo","minus","yabbe"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae3e3eb2ecb01505346ae9","username":"fawcan","display_name":"Fawcan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38051bdb-83c6-4716-95e0-731462e02b45-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61e2c53f095be332e3475ad4","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":47,"height":32,"frame_count":75,"size":22588,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":47,"height":32,"frame_count":75,"size":63402,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":94,"height":64,"frame_count":75,"size":56548,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":94,"height":64,"frame_count":75,"size":143854,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":141,"height":96,"frame_count":75,"size":101350,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":141,"height":96,"frame_count":75,"size":230886,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":188,"height":128,"frame_count":75,"size":155320,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":188,"height":128,"frame_count":75,"size":166116,"format":"WEBP"}]}}},{"id":"60ae84eb4b1ea4526d5bc117","name":"4WeirdBusiness","flags":0,"timestamp":1675031853799,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"60ae84eb4b1ea4526d5bc117","name":"4WeirdBusiness","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"63c2e401219a2920cb344dd7","username":"cnys_","display_name":"Cnys_","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/7bcc5b32-99e9-41ab-a8af-e03781a6f20c-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60ae84eb4b1ea4526d5bc117","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":33,"height":32,"frame_count":1,"size":1260,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":33,"height":32,"frame_count":1,"size":1148,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":66,"height":64,"frame_count":1,"size":2331,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":66,"height":64,"frame_count":1,"size":2742,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":99,"height":96,"frame_count":1,"size":3453,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":99,"height":96,"frame_count":1,"size":4516,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":132,"height":128,"frame_count":1,"size":4363,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":132,"height":128,"frame_count":1,"size":6178,"format":"WEBP"}]}}},{"id":"60af9bd160e24df01a93bcdd","name":"ILUVU","flags":0,"timestamp":1675091238922,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60af9bd160e24df01a93bcdd","name":"ILUVU","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60a536d1ac08622846bced71","username":"marcfryd_0","display_name":"marcfryd_0","avatar_url":"//cdn.7tv.app/user/60a536d1ac08622846bced71/av_63537c8f28e6aaaea2bb599e/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","631ef5ea03e9beb96f849a7e","6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60af9bd160e24df01a93bcdd","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":10,"size":5379,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":10,"size":10808,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":10,"size":10437,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":10,"size":22896,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":10,"size":16722,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":10,"size":36670,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":10,"size":24566,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":10,"size":39676,"format":"WEBP"}]}}},{"id":"619002c1b1eb03daac7d997d","name":"PoroHappy","flags":0,"timestamp":1675091239188,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"619002c1b1eb03daac7d997d","name":"PoroHappy","flags":0,"tags":["poro","porosad","league","lol"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60879d10fcf1f9923f6e1573","username":"somso2e","display_name":"Somso2e","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/7291e0ba-abe4-4928-9951-6becee40fb61-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/619002c1b1eb03daac7d997d","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1374,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1264,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":3030,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":3518,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":5069,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":6276,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":7650,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":9780,"format":"WEBP"}]}}},{"id":"60db66aa9a9fbb6acd8351c1","name":"4Love","flags":0,"timestamp":1675101761339,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60db66aa9a9fbb6acd8351c1","name":"4Love","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60b1b50be67fbb9dd7195bba","username":"zomballr","display_name":"zomballr","avatar_url":"//cdn.7tv.app/user/60b1b50be67fbb9dd7195bba/av_637eac05d85f3c9574d6e959/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60db66aa9a9fbb6acd8351c1","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1160,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1421,"format":"AVIF"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2595,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2656,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3720,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4308,"format":"WEBP"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":6100,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4946,"format":"AVIF"}]}}},{"id":"60714545dcae02001b44e527","name":"MaN","flags":0,"timestamp":1675177668772,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60714545dcae02001b44e527","name":"MaN","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"6071266ebde0639989dc5150","username":"quinndt","display_name":"QuinnDT","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/8dd6c04f-804b-4abe-a1a7-d040b45f1cc0-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60714545dcae02001b44e527","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":26,"height":32,"frame_count":1,"size":1014,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":26,"height":32,"frame_count":1,"size":1276,"format":"AVIF"},{"name":"2x.avif","static_name":"2x_static.avif","width":52,"height":64,"frame_count":1,"size":2514,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":52,"height":64,"frame_count":1,"size":2632,"format":"WEBP"},{"name":"3x.webp","static_name":"3x_static.webp","width":78,"height":96,"frame_count":1,"size":4818,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":78,"height":96,"frame_count":1,"size":4132,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":104,"height":128,"frame_count":1,"size":6337,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":104,"height":128,"frame_count":1,"size":7444,"format":"WEBP"}]}}},{"id":"603cb8be73d7a5001441f9ad","name":"arnoldHalt","flags":0,"timestamp":1675177669080,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"603cb8be73d7a5001441f9ad","name":"arnoldHalt","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"603ca71d96832ffa78bd7e2c","username":"grakanutyun","display_name":"grakanutyun","avatar_url":"//static-cdn.jtvnw.net/user-default-pictures-uv/dbdc9198-def8-11e9-8681-784f43822e80-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/603cb8be73d7a5001441f9ad","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1421,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1218,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":3010,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":3072,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4752,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":5424,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":6974,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":7920,"format":"WEBP"}]}}},{"id":"615321b443b2d9da0d32d157","name":"DankWave","flags":0,"timestamp":1675177669372,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"615321b443b2d9da0d32d157","name":"dankWave","flags":0,"tags":["feelsdankman","waving","cute"],"lifecycle":3,"state":["PERSONAL","LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae2af4aee2aa5538ab2144","username":"sunred_","display_name":"SunRed_","avatar_url":"//cdn.7tv.app/pp/60ae2af4aee2aa5538ab2144/acc28924022046e3b790ccaf4c7b4c53","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/615321b443b2d9da0d32d157","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":10,"size":4264,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":10,"size":4854,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":10,"size":6899,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":10,"size":8902,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":10,"size":11049,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":10,"size":14898,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":10,"size":14943,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":10,"size":15610,"format":"WEBP"}]}}},{"id":"60cfa860ca263e7ca4de398a","name":"AREYOUAGIRL","flags":0,"timestamp":1675199071943,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"60cfa860ca263e7ca4de398a","name":"AREYOUAGIRL","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"603c7fca96832ffa788a5f14","username":"hyruverse","display_name":"hyruverse","avatar_url":"//cdn.7tv.app/pp/603c7fca96832ffa788a5f14/2ed3bd237882444ebccf38ae918e8df6","style":{"color":849892095},"roles":["60724f65e93d828bf8858789","6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60cfa860ca263e7ca4de398a","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":56,"size":19554,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":56,"size":41322,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":56,"size":43570,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":56,"size":85620,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":56,"size":76004,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":56,"size":135318,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":56,"size":110786,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":56,"size":117804,"format":"WEBP"}]}}},{"id":"60aea4074b1ea4526d3c97a9","name":"BOOBA","flags":0,"timestamp":1675264098810,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60aea4074b1ea4526d3c97a9","name":"BOOBA","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae99954b1ea4526d8ac75b","username":"evilmessy","display_name":"evilmessy","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/aaec06da-90ff-46e4-9dfd-ec57221cd405-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60aea4074b1ea4526d3c97a9","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":55,"size":16291,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":55,"size":51756,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":55,"size":35408,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":55,"size":107490,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":55,"size":61335,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":55,"size":172450,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":55,"size":101262,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":55,"size":195802,"format":"WEBP"}]}}},{"id":"62293ad3b027edd02c8c02ca","name":"kek","flags":0,"timestamp":1675264099131,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"62293ad3b027edd02c8c02ca","name":"kek","flags":0,"tags":["kekw","tyler1","league","clm","erobbsbrother","lol"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60bf6023a396a2421e53c937","username":"aanglosaxon","display_name":"aanglosaxon","avatar_url":"//cdn.7tv.app/user/60bf6023a396a2421e53c937/av_6530ad1f1ef5bf2c0c17f934/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/62293ad3b027edd02c8c02ca","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":948,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":662,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1650,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":1646,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":2420,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":2652,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":3479,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":4212,"format":"WEBP"}]}}},{"id":"60ae4c0d5d3fdae583dd938b","name":"Swagging","flags":0,"timestamp":1675264099447,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60ae4c0d5d3fdae583dd938b","name":"Swagging","flags":0,"tags":["fifteen","ellie","swag","tlou","emoney"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae4b445d3fdae583d20e9a","username":"ethantp","display_name":"ethantp","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/617473d7-7627-4bd6-befa-a2ff489d8daa-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60ae4c0d5d3fdae583dd938b","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":232,"size":48943,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":232,"size":169688,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":232,"size":202273,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":232,"size":423008,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":232,"size":380776,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":232,"size":715950,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":232,"size":598520,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":232,"size":549490,"format":"WEBP"}]}}},{"id":"61a91b7315b3ff4a5bb8e72b","name":"Cooking","flags":0,"timestamp":1675276842755,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"61a91b7315b3ff4a5bb8e72b","name":"Cooking","flags":0,"tags":["chatting","forsen"],"lifecycle":3,"state":["PERSONAL","LISTED"],"listed":true,"animated":true,"owner":{"id":"60b0faaa8fb21a01bc3c0385","username":"enzo_supercraftz","display_name":"Enzo_SuperCraftZ","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61a91b7315b3ff4a5bb8e72b","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":10,"size":4858,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":10,"size":5854,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":10,"size":9437,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":10,"size":13746,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":10,"size":15511,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":10,"size":22592,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":10,"size":22254,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":10,"size":27510,"format":"WEBP"}]}}},{"id":"634379257361e04bb26bdb49","name":"coupleofidiots","flags":0,"timestamp":1675284258531,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"634379257361e04bb26bdb49","name":"coupleofidiots","flags":0,"tags":["yabbe","nymn"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60c92a0b043eea6bc36384fe","username":"mrbarnabass","display_name":"MrBarnabass","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/c9130163-31c0-4836-8894-c189b312526a-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/634379257361e04bb26bdb49","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":74,"height":32,"frame_count":1,"size":1807,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":74,"height":32,"frame_count":1,"size":3682,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":148,"height":64,"frame_count":1,"size":3828,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":148,"height":64,"frame_count":1,"size":11828,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":222,"height":96,"frame_count":1,"size":6416,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":222,"height":96,"frame_count":1,"size":24334,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":296,"height":128,"frame_count":1,"size":9085,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":296,"height":128,"frame_count":1,"size":41298,"format":"WEBP"}]}}},{"id":"60fb428d5b7deb3de031df64","name":"AlienLag","flags":0,"timestamp":1675350515194,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60fb428d5b7deb3de031df64","name":"AlienLag","flags":0,"lifecycle":3,"state":["PERSONAL","LISTED"],"listed":true,"animated":true,"owner":{"id":"60dfc16ddd6a810dd453c336","username":"jhonxto","display_name":"jhonxto","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/d4531555-a4e7-40ae-9afb-77fb66081418-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60fb428d5b7deb3de031df64","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":326,"size":41028,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":326,"size":253526,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":326,"size":89644,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":326,"size":619134,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":326,"size":156174,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":326,"size":1113734,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":326,"size":279551,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":326,"size":1396534,"format":"WEBP"}]}}},{"id":"609ee21a326f0aaa859f534f","name":"peepoS","flags":0,"timestamp":1675350515916,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"609ee21a326f0aaa859f534f","name":"peepoS","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"609ede7b4c18609a1d94c5ae","username":"yoim5th","display_name":"yoim5th","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/81c11127-ffa9-4b47-a5b0-7e602a998aae-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/609ee21a326f0aaa859f534f","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":4,"size":3352,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":4,"size":3062,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":4,"size":5548,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":4,"size":6222,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":4,"size":8074,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":4,"size":9792,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":4,"size":11556,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":4,"size":11130,"format":"WEBP"}]}}},{"id":"61360e15b7ef1a05d0a2109c","name":"Love0","flags":1,"timestamp":1675350635011,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"61360e15b7ef1a05d0a2109c","name":"Love","flags":256,"tags":["xqcl","heart"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"603c624796832ffa78678fd7","username":"juanjunki_6969","display_name":"JuanJunki_6969","avatar_url":"//cdn.7tv.app/user/603c624796832ffa78678fd7/av_64df2a212356a20967ed54d9/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61360e15b7ef1a05d0a2109c","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":827,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":528,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":1058,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1238,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":1622,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":1502,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":1919,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":2202,"format":"WEBP"}]}}},{"id":"63c1df8f6c05867c0ce8ace0","name":"vibE","flags":0,"timestamp":1675435783919,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"63c1df8f6c05867c0ce8ace0","name":"vibE","flags":0,"tags":["vibe","moon","forsen"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae3e3eb2ecb01505346ae9","username":"fawcan","display_name":"Fawcan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38051bdb-83c6-4716-95e0-731462e02b45-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63c1df8f6c05867c0ce8ace0","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":34,"size":16373,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":34,"size":18906,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":34,"size":31977,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":34,"size":36908,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":34,"size":50006,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":34,"size":58078,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":34,"size":66579,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":34,"size":68742,"format":"WEBP"}]}}},{"id":"603cc3c62c7b4500143b46c5","name":"hackerCD","flags":0,"timestamp":1675436915491,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"603cc3c62c7b4500143b46c5","name":"hackerCD","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"603cb87696832ffa78d57767","username":"obscurelambda","display_name":"obscurelambda","avatar_url":"//cdn.7tv.app/user/603cb87696832ffa78d57767/av_647a68a9804ca09ebf23e890/3x.webp","style":{"color":849892095},"roles":["60724f65e93d828bf8858789","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/603cc3c62c7b4500143b46c5","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":102,"size":48058,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":102,"size":99406,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":102,"size":124244,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":102,"size":231364,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":102,"size":205680,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":102,"size":387734,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":102,"size":448616,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":102,"size":559896,"format":"WEBP"}]}}},{"id":"610ff4353f3e99ddb4628023","name":"donkDriving","flags":0,"timestamp":1675439678346,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"610ff4353f3e99ddb4628023","name":"DonkDriving","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60aebf0ae90f445e43b37fe5","username":"prog0ldfish","display_name":"ProG0ldfish","avatar_url":"//cdn.7tv.app/user/60aebf0ae90f445e43b37fe5/av_6353ef0d7f642dee0e30c1f4/3x_static.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/610ff4353f3e99ddb4628023","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":2,"size":3412,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":2,"size":2248,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":2,"size":5410,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":2,"size":4916,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":2,"size":7803,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":2,"size":8056,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":2,"size":10429,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":2,"size":9960,"format":"WEBP"}]}}},{"id":"627e843e49607a2d9d9b7589","name":"RightNow","flags":1,"timestamp":1675523345571,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"627e843e49607a2d9d9b7589","name":"RightNow","flags":256,"tags":["rightnow","time"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"622a515dcb65b2eb65c03e9d","username":"oldschoolling","display_name":"OldSchoolling","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/2ac9c0a4-850e-4d8c-a5d6-fd555e1e230f-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/627e843e49607a2d9d9b7589","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":35,"height":32,"frame_count":2,"size":2637,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":35,"height":32,"frame_count":2,"size":1480,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":70,"height":64,"frame_count":2,"size":2820,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":70,"height":64,"frame_count":2,"size":3532,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":105,"height":96,"frame_count":2,"size":4334,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":105,"height":96,"frame_count":2,"size":4648,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":140,"height":128,"frame_count":2,"size":5391,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":140,"height":128,"frame_count":2,"size":5256,"format":"WEBP"}]}}},{"id":"62a27a7fb12d7075e259f113","name":"Concerned","flags":0,"timestamp":1675523345836,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"62a27a7fb12d7075e259f113","name":"Concerned","flags":0,"tags":["clueless","aware"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60b0faaa8fb21a01bc3c0385","username":"enzo_supercraftz","display_name":"Enzo_SuperCraftZ","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/62a27a7fb12d7075e259f113","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1225,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1116,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2242,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2596,"format":"WEBP"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4470,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3608,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5018,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":6826,"format":"WEBP"}]}}},{"id":"60ae99233c27a8b79c7fcb73","name":"Madge","flags":0,"timestamp":1675523408005,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"60ae99233c27a8b79c7fcb73","name":"Madge","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60ae8f064b1ea4526dd12b2e","username":"elpicos","display_name":"ElPicos","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/15e7e7f7-ae01-47b5-b892-52d46ad203c2-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60ae99233c27a8b79c7fcb73","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1333,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":960,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2613,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2418,"format":"WEBP"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4190,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3876,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5158,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":5446,"format":"WEBP"}]}}},{"id":"611687c2446a415801b1b55c","name":"XiJinNymN","flags":0,"timestamp":1675546373163,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"611687c2446a415801b1b55c","name":"XiJinNymN","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae3c29b2ecb015051f8f9a","username":"nymn","display_name":"NymN","avatar_url":"//cdn.7tv.app/pp/60ae3c29b2ecb015051f8f9a/71f269555aeb44c29100cae8aa59b56b","style":{"color":-1857617921},"roles":["6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/611687c2446a415801b1b55c","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1146,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":808,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2079,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":1904,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":2918,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":2936,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4086,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":4548,"format":"WEBP"}]}}},{"id":"62388548271ae0e02d721924","name":"based0","flags":1,"timestamp":1675609745299,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"62388548271ae0e02d721924","name":"based0","flags":256,"tags":["forsenbased","ezz"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"6042058896832ffa785800fe","username":"zhark","display_name":"Zhark","avatar_url":"//cdn.7tv.app/pp/6042058896832ffa785800fe/37ee95ffaa9846b286cb5554ff0716c5","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/62388548271ae0e02d721924","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":93,"size":20012,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":93,"size":45368,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":93,"size":36664,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":93,"size":89190,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":93,"size":65514,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":93,"size":142074,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":93,"size":89242,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":93,"size":127104,"format":"WEBP"}]}}},{"id":"631210ee113e0e8575d2d130","name":"SoCute","flags":0,"timestamp":1675609745576,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"631210ee113e0e8575d2d130","name":"SoCute","flags":0,"tags":["peepohappy","socute","peepoblush"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60af6d382c36aae19e1bfbd2","username":"ashsii","display_name":"ashsii","avatar_url":"//cdn.7tv.app/user/60af6d382c36aae19e1bfbd2/av_6397c58e9b8dbe094e96d2bf/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","60b3f1ea886e63449c5263b1","6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/631210ee113e0e8575d2d130","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":38,"height":32,"frame_count":12,"size":10451,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":38,"height":32,"frame_count":12,"size":11428,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":76,"height":64,"frame_count":12,"size":20669,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":76,"height":64,"frame_count":12,"size":24038,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":114,"height":96,"frame_count":12,"size":30994,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":114,"height":96,"frame_count":12,"size":37484,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":152,"height":128,"frame_count":12,"size":42079,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":152,"height":128,"frame_count":12,"size":51726,"format":"WEBP"}]}}},{"id":"60ae6b4486fc40d488d0b324","name":"AlienGathering","flags":0,"timestamp":1675609745858,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60ae6b4486fc40d488d0b324","name":"AlienGathering","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae49350e35477634486602","username":"justrogan","display_name":"JustRogan","avatar_url":"//cdn.7tv.app/pp/60ae49350e35477634486602/88d6e3c4265f4be0a452c812c146da50","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60ae6b4486fc40d488d0b324","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":96,"height":20,"frame_count":300,"size":684902,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":96,"height":20,"frame_count":300,"size":835760,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":192,"height":40,"frame_count":300,"size":1535437,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":192,"height":40,"frame_count":300,"size":2102086,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":288,"height":60,"frame_count":300,"size":2451790,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":288,"height":60,"frame_count":300,"size":3654026,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":384,"height":80,"frame_count":300,"size":3052819,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":384,"height":80,"frame_count":300,"size":4897114,"format":"WEBP"}]}}},{"id":"62f9c8cf00630d5b2acd66d1","name":"peepoTalk","flags":0,"timestamp":1675620584046,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"62f9c8cf00630d5b2acd66d1","name":"peepoTalk","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"603c7fca96832ffa788a5f14","username":"hyruverse","display_name":"hyruverse","avatar_url":"//cdn.7tv.app/pp/603c7fca96832ffa788a5f14/2ed3bd237882444ebccf38ae918e8df6","style":{"color":849892095},"roles":["60724f65e93d828bf8858789","6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/62f9c8cf00630d5b2acd66d1","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":16,"size":3981,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":16,"size":4150,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":16,"size":5510,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":16,"size":7752,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":16,"size":7501,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":16,"size":11656,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":16,"size":9864,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":16,"size":15742,"format":"WEBP"}]}}},{"id":"6237279d73f35ccbda40a64e","name":"MeowwartsSchool","flags":0,"timestamp":1675690682297,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"6237279d73f35ccbda40a64e","name":"MeowwartsSchool","flags":0,"tags":["hogwartz","kitten","harrypotter"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"61599595dc267a3441816d24","username":"little_meowisek_xd","display_name":"little_meowisek_xd","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/3db44c26-6d62-45ba-a3f6-7d8bd8a588ea-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6237279d73f35ccbda40a64e","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":131,"size":33109,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":131,"size":109850,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":131,"size":111918,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":131,"size":284426,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":131,"size":215251,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":131,"size":477274,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":131,"size":397595,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":131,"size":413102,"format":"WEBP"}]}}},{"id":"61ffb7775d4f1907669e8159","name":"ge0","flags":1,"timestamp":1675696175521,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"61ffb7775d4f1907669e8159","name":"ge0","flags":256,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60ae434b5d3fdae58382926a","username":"ayyybubu","display_name":"ayyybubu","avatar_url":"//cdn.7tv.app/user/60ae434b5d3fdae58382926a/av_636fa88735a18fd8b17e7399/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","62d86a8419fdcf401421c5ae","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61ffb7775d4f1907669e8159","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":908,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1062,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2086,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1944,"format":"AVIF"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":2961,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":3548,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":3705,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":4880,"format":"WEBP"}]}}},{"id":"60b2cce90616dd6156d14fc0","name":"Latege","flags":0,"timestamp":1675696175811,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60b2cce90616dd6156d14fc0","name":"Latege","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60b1122ec924cff187e0ab90","username":"airoh_","display_name":"Airoh_","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/6159e77c-24e6-45b4-ac30-78449df5da44-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60b2cce90616dd6156d14fc0","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":11,"size":5258,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":11,"size":10236,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":11,"size":9463,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":11,"size":23732,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":11,"size":17115,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":11,"size":40690,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":11,"size":27557,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":11,"size":46294,"format":"WEBP"}]}}},{"id":"6373c12922efe4715fbc7e7c","name":"notListening","flags":0,"timestamp":1675782575536,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"6373c12922efe4715fbc7e7c","name":"notListening","flags":0,"tags":["bateman","americanpsycho"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"6042058896832ffa785800fe","username":"zhark","display_name":"Zhark","avatar_url":"//cdn.7tv.app/pp/6042058896832ffa785800fe/37ee95ffaa9846b286cb5554ff0716c5","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6373c12922efe4715fbc7e7c","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":56,"height":32,"frame_count":162,"size":28838,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":56,"height":32,"frame_count":162,"size":103992,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":112,"height":64,"frame_count":162,"size":55396,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":112,"height":64,"frame_count":162,"size":240098,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":168,"height":96,"frame_count":162,"size":77302,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":168,"height":96,"frame_count":162,"size":374686,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":224,"height":128,"frame_count":162,"size":171422,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":224,"height":128,"frame_count":162,"size":565368,"format":"WEBP"}]}}},{"id":"60c6799b6184f8c1da5ed61f","name":"GAMING","flags":0,"timestamp":1675782575883,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60c6799b6184f8c1da5ed61f","name":"GAMING","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae5e109986a003497d2ea1","username":"grooot2","display_name":"Grooot2","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/9c6a0eb2-8446-4259-8f04-e76de1277bfe-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60c6799b6184f8c1da5ed61f","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":150,"size":93595,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":150,"size":158076,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":150,"size":265934,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":150,"size":385530,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":150,"size":454481,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":150,"size":656358,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":150,"size":696275,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":150,"size":728232,"format":"WEBP"}]}}},{"id":"61b8a304112f39cb68afc749","name":"areyoudonenymn","flags":0,"timestamp":1675782576175,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"61b8a304112f39cb68afc749","name":"areyoudonenymn","flags":0,"tags":["nymn","apollo"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae3cb1b2ecb0150521fa1f","username":"waterboiledpizza","display_name":"WaterBoiledPizza","avatar_url":"//cdn.7tv.app/user/60ae3cb1b2ecb0150521fa1f/av_652806843e9323c51e05082e/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61b8a304112f39cb68afc749","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":56,"size":10931,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":56,"size":39536,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":56,"size":29841,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":56,"size":94258,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":56,"size":60470,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":56,"size":157072,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":56,"size":105658,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":56,"size":116146,"format":"WEBP"}]}}},{"id":"60afa6b412f90fadd60a7d9b","name":"peepoPog","flags":0,"timestamp":1675800079480,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60afa6b412f90fadd60a7d9b","name":"peepoPog","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60af971d12f90fadd6aa9ff8","username":"viscoito","display_name":"Viscoito","avatar_url":"//cdn.7tv.app/user/60af971d12f90fadd6aa9ff8/av_651d964e32b1db5b90eead40/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60afa6b412f90fadd60a7d9b","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":902,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1207,"format":"AVIF"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1944,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2078,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":2931,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":3484,"format":"WEBP"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":4186,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":3644,"format":"AVIF"}]}}},{"id":"60420a8b77137b000de9e66e","name":"gachiHYPER","flags":0,"timestamp":1675869005474,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60420a8b77137b000de9e66e","name":"gachiHYPER","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"603ca8f696832ffa78c01eb4","username":"mauriplss","display_name":"Mauriplss","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/a428f0f0-bdd4-4c93-ac4b-ed174244cb66-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60420a8b77137b000de9e66e","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":60,"size":20764,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":60,"size":59696,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":60,"size":55912,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":60,"size":130846,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":60,"size":93655,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":60,"size":221462,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":60,"size":206156,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":60,"size":342528,"format":"WEBP"}]}}},{"id":"624f4c9e6bb22d4119fc81bc","name":"BREH","flags":0,"timestamp":1675869005762,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"624f4c9e6bb22d4119fc81bc","name":"BREH","flags":0,"tags":["breh","zoomer","bussin","bruh"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60af7b83a564afa26e9fd0eb","username":"happinson","display_name":"Happinson","avatar_url":"//cdn.7tv.app/user/60af7b83a564afa26e9fd0eb/av_657c054b604852811f0a6390/3x_static.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/624f4c9e6bb22d4119fc81bc","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1186,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":934,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2382,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2438,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3877,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4290,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":6024,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":7024,"format":"WEBP"}]}}},{"id":"60e1df02dac155e36624afaa","name":"Hmm","flags":0,"timestamp":1675869253176,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60e1df02dac155e36624afaa","name":"Hmm","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60aebf0ae90f445e43b37fe5","username":"prog0ldfish","display_name":"ProG0ldfish","avatar_url":"//cdn.7tv.app/user/60aebf0ae90f445e43b37fe5/av_6353ef0d7f642dee0e30c1f4/3x_static.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60e1df02dac155e36624afaa","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1000,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":800,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1884,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":1982,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":2713,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":3120,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":3738,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":4468,"format":"WEBP"}]}}},{"id":"60aec23d5174a619db1851ef","name":"3Heading","flags":0,"timestamp":1675955435285,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60aec23d5174a619db1851ef","name":"3Heading","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60aea31d229664e866695e3f","username":"psyclonetm","display_name":"PsycloneTM","avatar_url":"//cdn.7tv.app/pp/60aea31d229664e866695e3f/1a1bfe41c0144d46b0561b4cd9ae3c05","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60aec23d5174a619db1851ef","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":113,"size":58435,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":113,"size":107386,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":113,"size":145460,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":113,"size":259440,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":113,"size":247714,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":113,"size":429106,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":113,"size":363568,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":113,"size":464842,"format":"WEBP"}]}}},{"id":"60ae4bc55d3fdae583d93f34","name":"NOPERS","flags":0,"timestamp":1675955435571,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60ae4bc55d3fdae583d93f34","name":"NOPERS","flags":0,"lifecycle":3,"state":["PERSONAL","LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae4b445d3fdae583d20e9a","username":"ethantp","display_name":"ethantp","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/617473d7-7627-4bd6-befa-a2ff489d8daa-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60ae4bc55d3fdae583d93f34","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":32,"size":13211,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":32,"size":29904,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":32,"size":27646,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":32,"size":65946,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":32,"size":43112,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":32,"size":104246,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":32,"size":69624,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":32,"size":113414,"format":"WEBP"}]}}},{"id":"6042076f77137b000de9e666","name":"TRUEING","flags":0,"timestamp":1675955435843,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"6042076f77137b000de9e666","name":"TRUEING","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60b69d21f28060ef90d00ee2","username":"xenev","display_name":"xenev","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/3ca44a21-d878-4ad1-9c58-820987264ac8-profile_image-70x70.png","style":{"color":-1857617921},"roles":["6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6042076f77137b000de9e666","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":99,"size":19116,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":99,"size":58728,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":99,"size":34731,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":99,"size":130106,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":99,"size":56414,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":99,"size":214518,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":99,"size":128876,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":99,"size":427014,"format":"WEBP"}]}}},{"id":"60ae4f175d3fdae583148348","name":"headBang","flags":0,"timestamp":1675966425810,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"60ae4f175d3fdae583148348","name":"headBang","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"6044f2cc86a556e0b0210e40","username":"hewooo","display_name":"hewooo","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/953ea757-de1f-43c8-b8ea-d6bac5e3233b-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60ae4f175d3fdae583148348","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":7,"size":5842,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":7,"size":6392,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":7,"size":10537,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":7,"size":13350,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":7,"size":16365,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":7,"size":21556,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":7,"size":21131,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":7,"size":23496,"format":"WEBP"}]}}},{"id":"60ae839dea50f43c9ea4893d","name":"ThinkingAboutPoopernoodle","flags":0,"timestamp":1676041835960,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60ae839dea50f43c9ea4893d","name":"FeelsWowMan","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60b561cc04283ab952bfd4e0","username":"on_a_stack","display_name":"On_a_stack","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/1ed36b65-71c8-4eb2-a6a7-83ad2bb7566a-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60ae839dea50f43c9ea4893d","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":33,"height":32,"frame_count":1,"size":1580,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":33,"height":32,"frame_count":1,"size":1174,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":66,"height":64,"frame_count":1,"size":3142,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":66,"height":64,"frame_count":1,"size":2998,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":99,"height":96,"frame_count":1,"size":4778,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":99,"height":96,"frame_count":1,"size":5096,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":132,"height":128,"frame_count":1,"size":6540,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":132,"height":128,"frame_count":1,"size":6948,"format":"WEBP"}]}}},{"id":"61932682b1eb03daac7df6aa","name":"docJAMMER","flags":0,"timestamp":1676041836247,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"61932682b1eb03daac7df6aa","name":"docJAMMER","flags":0,"tags":["doc","jam"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"6193177e17e4d50afc0db403","username":"samlr__","display_name":"SAMlR__","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/ebd01158-8672-48fa-9ddd-a17a97521785-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61932682b1eb03daac7df6aa","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":262,"size":164262,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":262,"size":250380,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":262,"size":400113,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":262,"size":580502,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":262,"size":662344,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":262,"size":984124,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":262,"size":960087,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":262,"size":1162954,"format":"WEBP"}]}}},{"id":"60bb0a06c2415f99b5f6ceb6","name":"OMEGALUOL","flags":0,"timestamp":1676128235275,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60bb0a06c2415f99b5f6ceb6","name":"OMEGALOOL","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60b97c7626c484211d05b9d4","username":"porocutioner","display_name":"poroCUTIonEr","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/07282f8f-7bf4-4dcb-836f-edacd1eac9db-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60bb0a06c2415f99b5f6ceb6","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":5,"size":5114,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":5,"size":5036,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":5,"size":10698,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":5,"size":9163,"format":"AVIF"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":5,"size":13850,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":5,"size":17200,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":5,"size":18513,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":5,"size":18172,"format":"WEBP"}]}}},{"id":"60ae4bb30e35477634610fda","name":"NODDERS","flags":0,"timestamp":1676128235688,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60ae4bb30e35477634610fda","name":"NODDERS","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae4b445d3fdae583d20e9a","username":"ethantp","display_name":"ethantp","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/617473d7-7627-4bd6-befa-a2ff489d8daa-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60ae4bb30e35477634610fda","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":16,"size":7970,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":16,"size":14500,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":16,"size":14370,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":16,"size":32714,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":16,"size":23591,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":16,"size":52130,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":16,"size":36896,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":16,"size":54426,"format":"WEBP"}]}}},{"id":"63a9c3bbe2080789035383e7","name":"nymnStairs","flags":0,"timestamp":1676128236100,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"63a9c3bbe2080789035383e7","name":"nymnStairs","flags":0,"tags":["nymn","stairs"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae3e3eb2ecb01505346ae9","username":"fawcan","display_name":"Fawcan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38051bdb-83c6-4716-95e0-731462e02b45-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63a9c3bbe2080789035383e7","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":66,"size":9840,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":66,"size":15408,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":66,"size":16814,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":66,"size":48010,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":66,"size":29076,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":66,"size":79368,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":66,"size":55735,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":66,"size":112894,"format":"WEBP"}]}}},{"id":"60ba145c31abfff37bd0d280","name":"Ratge","flags":0,"timestamp":1676128317596,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60ba145c31abfff37bd0d280","name":"Ratge","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60ae7907e52a54a8e3a2c668","username":"benjaminyvr","display_name":"benjaminyvr","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/1a9471ce-122f-4ba4-963c-0e4260ad8c3c-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60ba145c31abfff37bd0d280","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":41,"height":32,"frame_count":1,"size":1636,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":41,"height":32,"frame_count":1,"size":1394,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":82,"height":64,"frame_count":1,"size":3294,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":82,"height":64,"frame_count":1,"size":3500,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":123,"height":96,"frame_count":1,"size":4887,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":123,"height":96,"frame_count":1,"size":5878,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":164,"height":128,"frame_count":1,"size":6491,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":164,"height":128,"frame_count":1,"size":7172,"format":"WEBP"}]}}},{"id":"60ae745fdc23eca68e4e0a3d","name":"SoyScream","flags":0,"timestamp":1676128518504,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60ae745fdc23eca68e4e0a3d","name":"SoyScream","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae461c9986a003493358f3","username":"gaib_","display_name":"gaib_","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/ce7e4c60-a008-4e7e-8972-e905ffc54e71-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60ae745fdc23eca68e4e0a3d","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":67,"size":27659,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":67,"size":66578,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":67,"size":64988,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":67,"size":149896,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":67,"size":116110,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":67,"size":243386,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":67,"size":172135,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":67,"size":255152,"format":"WEBP"}]}}},{"id":"60ae387cb2ecb0150505e235","name":"Tssk","flags":0,"timestamp":1676214665655,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60ae387cb2ecb0150505e235","name":"Tssk","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60635b50452cea4685f26b34","username":"hecrzy","display_name":"heCrzy","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/583dd5ac-2fe8-4ead-a20d-e10770118c5f-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60ae387cb2ecb0150505e235","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":24,"size":13338,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":24,"size":20310,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":24,"size":25616,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":24,"size":40498,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":24,"size":39352,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":24,"size":63386,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":24,"size":53192,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":24,"size":72508,"format":"WEBP"}]}}},{"id":"63c55a52a5f1a56aa0e6ddd1","name":"BymN","flags":0,"timestamp":1676214665932,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"63c55a52a5f1a56aa0e6ddd1","name":"BymN","flags":0,"tags":["mrbean","nymn"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae233e259ac5a73eafe07c","username":"fratroisk","display_name":"fratroisk","avatar_url":"//cdn.7tv.app/user/60ae233e259ac5a73eafe07c/av_6558d64ef95c3b0191d19b06/3x_static.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63c55a52a5f1a56aa0e6ddd1","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":29,"height":32,"frame_count":1,"size":1186,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":29,"height":32,"frame_count":1,"size":1748,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":58,"height":64,"frame_count":1,"size":2227,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":58,"height":64,"frame_count":1,"size":5350,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":87,"height":96,"frame_count":1,"size":3428,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":87,"height":96,"frame_count":1,"size":10524,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":116,"height":128,"frame_count":1,"size":4726,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":116,"height":128,"frame_count":1,"size":17028,"format":"WEBP"}]}}},{"id":"613bf9aebe977eb5b436c816","name":"Modge","flags":0,"timestamp":1676214666230,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"613bf9aebe977eb5b436c816","name":"Modge","flags":0,"tags":["mods","modge","perma","sadge"],"lifecycle":3,"state":["PERSONAL","LISTED"],"listed":true,"animated":true,"owner":{"id":"60aea31d229664e866695e3f","username":"psyclonetm","display_name":"PsycloneTM","avatar_url":"//cdn.7tv.app/pp/60aea31d229664e866695e3f/1a1bfe41c0144d46b0561b4cd9ae3c05","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/613bf9aebe977eb5b436c816","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":13,"size":7292,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":13,"size":12780,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":13,"size":16181,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":13,"size":31620,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":13,"size":27429,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":13,"size":54910,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":13,"size":45672,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":13,"size":70426,"format":"WEBP"}]}}},{"id":"60af12e17e8706b572e5c326","name":"doctorWTF","flags":0,"timestamp":1676301065360,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60af12e17e8706b572e5c326","name":"doctorWTF","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60aeb19256f54d7a40627c3a","username":"nukro","display_name":"nukro","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/b91bdf41-79d9-4472-b606-5a2f2e9cca5d-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60af12e17e8706b572e5c326","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1320,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1504,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":3298,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":3087,"format":"AVIF"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4762,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":5740,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":6659,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":6928,"format":"WEBP"}]}}},{"id":"60faf6f74653f5d6c1c65a04","name":"donkiBonk","flags":0,"timestamp":1676301065809,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60faf6f74653f5d6c1c65a04","name":"donkiBonk","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60f0577e48cde2fcc3e6eb12","username":"bopens1_reformed","display_name":"bopens1_reformed","avatar_url":"//static-cdn.jtvnw.net/user-default-pictures-uv/75305d54-c7cc-40d1-bb9c-91fbe85943c7-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60faf6f74653f5d6c1c65a04","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":4,"size":4310,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":4,"size":3934,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":4,"size":7247,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":4,"size":8590,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":4,"size":11471,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":4,"size":14876,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":4,"size":16122,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":4,"size":19280,"format":"WEBP"}]}}},{"id":"60af5b5135c50a77928212f3","name":"Sank","flags":0,"timestamp":1676301066116,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60af5b5135c50a77928212f3","name":"Sank","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae2a79259ac5a73ec9cd07","username":"aifanny","display_name":"Aifanny","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/410d4bae-a02b-464b-b250-678eb5e42ae4-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60af5b5135c50a77928212f3","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":25,"size":10642,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":25,"size":20846,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":25,"size":22232,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":25,"size":41618,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":25,"size":37747,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":25,"size":63586,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":25,"size":52689,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":25,"size":70818,"format":"WEBP"}]}}},{"id":"63d937ccf74db58df4e60f87","name":"Confused","flags":0,"timestamp":1676387465316,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"63d937ccf74db58df4e60f87","name":"Confused","flags":0,"tags":["what","pepe","peepo","cat","huh","clueless"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae409daee2aa55383ebb4b","username":"tolatos","display_name":"tolatos","avatar_url":"//cdn.7tv.app/user/60ae409daee2aa55383ebb4b/av_657b75e8b0d945ef35823739/3x.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63d937ccf74db58df4e60f87","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":28,"height":32,"frame_count":189,"size":37712,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":28,"height":32,"frame_count":189,"size":84564,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":56,"height":64,"frame_count":189,"size":75554,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":56,"height":64,"frame_count":189,"size":157476,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":84,"height":96,"frame_count":189,"size":125790,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":84,"height":96,"frame_count":189,"size":246262,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":112,"height":128,"frame_count":189,"size":172713,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":112,"height":128,"frame_count":189,"size":308832,"format":"WEBP"}]}}},{"id":"60aec2196cfcffe15f4e4f93","name":"Prayge","flags":0,"timestamp":1676387465617,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60aec2196cfcffe15f4e4f93","name":"Prayge","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60ac7c354ef7db1ec1e9b730","username":"neowav","display_name":"Neowav","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/bf4df7d8-9342-48b4-90b6-30abb7d8dd40-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60aec2196cfcffe15f4e4f93","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":936,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1289,"format":"AVIF"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2443,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2278,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3650,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":3896,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4926,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":4944,"format":"WEBP"}]}}},{"id":"6325e45f55eabea21b003802","name":"Munchin","flags":0,"timestamp":1676387465891,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"6325e45f55eabea21b003802","name":"Munchin","flags":0,"tags":["eating","rat","reaction"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"61ec247ecc9507d24fd4a789","username":"gloft_0001","display_name":"Gloft_0001","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6325e45f55eabea21b003802","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":146,"size":35490,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":146,"size":118072,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":146,"size":86092,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":146,"size":239722,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":146,"size":156700,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":146,"size":338400,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":146,"size":251198,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":146,"size":443722,"format":"WEBP"}]}}},{"id":"63ebd1953eab12f5199c044a","name":"TAUNTED","flags":0,"timestamp":1676399071109,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"63ebd1953eab12f5199c044a","name":"TAUNTED","flags":0,"tags":["taunted","pepw","nymn","nymning","rage","taunt"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae759bdf5735e04acb69d9","username":"hotbear1110","display_name":"HotBear1110","avatar_url":"//cdn.7tv.app/pp/60ae759bdf5735e04acb69d9/80e2b49378c14dc6914fde8cb72fa673","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63ebd1953eab12f5199c044a","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":44,"height":32,"frame_count":90,"size":49479,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":44,"height":32,"frame_count":90,"size":77648,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":88,"height":64,"frame_count":90,"size":116204,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":88,"height":64,"frame_count":90,"size":182910,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":132,"height":96,"frame_count":90,"size":220668,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":132,"height":96,"frame_count":90,"size":286830,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":176,"height":128,"frame_count":90,"size":342018,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":176,"height":128,"frame_count":90,"size":370696,"format":"WEBP"}]}}},{"id":"63ebe1fc0cb254f7266f4af3","name":"NYMNING","flags":0,"timestamp":1676403380800,"actor_id":"60ae8fc0ea50f43c9e3ae255","data":{"id":"63ebe1fc0cb254f7266f4af3","name":"NYMNING","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae8fc0ea50f43c9e3ae255","username":"agenttud","display_name":"agenttud","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/273db808-d42f-4dab-9b39-9780ef2777b0-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63ebe1fc0cb254f7266f4af3","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":108,"size":44245,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":108,"size":69852,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":108,"size":110660,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":108,"size":143814,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":108,"size":260860,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":108,"size":222198,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":108,"size":546368,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":108,"size":328630,"format":"WEBP"}]}}},{"id":"619fffbbffa9aba101bb1bfc","name":"Looking","flags":0,"timestamp":1676473895278,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"619fffbbffa9aba101bb1bfc","name":"Looking","flags":0,"tags":["looking","sussy"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"611fc949e6a24615da9d21cf","username":"krewlex","display_name":"Krewlex","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/85d97663-2fa4-45b8-832a-6c2be6102a3c-profile_image-70x70.png","style":{"color":-1857617921},"roles":["6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/619fffbbffa9aba101bb1bfc","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":702,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1108,"format":"AVIF"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1914,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":1790,"format":"WEBP"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":3136,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":2954,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":3929,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":4736,"format":"WEBP"}]}}},{"id":"60ba5e80671673093a6274e1","name":"BBoomer","flags":0,"timestamp":1676473895596,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60ba5e80671673093a6274e1","name":"BBoomer","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ba57b2cc31c8eade405cb4","username":"mflashr","display_name":"mFLASHr","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/2c2cd23c-c609-4589-b3b2-1e3525028a8d-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60ba5e80671673093a6274e1","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":13,"size":8221,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":13,"size":12958,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":13,"size":18180,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":13,"size":32188,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":13,"size":30620,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":13,"size":56416,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":13,"size":46569,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":13,"size":67558,"format":"WEBP"}]}}},{"id":"60aeb2da5174a619db6cd0e7","name":"Gladge","flags":0,"timestamp":1676473895986,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60aeb2da5174a619db6cd0e7","name":"Gladge","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60ae9207ac03cad607d3980f","username":"onkel_jodok","display_name":"onkel_jodok","avatar_url":"//cdn.7tv.app/pp/60ae9207ac03cad607d3980f/a7bb23d0f0ad46bb8105768c642ce6a1","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60aeb2da5174a619db6cd0e7","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1285,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":924,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2373,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2286,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3678,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":3868,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4910,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":5002,"format":"WEBP"}]}}},{"id":"63b70f6dc57736fec02f900c","name":"Donkborne","flags":0,"timestamp":1676560303152,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"63b70f6dc57736fec02f900c","name":"Donkborne","flags":0,"tags":["feelsdonkman","bloodborne","souls","donk"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ff054ffbd646ea3b221dc9","username":"tunari__","display_name":"Tunari__","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/bc530a7a-e04d-4765-a662-bb3efde482e2-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63b70f6dc57736fec02f900c","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":48,"height":32,"frame_count":1,"size":1727,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":48,"height":32,"frame_count":1,"size":2532,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":96,"height":64,"frame_count":1,"size":3913,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":96,"height":64,"frame_count":1,"size":7758,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":144,"height":96,"frame_count":1,"size":6692,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":144,"height":96,"frame_count":1,"size":15524,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":192,"height":128,"frame_count":1,"size":9680,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":192,"height":128,"frame_count":1,"size":25298,"format":"WEBP"}]}}},{"id":"63eb71630cb254f7266f4044","name":"nymnBaited","flags":0,"timestamp":1676560317305,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"63eb71630cb254f7266f4044","name":"nymnBaited","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae8fc0ea50f43c9e3ae255","username":"agenttud","display_name":"agenttud","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/273db808-d42f-4dab-9b39-9780ef2777b0-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63eb71630cb254f7266f4044","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":89,"size":20696,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":89,"size":37588,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":89,"size":39416,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":89,"size":67078,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":89,"size":62007,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":89,"size":97784,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":89,"size":156689,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":89,"size":131684,"format":"WEBP"}]}}},{"id":"60e4b66b73d5b443db31ed3b","name":"Deadlole","flags":0,"timestamp":1676560317611,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60e4b66b73d5b443db31ed3b","name":"Deadlole","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae653c9627f9aff4f5ccd1","username":"xoo_6119","display_name":"xoo_6119","avatar_url":"//cdn.7tv.app/user/60ae653c9627f9aff4f5ccd1/av_63ca0eccdedb49b24383ae5c/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60e4b66b73d5b443db31ed3b","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":19,"size":4588,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":16,"size":4432,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":19,"size":6294,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":18,"size":9408,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":19,"size":9410,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":18,"size":13844,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":19,"size":15059,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":18,"size":15260,"format":"WEBP"}]}}},{"id":"6349783bf614dc272b1f940b","name":"pl","flags":0,"timestamp":1676560317919,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"6349783bf614dc272b1f940b","name":"pl","flags":0,"tags":["gurom","poland","okay","polska","nymn"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60b6746f64faf92496330d3c","username":"littlescampi","display_name":"LittleScampi","avatar_url":"//cdn.7tv.app/pp/60b6746f64faf92496330d3c/a1ff043123944a119d69cf9a7062a442","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6349783bf614dc272b1f940b","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1092,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1136,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1977,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2754,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":2917,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4950,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":3702,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":7476,"format":"WEBP"}]}}},{"id":"60ccd826197108c5ca4c1169","name":"gekPls","flags":0,"timestamp":1676636843061,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"60ccd826197108c5ca4c1169","name":"gekPls","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60a536d1ac08622846bced71","username":"marcfryd_0","display_name":"marcfryd_0","avatar_url":"//cdn.7tv.app/user/60a536d1ac08622846bced71/av_63537c8f28e6aaaea2bb599e/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","631ef5ea03e9beb96f849a7e","6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60ccd826197108c5ca4c1169","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":59,"size":27265,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":59,"size":45904,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":59,"size":54915,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":59,"size":92588,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":59,"size":87190,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":59,"size":151898,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":59,"size":114552,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":59,"size":165418,"format":"WEBP"}]}}},{"id":"631bb95ad47e611c913d93ce","name":"WalterVibe","flags":0,"timestamp":1676646717106,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"631bb95ad47e611c913d93ce","name":"WalterVibe","flags":0,"tags":["walter","breakingbad","saulgoodman","saul","jesse","meth"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"610345da41ab14baee7e299e","username":"tmh616","display_name":"TMH616","avatar_url":"//cdn.7tv.app/user/610345da41ab14baee7e299e/av_63cc40ee3d2332c1835aeda7/3x.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/631bb95ad47e611c913d93ce","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":57,"height":32,"frame_count":213,"size":145393,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":57,"height":32,"frame_count":206,"size":135950,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":114,"height":64,"frame_count":213,"size":344126,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":114,"height":64,"frame_count":210,"size":331248,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":171,"height":96,"frame_count":213,"size":559375,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":171,"height":96,"frame_count":211,"size":536122,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":228,"height":128,"frame_count":213,"size":758722,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":228,"height":128,"frame_count":212,"size":750804,"format":"WEBP"}]}}},{"id":"63333a09078a9df7a28c58c2","name":"peepoChatbutpeepoisnotchatting","flags":0,"timestamp":1676646717697,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"63333a09078a9df7a28c58c2","name":"peepoChatbutpeepoisnotchatting","flags":0,"tags":["chat","halloween","keyboard","chatting","peepo","not"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"6054fb35b4d31e459f7cde73","username":"21mtd","display_name":"21mtd","avatar_url":"//cdn.7tv.app/user/6054fb35b4d31e459f7cde73/av_657c1e5fa8b03226340b14d7/3x_static.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63333a09078a9df7a28c58c2","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1384,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1876,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2943,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":5600,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4541,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":11098,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":6456,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":17702,"format":"WEBP"}]}}},{"id":"6040a8bccf6746000db10348","name":"pepeJAM","flags":0,"timestamp":1676652635802,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"6040a8bccf6746000db10348","name":"pepeJAM","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"618899d04ea2f24e5009cccc","username":"shakothewacko","display_name":"ShakoTheWacko","avatar_url":"//cdn.7tv.app/pp/618899d04ea2f24e5009cccc/ef6ac364e53d4690b86b127f1df74bc1","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6040a8bccf6746000db10348","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":33,"height":32,"frame_count":4,"size":4047,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":33,"height":32,"frame_count":4,"size":4526,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":66,"height":64,"frame_count":4,"size":7279,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":66,"height":64,"frame_count":4,"size":10512,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":99,"height":96,"frame_count":4,"size":10841,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":99,"height":96,"frame_count":4,"size":17636,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":132,"height":128,"frame_count":4,"size":18449,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":132,"height":128,"frame_count":4,"size":25270,"format":"WEBP"}]}}},{"id":"61e32b713441abfa431ca77c","name":"Rime","flags":0,"timestamp":1676732549730,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"61e32b713441abfa431ca77c","name":"Rime","flags":0,"tags":["russel","comedy","rime","lime","nime"],"lifecycle":3,"state":["PERSONAL","LISTED"],"listed":true,"animated":false,"owner":{"id":"612a548c529c91532ab271fc","username":"gamermonth","display_name":"gamermonth","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/685406ea-2008-4c4b-9031-7112042f8d7a-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61e32b713441abfa431ca77c","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1138,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":764,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1866,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":1800,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":2800,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":3162,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":3710,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":4450,"format":"WEBP"}]}}},{"id":"60bd14f67cef73d00a404896","name":"How2Read","flags":0,"timestamp":1676733147124,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60bd14f67cef73d00a404896","name":"How2Read","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60b28a4a4f32610f15d19e61","username":"xaeriia","display_name":"xAeriia","avatar_url":"//cdn.7tv.app/user/60b28a4a4f32610f15d19e61/av_647bb5415579ae9e28079f9e/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60bd14f67cef73d00a404896","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1735,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1228,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":3253,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2930,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4854,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4992,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":6515,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":6368,"format":"WEBP"}]}}},{"id":"6262dbf4e38c52ba50e8c188","name":"NymnPretendingToEnjoyHisCrappyRemix","flags":0,"timestamp":1676733147422,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"6262dbf4e38c52ba50e8c188","name":"NymnPretendingToEnjoyHisCrappyRemix","flags":0,"tags":["nymn","jam"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"609ea088a38a46f969b61e98","username":"kufric","display_name":"Kufric","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6262dbf4e38c52ba50e8c188","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":166,"size":26756,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":166,"size":128592,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":166,"size":56944,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":166,"size":302506,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":166,"size":98287,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":166,"size":503142,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":166,"size":322939,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":166,"size":864218,"format":"WEBP"}]}}},{"id":"6398fb5651402d3cdab9b26a","name":"EEEK","flags":0,"timestamp":1676733147802,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"6398fb5651402d3cdab9b26a","name":"EEEK","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae434b5d3fdae58382926a","username":"ayyybubu","display_name":"ayyybubu","avatar_url":"//cdn.7tv.app/user/60ae434b5d3fdae58382926a/av_636fa88735a18fd8b17e7399/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","62d86a8419fdcf401421c5ae","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6398fb5651402d3cdab9b26a","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":53,"size":27111,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":53,"size":32980,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":53,"size":56032,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":53,"size":59502,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":53,"size":96767,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":53,"size":86220,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":53,"size":145351,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":53,"size":114542,"format":"WEBP"}]}}},{"id":"60420e5a77137b000de9e676","name":"PepeHands","flags":0,"timestamp":1676819577407,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60420e5a77137b000de9e676","name":"PepeHands","flags":0,"lifecycle":3,"state":["PERSONAL","LISTED"],"listed":true,"animated":false,"owner":{"id":"603ca8f696832ffa78c01eb4","username":"mauriplss","display_name":"Mauriplss","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/a428f0f0-bdd4-4c93-ac4b-ed174244cb66-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60420e5a77137b000de9e676","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1633,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1198,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":3265,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":3038,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":5068,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":5168,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":7013,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":7434,"format":"WEBP"}]}}},{"id":"609eebd34c18609a1d984f3f","name":"pepeMeltdown","flags":0,"timestamp":1676819648496,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"609eebd34c18609a1d984f3f","name":"pepeMeltdown","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"609ede7b4c18609a1d94c5ae","username":"yoim5th","display_name":"yoim5th","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/81c11127-ffa9-4b47-a5b0-7e602a998aae-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/609eebd34c18609a1d984f3f","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":10,"size":10160,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":10,"size":9486,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":10,"size":17222,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":10,"size":18430,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":10,"size":26903,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":10,"size":30330,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":10,"size":32248,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":10,"size":32782,"format":"WEBP"}]}}},{"id":"63f2440c3ebf15a76f4f07e9","name":"VeryClean","flags":0,"timestamp":1676822426010,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"63f2440c3ebf15a76f4f07e9","name":"VeryClean","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae518c0e35477634c151f1","username":"fabulouspotato69","display_name":"FabulousPotato69","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63f2440c3ebf15a76f4f07e9","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":663,"size":96871,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":663,"size":172392,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":663,"size":188807,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":663,"size":325688,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":663,"size":319908,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":663,"size":488372,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":663,"size":492789,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":663,"size":685540,"format":"WEBP"}]}}},{"id":"63f25606bb16b52ef4a0d27f","name":"DOCBOZO","flags":0,"timestamp":1676826222261,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"63f25606bb16b52ef4a0d27f","name":"DOCBOZO","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae518c0e35477634c151f1","username":"fabulouspotato69","display_name":"FabulousPotato69","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63f25606bb16b52ef4a0d27f","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":295,"size":48615,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":290,"size":109362,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":295,"size":107498,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":295,"size":232122,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":295,"size":178383,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":295,"size":337418,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":295,"size":273510,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":295,"size":445744,"format":"WEBP"}]}}},{"id":"60ef515648cde2fcc3c699da","name":"PokerFace","flags":0,"timestamp":1676827009948,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60ef515648cde2fcc3c699da","name":"PokerFace","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60e022fe50830d688ae2861f","username":"rvdog815","display_name":"rvdog815","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/892a0be4-51f1-4741-9a02-b634e0476a5e-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60ef515648cde2fcc3c699da","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":936,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":720,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1502,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":1482,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":2257,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":2576,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":3035,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":3884,"format":"WEBP"}]}}},{"id":"63f27b343b0894cb3bf5c950","name":"heCrazy","flags":0,"timestamp":1676835729459,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"63f27b343b0894cb3bf5c950","name":"heCrazy","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae518c0e35477634c151f1","username":"fabulouspotato69","display_name":"FabulousPotato69","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63f27b343b0894cb3bf5c950","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":86,"size":33566,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":86,"size":51286,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":86,"size":85555,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":86,"size":100972,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":86,"size":148593,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":86,"size":162810,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":86,"size":312044,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":86,"size":264490,"format":"WEBP"}]}}},{"id":"63f2806d08f5788b589253c7","name":"FeelsWeakMan","flags":0,"timestamp":1676837347025,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"63f2806d08f5788b589253c7","name":"FeelsWeakMan","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae518c0e35477634c151f1","username":"fabulouspotato69","display_name":"FabulousPotato69","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63f2806d08f5788b589253c7","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":144,"size":56267,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":144,"size":108664,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":144,"size":160667,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":144,"size":217332,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":144,"size":302752,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":144,"size":350160,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":144,"size":644296,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":144,"size":581210,"format":"WEBP"}]}}},{"id":"63f28aebf2915b442ca80ce5","name":"BatDisco","flags":0,"timestamp":1676839730607,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"63f28aebf2915b442ca80ce5","name":"BatDisco","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae518c0e35477634c151f1","username":"fabulouspotato69","display_name":"FabulousPotato69","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63f28aebf2915b442ca80ce5","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":56,"height":32,"frame_count":57,"size":55576,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":56,"height":32,"frame_count":57,"size":50286,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":112,"height":64,"frame_count":57,"size":129458,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":112,"height":64,"frame_count":57,"size":91294,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":168,"height":96,"frame_count":57,"size":224711,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":168,"height":96,"frame_count":57,"size":150976,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":224,"height":128,"frame_count":57,"size":387429,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":224,"height":128,"frame_count":57,"size":203620,"format":"WEBP"}]}}},{"id":"63f1fe3f5dccf65d6e8d2b39","name":"Lounging","flags":0,"timestamp":1676905977094,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"63f1fe3f5dccf65d6e8d2b39","name":"Lounging","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae8fc0ea50f43c9e3ae255","username":"agenttud","display_name":"agenttud","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/273db808-d42f-4dab-9b39-9780ef2777b0-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63f1fe3f5dccf65d6e8d2b39","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":59,"height":32,"frame_count":1,"size":1471,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":59,"height":32,"frame_count":1,"size":2480,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":118,"height":64,"frame_count":1,"size":7104,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":118,"height":64,"frame_count":1,"size":3051,"format":"AVIF"},{"name":"3x.avif","static_name":"3x_static.avif","width":177,"height":96,"frame_count":1,"size":5144,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":177,"height":96,"frame_count":1,"size":12940,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":236,"height":128,"frame_count":1,"size":7600,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":236,"height":128,"frame_count":1,"size":20222,"format":"WEBP"}]}}},{"id":"60d264b560c4a1a365139405","name":"PogTasty","flags":0,"timestamp":1676905977378,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60d264b560c4a1a365139405","name":"PogTasty","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae7907e52a54a8e3a2c668","username":"benjaminyvr","display_name":"benjaminyvr","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/1a9471ce-122f-4ba4-963c-0e4260ad8c3c-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60d264b560c4a1a365139405","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":120,"size":19869,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":120,"size":67766,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":120,"size":47375,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":120,"size":134206,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":120,"size":84197,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":120,"size":210074,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":120,"size":137974,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":120,"size":224350,"format":"WEBP"}]}}},{"id":"636814770b55276c97956724","name":"donkWalk","flags":0,"timestamp":1676905977660,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"636814770b55276c97956724","name":"donkWalk","flags":0,"tags":["donkwalk","donk","walk"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae3e3eb2ecb01505346ae9","username":"fawcan","display_name":"Fawcan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38051bdb-83c6-4716-95e0-731462e02b45-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/636814770b55276c97956724","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":10,"size":11749,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":10,"size":10600,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":10,"size":23866,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":10,"size":22304,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":10,"size":38211,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":10,"size":33296,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":10,"size":47493,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":10,"size":43918,"format":"WEBP"}]}}},{"id":"63f36852f2915b442ca820ad","name":"batPls","flags":0,"timestamp":1676919830967,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"63f36852f2915b442ca820ad","name":"batPls","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae518c0e35477634c151f1","username":"fabulouspotato69","display_name":"FabulousPotato69","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63f36852f2915b442ca820ad","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":228,"size":86948,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":228,"size":131842,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":228,"size":231022,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":228,"size":242708,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":228,"size":477101,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":228,"size":397834,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":228,"size":1147268,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":228,"size":711042,"format":"WEBP"}]}}},{"id":"63f3697e0588a70e9a8d1f6f","name":"batJAM","flags":0,"timestamp":1676919847550,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"63f3697e0588a70e9a8d1f6f","name":"batJAM","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae518c0e35477634c151f1","username":"fabulouspotato69","display_name":"FabulousPotato69","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63f3697e0588a70e9a8d1f6f","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":361,"size":110127,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":361,"size":235124,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":361,"size":289604,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":361,"size":458120,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":361,"size":512981,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":361,"size":728084,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":361,"size":1151536,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":361,"size":1168638,"format":"WEBP"}]}}},{"id":"63f3c388face0f3bbeaad1d1","name":"docL","flags":0,"timestamp":1676919884194,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"63f3c388face0f3bbeaad1d1","name":"docL","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae518c0e35477634c151f1","username":"fabulouspotato69","display_name":"FabulousPotato69","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63f3c388face0f3bbeaad1d1","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":159,"size":34678,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":159,"size":108502,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":159,"size":81446,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":159,"size":215012,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":159,"size":155120,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":159,"size":314410,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":159,"size":272676,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":159,"size":423374,"format":"WEBP"}]}}},{"id":"6072a067dcae02001b44e604","name":"DANKIES","flags":0,"timestamp":1676992407145,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"6072a067dcae02001b44e604","name":"DANKIES","flags":0,"lifecycle":3,"state":["PERSONAL","LISTED"],"listed":true,"animated":true,"owner":{"id":"60729f38bde0639989f2be94","username":"gentvh","display_name":"gentvh","avatar_url":"//cdn.7tv.app/user/60729f38bde0639989f2be94/av_639bf96c5af9734ac8daf760/3x_static.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6072a067dcae02001b44e604","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":10,"size":6309,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":10,"size":8890,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":10,"size":11495,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":10,"size":19576,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":10,"size":18055,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":10,"size":33276,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":10,"size":19606,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":10,"size":41508,"format":"WEBP"}]}}},{"id":"62642a25f95146e0da382bda","name":"Pain","flags":0,"timestamp":1676992407440,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"62642a25f95146e0da382bda","name":"Pain","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae434b5d3fdae58382926a","username":"ayyybubu","display_name":"ayyybubu","avatar_url":"//cdn.7tv.app/user/60ae434b5d3fdae58382926a/av_636fa88735a18fd8b17e7399/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","62d86a8419fdcf401421c5ae","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/62642a25f95146e0da382bda","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":65,"height":32,"frame_count":102,"size":13421,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":65,"height":32,"frame_count":102,"size":105572,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":130,"height":64,"frame_count":102,"size":39793,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":130,"height":64,"frame_count":102,"size":325876,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":195,"height":96,"frame_count":102,"size":76758,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":195,"height":96,"frame_count":102,"size":545034,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":260,"height":128,"frame_count":102,"size":169555,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":260,"height":128,"frame_count":102,"size":842150,"format":"WEBP"}]}}},{"id":"62c2e59768a0391cc239cdc2","name":"pikaMine","flags":0,"timestamp":1677078827879,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"62c2e59768a0391cc239cdc2","name":"pikaMine","flags":0,"tags":["pika","mine","moonmoon"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae3e3eb2ecb01505346ae9","username":"fawcan","display_name":"Fawcan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38051bdb-83c6-4716-95e0-731462e02b45-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/62c2e59768a0391cc239cdc2","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":2,"size":2014,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":2,"size":3342,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":2,"size":4184,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":2,"size":5116,"format":"AVIF"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":2,"size":7128,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":2,"size":6736,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":2,"size":8760,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":2,"size":8842,"format":"WEBP"}]}}},{"id":"63f6464cf8070da4e44bb855","name":"plink","flags":0,"timestamp":1677088300737,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"63f6464cf8070da4e44bb855","name":"plink","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae518c0e35477634c151f1","username":"fabulouspotato69","display_name":"FabulousPotato69","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63f6464cf8070da4e44bb855","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":80,"height":32,"frame_count":83,"size":37589,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":80,"height":32,"frame_count":83,"size":71714,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":160,"height":64,"frame_count":83,"size":102154,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":160,"height":64,"frame_count":83,"size":141934,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":240,"height":96,"frame_count":83,"size":189836,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":240,"height":96,"frame_count":83,"size":233108,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":320,"height":128,"frame_count":83,"size":395241,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":320,"height":128,"frame_count":83,"size":536878,"format":"WEBP"}]}}},{"id":"60b2fd1aab2a2a9c95bd44a8","name":"YOURM0M","flags":0,"timestamp":1677165258036,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60b2fd1aab2a2a9c95bd44a8","name":"YOURM0M","flags":0,"lifecycle":3,"state":["NO_PERSONAL","LISTED"],"listed":true,"animated":true,"owner":{"id":"60b157c63f98974e48c9b7a8","username":"bh4tti","display_name":"bh4tti","avatar_url":"//cdn.7tv.app/user/60b157c63f98974e48c9b7a8/av_647d5e32d4b5d6083e92328c/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60b2fd1aab2a2a9c95bd44a8","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":13,"size":6145,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":13,"size":9950,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":13,"size":12073,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":13,"size":24202,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":13,"size":19522,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":13,"size":38718,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":13,"size":32378,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":13,"size":43644,"format":"WEBP"}]}}},{"id":"61d9b70927a4f6d6544e545e","name":"nymnLulwut","flags":0,"timestamp":1677165258315,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"61d9b70927a4f6d6544e545e","name":"nymnLulwut","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae8fc0ea50f43c9e3ae255","username":"agenttud","display_name":"agenttud","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/273db808-d42f-4dab-9b39-9780ef2777b0-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61d9b70927a4f6d6544e545e","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":159,"size":24445,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":159,"size":134510,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":159,"size":74453,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":159,"size":310182,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":159,"size":150157,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":159,"size":505674,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":159,"size":292595,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":159,"size":550600,"format":"WEBP"}]}}},{"id":"60ae55800e35477634f878fd","name":"forsenParty","flags":0,"timestamp":1677165258619,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60ae55800e35477634f878fd","name":"forsenParty","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60bb55eac2415f99b55a7731","username":"hadezzishappy","display_name":"hadezzishappy","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/23efe3c6-dc9b-4a48-93c3-5eec7b6ca6e0-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60ae55800e35477634f878fd","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":60,"size":17955,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":60,"size":36360,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":60,"size":33477,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":60,"size":70460,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":60,"size":51084,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":60,"size":111144,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":60,"size":71276,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":60,"size":128888,"format":"WEBP"}]}}},{"id":"618330c5f1ae15abc7ebb8c6","name":"thIS","flags":0,"timestamp":1677165259269,"actor_id":"60ae8fc0ea50f43c9e3ae255","data":{"id":"618330c5f1ae15abc7ebb8c6","name":"THIS","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae518c0e35477634c151f1","username":"fabulouspotato69","display_name":"FabulousPotato69","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/618330c5f1ae15abc7ebb8c6","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":53,"height":32,"frame_count":31,"size":13197,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":53,"height":32,"frame_count":31,"size":42224,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":106,"height":64,"frame_count":31,"size":39278,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":106,"height":64,"frame_count":31,"size":101722,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":159,"height":96,"frame_count":31,"size":68584,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":159,"height":96,"frame_count":31,"size":177156,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":212,"height":128,"frame_count":31,"size":109119,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":212,"height":128,"frame_count":31,"size":195216,"format":"WEBP"}]}}},{"id":"61fc0f1123f0a55b0ba8313d","name":"Fridge","flags":0,"timestamp":1677249038027,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"61fc0f1123f0a55b0ba8313d","name":"Fridge","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60ae3cb1b2ecb0150521fa1f","username":"waterboiledpizza","display_name":"WaterBoiledPizza","avatar_url":"//cdn.7tv.app/user/60ae3cb1b2ecb0150521fa1f/av_652806843e9323c51e05082e/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61fc0f1123f0a55b0ba8313d","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1142,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":930,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2161,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2206,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3291,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":3872,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4695,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":5764,"format":"WEBP"}]}}},{"id":"60aef6b7a564afa26eaabc37","name":"OMGScoots","flags":0,"timestamp":1677251687882,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60aef6b7a564afa26eaabc37","name":"OMGScoots","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60aee97511a994a4acbefca7","username":"voidmakesvids","display_name":"VoidMakesVids","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/8dd7f316-85f5-414a-bd0d-603d67289cbf-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60aef6b7a564afa26eaabc37","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1240,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1008,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2283,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2430,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3334,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":3826,"format":"WEBP"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":3788,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4522,"format":"AVIF"}]}}},{"id":"60b0e22c4daf0d3e211877ca","name":"PepeA","flags":0,"timestamp":1677251688177,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60b0e22c4daf0d3e211877ca","name":"PepeA","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae3df2aee2aa55382ba24d","username":"khaltour","display_name":"Khaltour","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/f39529e7-1d39-4e94-8a60-d4097c3ec31a-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60b0e22c4daf0d3e211877ca","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":3,"size":3025,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":3,"size":3398,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":3,"size":4903,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":3,"size":8066,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":3,"size":7186,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":3,"size":13522,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":3,"size":10132,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":3,"size":15128,"format":"WEBP"}]}}},{"id":"60ae9d8f229664e8660449aa","name":"nymnShuffle","flags":0,"timestamp":1677251688471,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60ae9d8f229664e8660449aa","name":"nymnShuffle","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae3ca8aee2aa553822cef3","username":"justmariusz","display_name":"JUSTmariusz","avatar_url":"//cdn.7tv.app/user/60ae3ca8aee2aa553822cef3/av_63ab8d2324f58877cf6dd47f/3x_static.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60ae9d8f229664e8660449aa","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":16,"size":14218,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":16,"size":7984,"format":"AVIF"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":16,"size":14737,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":16,"size":30308,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":16,"size":22446,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":16,"size":48902,"format":"WEBP"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":16,"size":52670,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":16,"size":33583,"format":"AVIF"}]}}},{"id":"603e6f69284626000d068846","name":"VaN","flags":0,"timestamp":1677338118134,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"603e6f69284626000d068846","name":"VaN","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"603cac0896832ffa78c463e1","username":"rupusen","display_name":"rupusen","avatar_url":"//static-cdn.jtvnw.net/user-default-pictures-uv/998f01ae-def8-11e9-b95c-784f43822e80-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/603e6f69284626000d068846","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":29,"height":32,"frame_count":1,"size":1148,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":29,"height":32,"frame_count":1,"size":1363,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":58,"height":64,"frame_count":1,"size":3086,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":58,"height":64,"frame_count":1,"size":2792,"format":"AVIF"},{"name":"3x.avif","static_name":"3x_static.avif","width":87,"height":96,"frame_count":1,"size":4301,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":87,"height":96,"frame_count":1,"size":5306,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":116,"height":128,"frame_count":1,"size":6185,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":116,"height":128,"frame_count":1,"size":7712,"format":"WEBP"}]}}},{"id":"60ae65b29627f9aff4fd8bef","name":"NOOOO","flags":0,"timestamp":1677338118442,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60ae65b29627f9aff4fd8bef","name":"NOOOO","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae49350e35477634486602","username":"justrogan","display_name":"JustRogan","avatar_url":"//cdn.7tv.app/pp/60ae49350e35477634486602/88d6e3c4265f4be0a452c812c146da50","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60ae65b29627f9aff4fd8bef","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":34,"height":32,"frame_count":6,"size":5897,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":34,"height":32,"frame_count":6,"size":7004,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":68,"height":64,"frame_count":6,"size":10420,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":68,"height":64,"frame_count":6,"size":15240,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":102,"height":96,"frame_count":6,"size":15881,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":102,"height":96,"frame_count":6,"size":24568,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":136,"height":128,"frame_count":6,"size":21125,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":136,"height":128,"frame_count":6,"size":31006,"format":"WEBP"}]}}},{"id":"60aef3aea564afa26e686d8c","name":"5Head","flags":0,"timestamp":1677338118741,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60aef3aea564afa26e686d8c","name":"5Head","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60ae49350e35477634486602","username":"justrogan","display_name":"JustRogan","avatar_url":"//cdn.7tv.app/pp/60ae49350e35477634486602/88d6e3c4265f4be0a452c812c146da50","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60aef3aea564afa26e686d8c","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":26,"height":32,"frame_count":1,"size":1164,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":26,"height":32,"frame_count":1,"size":866,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":52,"height":64,"frame_count":1,"size":1893,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":52,"height":64,"frame_count":1,"size":1918,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":78,"height":96,"frame_count":1,"size":2959,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":78,"height":96,"frame_count":1,"size":3126,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":104,"height":128,"frame_count":1,"size":3789,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":104,"height":128,"frame_count":1,"size":3652,"format":"WEBP"}]}}},{"id":"616ee25bb6d21adaffbe9177","name":"ApolloWake","flags":0,"timestamp":1677348522278,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"616ee25bb6d21adaffbe9177","name":"ApolloWake","flags":0,"tags":["apollo","wokege","awakege"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae3e3eb2ecb01505346ae9","username":"fawcan","display_name":"Fawcan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38051bdb-83c6-4716-95e0-731462e02b45-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/616ee25bb6d21adaffbe9177","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":61,"size":9717,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":61,"size":43672,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":61,"size":31009,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":61,"size":107138,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":61,"size":68123,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":61,"size":181832,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":61,"size":123411,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":61,"size":142316,"format":"WEBP"}]}}},{"id":"63fa2b0117478c0c59fc73c1","name":"MEMONEYING","flags":0,"timestamp":1677359177728,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"63fa2b0117478c0c59fc73c1","name":"MEMONEYING","flags":0,"tags":["nymn","memoney","nime","timetonime","scammer","money"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"6131dab2af9287c4eb609268","username":"vicneeel","display_name":"vicneeel","avatar_url":"//cdn.7tv.app/user/6131dab2af9287c4eb609268/av_6520576332b1db5b90ef6b24/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63fa2b0117478c0c59fc73c1","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":48,"height":32,"frame_count":6,"size":8387,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":48,"height":32,"frame_count":6,"size":7488,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":96,"height":64,"frame_count":6,"size":18143,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":96,"height":64,"frame_count":6,"size":16872,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":144,"height":96,"frame_count":6,"size":28594,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":144,"height":96,"frame_count":6,"size":27170,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":192,"height":128,"frame_count":6,"size":39152,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":192,"height":128,"frame_count":6,"size":38032,"format":"WEBP"}]}}},{"id":"60a1babb3c3362f9a4b8b33a","name":"catKISS","flags":0,"timestamp":1677408784164,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"60a1babb3c3362f9a4b8b33a","name":"catKISS","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60867b015e01df61570ab900","username":"cupofeggy","display_name":"CupOfEggy","avatar_url":"//cdn.7tv.app/pp/60867b015e01df61570ab900/cb06710bb97347d6bd78febdab716ac0","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60a1babb3c3362f9a4b8b33a","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":51,"size":24237,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":51,"size":40876,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":51,"size":86982,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":51,"size":49974,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":51,"size":139518,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":51,"size":79505,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":51,"size":110441,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":51,"size":146510,"format":"WEBP"}]}}},{"id":"60b38397e42f1681cfbcfc79","name":"tensePls","flags":0,"timestamp":1677416916643,"actor_id":"60ae8fc0ea50f43c9e3ae255","data":{"id":"60b38397e42f1681cfbcfc79","name":"tensePls","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60b1428c213e3888f9638acf","username":"senderak","display_name":"senderak","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/9d953c4e-3f61-48b8-8e45-08071276d03d-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60b38397e42f1681cfbcfc79","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":99,"size":51569,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":99,"size":92598,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":99,"size":116885,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":99,"size":201828,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":99,"size":191035,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":99,"size":334342,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":99,"size":272592,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":99,"size":364564,"format":"WEBP"}]}}},{"id":"6298e43b4e04a1a42ae729e8","name":"FDM","flags":0,"timestamp":1677420364894,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"6298e43b4e04a1a42ae729e8","name":"FDM","flags":0,"tags":["feelsdankman","dank","feelsdankerman","feelsdonkman"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60b04a7fad7fb4b50bd3a982","username":"brian6932","display_name":"brian6932","avatar_url":"//cdn.7tv.app/user/60b04a7fad7fb4b50bd3a982/av_64f8035f8bef730969094d7a/3x_static.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6298e43b4e04a1a42ae729e8","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1196,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1008,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2398,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2270,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":3956,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3283,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":6008,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4609,"format":"AVIF"}]}}},{"id":"63fb74b848d607ec9b98f08f","name":"myeah","flags":0,"timestamp":1677423859729,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"63fb74b848d607ec9b98f08f","name":"VibeOff","flags":0,"tags":["vibeoff","finger","breakingbad","despair","life","dance"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae434b5d3fdae58382926a","username":"ayyybubu","display_name":"ayyybubu","avatar_url":"//cdn.7tv.app/user/60ae434b5d3fdae58382926a/av_636fa88735a18fd8b17e7399/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","62d86a8419fdcf401421c5ae","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63fb74b848d607ec9b98f08f","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":57,"height":32,"frame_count":79,"size":21745,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":57,"height":32,"frame_count":79,"size":54088,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":114,"height":64,"frame_count":79,"size":51556,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":114,"height":64,"frame_count":79,"size":122104,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":171,"height":96,"frame_count":79,"size":78302,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":171,"height":96,"frame_count":79,"size":190482,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":228,"height":128,"frame_count":79,"size":121995,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":228,"height":128,"frame_count":79,"size":266852,"format":"WEBP"}]}}},{"id":"61bb1c745804e220aa6aafe2","name":"GuitarYime","flags":0,"timestamp":1677424260473,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"61bb1c745804e220aa6aafe2","name":"GuitarYime","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"61938ebbd34608492cc37ff1","username":"fluxenis","display_name":"fluxenis","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61bb1c745804e220aa6aafe2","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":45,"height":32,"frame_count":105,"size":56455,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":45,"height":32,"frame_count":105,"size":139148,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":90,"height":64,"frame_count":105,"size":155635,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":90,"height":64,"frame_count":105,"size":327836,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":135,"height":96,"frame_count":105,"size":307600,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":135,"height":96,"frame_count":105,"size":564100,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":180,"height":128,"frame_count":105,"size":460686,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":180,"height":128,"frame_count":105,"size":726320,"format":"WEBP"}]}}},{"id":"6040aa41cf6746000db1034e","name":"ppPoof","flags":0,"timestamp":1677428131518,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"6040aa41cf6746000db1034e","name":"ppPoof","flags":0,"lifecycle":3,"state":["PERSONAL","LISTED"],"listed":true,"animated":true,"owner":{"id":"603ca8f696832ffa78c01eb4","username":"mauriplss","display_name":"Mauriplss","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/a428f0f0-bdd4-4c93-ac4b-ed174244cb66-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6040aa41cf6746000db1034e","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":41,"size":9739,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":41,"size":10256,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":41,"size":16918,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":41,"size":14363,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":41,"size":30090,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":41,"size":20506,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":41,"size":20366,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":41,"size":25076,"format":"WEBP"}]}}},{"id":"616b1b7fc52da56cd490a72f","name":"vanish0","flags":1,"timestamp":1677428172120,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"616b1b7fc52da56cd490a72f","name":"vanish","flags":256,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae3e3eb2ecb01505346ae9","username":"fawcan","display_name":"Fawcan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38051bdb-83c6-4716-95e0-731462e02b45-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/616b1b7fc52da56cd490a72f","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":35,"size":5717,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":35,"size":3734,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":35,"size":7779,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":35,"size":4340,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":35,"size":11368,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":35,"size":6144,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":35,"size":14227,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":35,"size":5480,"format":"WEBP"}]}}},{"id":"60b040934d83b66c44f21992","name":"Borpa","flags":0,"timestamp":1677439952485,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60b040934d83b66c44f21992","name":"Borpa","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60aebf0ae90f445e43b37fe5","username":"prog0ldfish","display_name":"ProG0ldfish","avatar_url":"//cdn.7tv.app/user/60aebf0ae90f445e43b37fe5/av_6353ef0d7f642dee0e30c1f4/3x_static.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60b040934d83b66c44f21992","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1029,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":682,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1599,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":1626,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":2381,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":2590,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":2924,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":3502,"format":"WEBP"}]}}},{"id":"63fb71e50fd141cefb090fc7","name":"cupFlip","flags":0,"timestamp":1677445161036,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"63fb71e50fd141cefb090fc7","name":"cupFlip","flags":0,"tags":["cat","cup","flip"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae3e3eb2ecb01505346ae9","username":"fawcan","display_name":"Fawcan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38051bdb-83c6-4716-95e0-731462e02b45-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63fb71e50fd141cefb090fc7","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":24,"height":32,"frame_count":74,"size":17086,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":24,"height":32,"frame_count":74,"size":33776,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":48,"height":64,"frame_count":74,"size":37097,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":48,"height":64,"frame_count":74,"size":77668,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":72,"height":96,"frame_count":74,"size":61452,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":72,"height":96,"frame_count":74,"size":129584,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":96,"height":128,"frame_count":74,"size":86496,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":96,"height":128,"frame_count":74,"size":177962,"format":"WEBP"}]}}},{"id":"6143bb1d962a60904864c20f","name":"HACKERMANS","flags":0,"timestamp":1677500778645,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"6143bb1d962a60904864c20f","name":"HACKERMANS","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60b04a7fad7fb4b50bd3a982","username":"brian6932","display_name":"brian6932","avatar_url":"//cdn.7tv.app/user/60b04a7fad7fb4b50bd3a982/av_64f8035f8bef730969094d7a/3x_static.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6143bb1d962a60904864c20f","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":11,"size":5353,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":11,"size":10652,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":11,"size":12171,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":11,"size":28024,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":11,"size":21939,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":11,"size":50764,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":11,"size":38741,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":11,"size":61916,"format":"WEBP"}]}}},{"id":"60b7dc3655c320f0e8aa0096","name":"DonkLeave","flags":0,"timestamp":1677500949180,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60b7dc3655c320f0e8aa0096","name":"DonkLeave","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60b2a8d9be695c536f66179e","username":"venceslavsquare","display_name":"VenceslavSquare","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/9bce07d2-d3f9-4977-8e17-028ede768a35-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60b7dc3655c320f0e8aa0096","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":45,"size":19625,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":45,"size":33798,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":45,"size":37888,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":45,"size":64700,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":45,"size":60694,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":45,"size":100520,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":45,"size":82333,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":45,"size":113570,"format":"WEBP"}]}}},{"id":"60b539041fd3f03d860ad49b","name":"DonkEnter","flags":0,"timestamp":1677500951070,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60b539041fd3f03d860ad49b","name":"DonkArrive","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60b0133774d234a96956705c","username":"gabrelaarves","display_name":"gabrelaarves","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/951765d1-54d7-4573-b890-19072960942e-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60b539041fd3f03d860ad49b","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":105,"size":27171,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":105,"size":74778,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":105,"size":56872,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":105,"size":143026,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":105,"size":92785,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":105,"size":221362,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":105,"size":139562,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":105,"size":252366,"format":"WEBP"}]}}},{"id":"623c67e4955cccbe23975657","name":"RIPBOZO","flags":0,"timestamp":1677500955520,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"623c67e4955cccbe23975657","name":"RIPBOZO","flags":0,"tags":["bozo","foxenkin"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"61938ebbd34608492cc37ff1","username":"fluxenis","display_name":"fluxenis","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/623c67e4955cccbe23975657","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":40,"height":32,"frame_count":167,"size":36536,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":40,"height":32,"frame_count":167,"size":145334,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":80,"height":64,"frame_count":167,"size":129891,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":80,"height":64,"frame_count":167,"size":370634,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":120,"height":96,"frame_count":167,"size":249623,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":120,"height":96,"frame_count":167,"size":642334,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":160,"height":128,"frame_count":167,"size":486098,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":160,"height":128,"frame_count":167,"size":535824,"format":"WEBP"}]}}},{"id":"6183c747f1ae15abc7ebd487","name":"FEELSWAYTOODONKMAN","flags":0,"timestamp":1677517778023,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"6183c747f1ae15abc7ebd487","name":"FEELSWAYTOODONKMAN","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60eed06ef56d067b5420e4f3","username":"tepx","display_name":"tepx","avatar_url":"//cdn.7tv.app/pp/60eed06ef56d067b5420e4f3/f8a8017ef06c4a58bcda4c2074239ba8","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6183c747f1ae15abc7ebd487","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":479,"size":247314,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":479,"size":368000,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":479,"size":532935,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":479,"size":749084,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":479,"size":995676,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":479,"size":1280664,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":479,"size":1570752,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":479,"size":1484408,"format":"WEBP"}]}}},{"id":"6318ae635a703c4a98dad44b","name":"!vanish","flags":0,"timestamp":1677531630837,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"6318ae635a703c4a98dad44b","name":"peepoVanish","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60fd5a2b4653f5d6c174f52c","username":"gambloide","display_name":"Gambloide","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/cf482c8f-4f2a-4ab1-878d-00850cc8c1eb-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6318ae635a703c4a98dad44b","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":38,"height":32,"frame_count":48,"size":7969,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":38,"height":32,"frame_count":48,"size":27782,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":76,"height":64,"frame_count":48,"size":13005,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":76,"height":64,"frame_count":48,"size":61082,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":114,"height":96,"frame_count":48,"size":18694,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":114,"height":96,"frame_count":48,"size":97866,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":152,"height":128,"frame_count":48,"size":25660,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":152,"height":128,"frame_count":48,"size":134572,"format":"WEBP"}]}}},{"id":"60aebc35e04200b3d12fbc40","name":"donkJam","flags":0,"timestamp":1677554118584,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60aebc35e04200b3d12fbc40","name":"donkJam","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"6091cdb92fe19cf34b935a66","username":"herohyrule","display_name":"HeroHyrule","avatar_url":"//cdn.7tv.app/pp/6091cdb92fe19cf34b935a66/6396e971b87742d29531c44195778442","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60aebc35e04200b3d12fbc40","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":2,"size":2234,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":2,"size":3500,"format":"AVIF"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":2,"size":5803,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":2,"size":4682,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":2,"size":8304,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":2,"size":7286,"format":"WEBP"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":2,"size":8226,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":2,"size":10773,"format":"AVIF"}]}}},{"id":"62348f2a961be6c9af3d226e","name":"bruhSlide","flags":0,"timestamp":1677554118887,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"62348f2a961be6c9af3d226e","name":"bruhSlide","flags":0,"tags":["bruh","bruhsit","cmonbruh"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60be7d86529aefe4a0c5be9a","username":"djoka","display_name":"Djoka","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/888b00cf-48ca-48b9-be2d-490c95d70ced-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/62348f2a961be6c9af3d226e","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":96,"height":32,"frame_count":109,"size":27684,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":96,"height":32,"frame_count":109,"size":107200,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":192,"height":64,"frame_count":109,"size":61400,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":192,"height":64,"frame_count":109,"size":237930,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":288,"height":96,"frame_count":109,"size":134863,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":288,"height":96,"frame_count":109,"size":386190,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":384,"height":128,"frame_count":109,"size":228055,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":384,"height":128,"frame_count":109,"size":398224,"format":"WEBP"}]}}},{"id":"63fdf8aeface0f3bbeabcafc","name":"Beerge","flags":0,"timestamp":1677588986625,"actor_id":"60ae8fc0ea50f43c9e3ae255","data":{"id":"63fdf8aeface0f3bbeabcafc","name":"Beerge","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae518c0e35477634c151f1","username":"fabulouspotato69","display_name":"FabulousPotato69","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63fdf8aeface0f3bbeabcafc","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":48,"height":32,"frame_count":166,"size":42697,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":48,"height":32,"frame_count":166,"size":140684,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":96,"height":64,"frame_count":166,"size":113634,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":96,"height":64,"frame_count":166,"size":307658,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":144,"height":96,"frame_count":166,"size":218887,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":144,"height":96,"frame_count":166,"size":489922,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":192,"height":128,"frame_count":166,"size":485016,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":192,"height":128,"frame_count":166,"size":786950,"format":"WEBP"}]}}},{"id":"60af808584a2b8e655a47928","name":"Offline","flags":0,"timestamp":1677589043309,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60af808584a2b8e655a47928","name":"Offline","flags":0,"tags":["cry","sad","peeposhut","peepo"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"6117c599815bc8f5557a46c9","username":"glhf115","display_name":"glhf115","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60af808584a2b8e655a47928","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":36,"height":32,"frame_count":40,"size":9040,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":36,"height":32,"frame_count":40,"size":36662,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":72,"height":64,"frame_count":40,"size":86548,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":72,"height":64,"frame_count":40,"size":20233,"format":"AVIF"},{"name":"3x.avif","static_name":"3x_static.avif","width":108,"height":96,"frame_count":40,"size":35529,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":108,"height":96,"frame_count":40,"size":141594,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":144,"height":128,"frame_count":40,"size":55466,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":144,"height":128,"frame_count":40,"size":165560,"format":"WEBP"}]}}},{"id":"63cd8ca0f4657baa9ed9910f","name":"ppCircle","flags":0,"timestamp":1677599848194,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"63cd8ca0f4657baa9ed9910f","name":"ppCircle","flags":0,"tags":["ppl"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae2af4aee2aa5538ab2144","username":"sunred_","display_name":"SunRed_","avatar_url":"//cdn.7tv.app/pp/60ae2af4aee2aa5538ab2144/acc28924022046e3b790ccaf4c7b4c53","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63cd8ca0f4657baa9ed9910f","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":36,"size":11614,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":36,"size":10456,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":36,"size":24385,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":36,"size":21364,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":36,"size":39818,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":36,"size":32686,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":36,"size":55003,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":36,"size":43610,"format":"WEBP"}]}}},{"id":"63fd1165face0f3bbeabb6ed","name":"nimeJAM","flags":0,"timestamp":1677614622369,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"63fd1165face0f3bbeabb6ed","name":"nimeJAM","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae409daee2aa55383ebb4b","username":"tolatos","display_name":"tolatos","avatar_url":"//cdn.7tv.app/user/60ae409daee2aa55383ebb4b/av_657b75e8b0d945ef35823739/3x.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63fd1165face0f3bbeabb6ed","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":283,"size":80975,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":283,"size":151268,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":283,"size":189197,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":283,"size":276066,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":283,"size":352684,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":283,"size":430194,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":283,"size":657412,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":283,"size":569284,"format":"WEBP"}]}}},{"id":"61827c364ea2f24e50092f69","name":"partyTime","flags":0,"timestamp":1677760013961,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"61827c364ea2f24e50092f69","name":"partyTime","flags":0,"tags":["cat","kitten","kitty","meow","rave","rainbow"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60d37adc205cf63c7eef6871","username":"eazylemnsqeezy","display_name":"eazylemnsqeezy","avatar_url":"//cdn.7tv.app/user/60d37adc205cf63c7eef6871/av_64200b330ef35e7ab89f2db4/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61827c364ea2f24e50092f69","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":69,"height":32,"frame_count":300,"size":135603,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":69,"height":32,"frame_count":300,"size":375690,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":138,"height":64,"frame_count":300,"size":404899,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":138,"height":64,"frame_count":300,"size":929366,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":207,"height":96,"frame_count":300,"size":740567,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":207,"height":96,"frame_count":300,"size":1521364,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":276,"height":128,"frame_count":300,"size":1086988,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":276,"height":128,"frame_count":300,"size":1776472,"format":"WEBP"}]}}},{"id":"6102a37ba57eeb23c0e3e5cb","name":"peepoDJ","flags":0,"timestamp":1677760019738,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"6102a37ba57eeb23c0e3e5cb","name":"peepoDJ","flags":0,"lifecycle":3,"state":["NO_PERSONAL","LISTED"],"listed":true,"animated":true,"owner":{"id":"60f28c1e31ba6ae622a49c16","username":"rsnowwolf","display_name":"rSnowWolf","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/f43e8c2d-5eb2-4747-9351-0a3574c535c0-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6102a37ba57eeb23c0e3e5cb","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":72,"size":48798,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":72,"size":77398,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":72,"size":115826,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":72,"size":179294,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":72,"size":199776,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":72,"size":303142,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":72,"size":301659,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":72,"size":374542,"format":"WEBP"}]}}},{"id":"6360b863593eb316d898eb78","name":"WideRaveTime","flags":1,"timestamp":1677760059276,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"6360b863593eb316d898eb78","name":"WideRaveTime","flags":256,"tags":["edm","disco","jam","pls","dance","party"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60f5e290e57bec021618c4a4","username":"ansonx10","display_name":"AnsonX10","avatar_url":"//cdn.7tv.app/user/60f5e290e57bec021618c4a4/av_63617cc39018da6429bc0298/3x_static.webp","style":{"color":401323775},"roles":["60b3f1ea886e63449c5263b1","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6360b863593eb316d898eb78","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":96,"height":32,"frame_count":230,"size":222927,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":96,"height":32,"frame_count":230,"size":495282,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":192,"height":64,"frame_count":230,"size":413033,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":192,"height":64,"frame_count":230,"size":1155286,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":288,"height":96,"frame_count":230,"size":608043,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":288,"height":96,"frame_count":230,"size":1870006,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":384,"height":128,"frame_count":230,"size":835329,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":384,"height":128,"frame_count":230,"size":2767168,"format":"WEBP"}]}}},{"id":"631d3178d47e611c913db50a","name":"RaveTime","flags":1,"timestamp":1677760142065,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"631d3178d47e611c913db50a","name":"RaveTime","flags":256,"tags":["edm","party","lights","dance","rave","disco"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60f5e290e57bec021618c4a4","username":"ansonx10","display_name":"AnsonX10","avatar_url":"//cdn.7tv.app/user/60f5e290e57bec021618c4a4/av_63617cc39018da6429bc0298/3x_static.webp","style":{"color":401323775},"roles":["60b3f1ea886e63449c5263b1","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/631d3178d47e611c913db50a","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":184,"size":103285,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":184,"size":190170,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":184,"size":185490,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":184,"size":441520,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":184,"size":279313,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":184,"size":714322,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":184,"size":407913,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":184,"size":1093616,"format":"WEBP"}]}}},{"id":"61335162a1968527a66917c7","name":"Adge","flags":0,"timestamp":1677761995918,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"61335162a1968527a66917c7","name":"Adge","flags":0,"tags":["ads","twitch","adblock"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"6121baae0c9f70780af1766d","username":"sp4rkillz","display_name":"Sp4rkillz","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/43da3c6e-ac1c-4bdd-b89d-65c4f783bf30-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61335162a1968527a66917c7","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":96,"height":25,"frame_count":31,"size":8740,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":96,"height":25,"frame_count":31,"size":8056,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":192,"height":50,"frame_count":31,"size":15901,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":192,"height":50,"frame_count":31,"size":15074,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":288,"height":75,"frame_count":31,"size":23510,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":288,"height":75,"frame_count":31,"size":27160,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":384,"height":100,"frame_count":31,"size":29244,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":384,"height":100,"frame_count":31,"size":34862,"format":"WEBP"}]}}},{"id":"62580411131d4588262a76ec","name":"ZZoomer","flags":0,"timestamp":1677763985342,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"62580411131d4588262a76ec","name":"ZZoomer","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"61390fdcf5c7c1b549d5c10d","username":"skamiro","display_name":"SkaMiro","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/170ee35b-e26b-4558-a2b9-b91f8f16414c-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/62580411131d4588262a76ec","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":13,"size":7838,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":13,"size":13696,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":13,"size":14702,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":13,"size":31522,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":13,"size":21235,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":13,"size":52680,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":13,"size":22500,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":13,"size":73824,"format":"WEBP"}]}}},{"id":"63fbe1c7f2915b442ca8fe70","name":"Ditching","flags":0,"timestamp":1677770118380,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"63fbe1c7f2915b442ca8fe70","name":"Ditching","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"63195e6329a5627b71e31b9b","username":"windycityrockr","display_name":"WindyCityRockr","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/4312bc04-0ea5-45fd-8c4d-8fe72d86448b-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63fbe1c7f2915b442ca8fe70","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":96,"height":32,"frame_count":1,"size":2086,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":96,"height":32,"frame_count":1,"size":3422,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":192,"height":64,"frame_count":1,"size":4014,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":192,"height":64,"frame_count":1,"size":9774,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":288,"height":96,"frame_count":1,"size":5552,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":288,"height":96,"frame_count":1,"size":17528,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":384,"height":128,"frame_count":1,"size":7284,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":384,"height":128,"frame_count":1,"size":26952,"format":"WEBP"}]}}},{"id":"60cecde0bfcc20ec67dd0ed0","name":"HarryPottah","flags":0,"timestamp":1677770118693,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60cecde0bfcc20ec67dd0ed0","name":"HarryPottah","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60b561cc04283ab952bfd4e0","username":"on_a_stack","display_name":"On_a_stack","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/1ed36b65-71c8-4eb2-a6a7-83ad2bb7566a-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60cecde0bfcc20ec67dd0ed0","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1495,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1112,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2910,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2762,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4381,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4656,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5780,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":5816,"format":"WEBP"}]}}},{"id":"620425c25ccb247397667d59","name":"NOkey","flags":0,"timestamp":1677770119225,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"620425c25ccb247397667d59","name":"NOkey","flags":0,"tags":["notokay","xqcl"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae3d75aee2aa55382883c2","username":"victorbaya","display_name":"victorbaya","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/4f4c5649-c2f3-4837-a46f-486df3dde891-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/620425c25ccb247397667d59","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1693,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1356,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":3678,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":3466,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":5877,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":6052,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":8605,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":9294,"format":"WEBP"}]}}},{"id":"60ccf4479f5edeff9938fa77","name":"SUSSY","flags":0,"timestamp":1677770119542,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60ccf4479f5edeff9938fa77","name":"SUSSY","flags":0,"lifecycle":3,"state":["PERSONAL","LISTED"],"listed":true,"animated":true,"owner":{"id":"6144a9f80969108b67193814","username":"epicdonutdude_","display_name":"EpicDonutDude_","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/624c1b52-530b-4707-87d3-45b4c9cd1acf-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60ccf4479f5edeff9938fa77","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":163,"size":49082,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":163,"size":145236,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":163,"size":116649,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":163,"size":323622,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":163,"size":204573,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":163,"size":526310,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":163,"size":314301,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":163,"size":558662,"format":"WEBP"}]}}},{"id":"60ae3714aee2aa553806de31","name":"forsenShuffle","flags":0,"timestamp":1677770119875,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60ae3714aee2aa553806de31","name":"forsenShuffle","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60635b50452cea4685f26b34","username":"hecrzy","display_name":"heCrzy","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/583dd5ac-2fe8-4ead-a20d-e10770118c5f-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60ae3714aee2aa553806de31","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":10,"size":8118,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":10,"size":5769,"format":"AVIF"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":10,"size":10930,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":10,"size":17212,"format":"WEBP"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":10,"size":29134,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":10,"size":16653,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":10,"size":31612,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":10,"size":22222,"format":"AVIF"}]}}},{"id":"6401add417478c0c59fd2620","name":"nymnLove","flags":0,"timestamp":1677831811845,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"6401add417478c0c59fd2620","name":"nymnLove","flags":0,"tags":["gondola","heart","nymn","love"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"612fad78a77c17adb4478f0f","username":"rexinus1","display_name":"Rexinus1","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/47acff52-7ea8-4d2e-8f40-4c050b1d360c-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6401add417478c0c59fd2620","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1421,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1846,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2530,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":5684,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3700,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":8478,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4637,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":13138,"format":"WEBP"}]}}},{"id":"61377f7d1d23ef7131f59b76","name":"WaterIceSaltAyy","flags":0,"timestamp":1677847764664,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"61377f7d1d23ef7131f59b76","name":"sonic","flags":0,"tags":["watericesaltayy"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60b0c4c788e8246a4b16258b","username":"der_sheff","display_name":"der_sheff","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/1add0749-b3be-454b-aa82-cb332c03e843-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61377f7d1d23ef7131f59b76","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":31,"height":32,"frame_count":9,"size":8286,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":31,"height":32,"frame_count":9,"size":6014,"format":"AVIF"},{"name":"2x.avif","static_name":"2x_static.avif","width":62,"height":64,"frame_count":9,"size":11561,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":62,"height":64,"frame_count":9,"size":16648,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":93,"height":96,"frame_count":9,"size":18962,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":93,"height":96,"frame_count":9,"size":26394,"format":"WEBP"},{"name":"4x.webp","static_name":"4x_static.webp","width":124,"height":128,"frame_count":9,"size":29726,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":124,"height":128,"frame_count":9,"size":24446,"format":"AVIF"}]}}},{"id":"619c69e370bd99598795c215","name":"BOOMIES","flags":0,"timestamp":1677852227410,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"619c69e370bd99598795c215","name":"BOOMIES","flags":0,"lifecycle":3,"state":["PERSONAL","LISTED"],"listed":true,"animated":true,"owner":{"id":"6053853cb4d31e459fdaa2dc","username":"laden","display_name":"Laden","avatar_url":"//cdn.7tv.app/pp/6053853cb4d31e459fdaa2dc/a94c67d7736940feb543e42024b740ef","style":{"color":849892095},"roles":["60724f65e93d828bf8858789","6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/619c69e370bd99598795c215","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":100,"size":65971,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":100,"size":110776,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":100,"size":185153,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":100,"size":270468,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":100,"size":314422,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":100,"size":470538,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":100,"size":535174,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":100,"size":551310,"format":"WEBP"}]}}},{"id":"64026a10a283055403bd5410","name":"silly","flags":0,"timestamp":1677880510400,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"64026a10a283055403bd5410","name":"Silly","flags":0,"tags":["animal","cat","nymn","silly","funny","cute"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"612fad78a77c17adb4478f0f","username":"rexinus1","display_name":"Rexinus1","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/47acff52-7ea8-4d2e-8f40-4c050b1d360c-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64026a10a283055403bd5410","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":31,"height":32,"frame_count":1,"size":1165,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":31,"height":32,"frame_count":1,"size":1758,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":62,"height":64,"frame_count":1,"size":1885,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":62,"height":64,"frame_count":1,"size":4620,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":93,"height":96,"frame_count":1,"size":2623,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":93,"height":96,"frame_count":1,"size":8498,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":124,"height":128,"frame_count":1,"size":3350,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":124,"height":128,"frame_count":1,"size":13386,"format":"WEBP"}]}}},{"id":"61178e9c25a41a1170572a0b","name":"forseninsane","flags":0,"timestamp":1677986118159,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"61178e9c25a41a1170572a0b","name":"forsenInsane","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae82ae8d6322a2a8db908e","username":"servasoida","display_name":"servasoida","avatar_url":"//cdn.7tv.app/user/60ae82ae8d6322a2a8db908e/av_63b6014c888238aa9917dc3b/3x_static.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61178e9c25a41a1170572a0b","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":3,"size":3144,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":3,"size":2690,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":3,"size":5415,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":3,"size":6170,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":3,"size":8009,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":3,"size":10458,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":3,"size":12090,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":3,"size":12202,"format":"WEBP"}]}}},{"id":"611670497327a61fe25e56b0","name":"OkeyL","flags":0,"timestamp":1677986118929,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"611670497327a61fe25e56b0","name":"OkeyL","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60c86e09bfc4a1dd77bf4bc0","username":"tahadm_","display_name":"TahaDM_","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/8ebf1343-968d-4156-9b18-0fa78f2268c2-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/611670497327a61fe25e56b0","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":31,"height":32,"frame_count":1,"size":1533,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":31,"height":32,"frame_count":1,"size":1362,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":62,"height":64,"frame_count":1,"size":3328,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":62,"height":64,"frame_count":1,"size":3630,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":93,"height":96,"frame_count":1,"size":5249,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":93,"height":96,"frame_count":1,"size":6364,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":124,"height":128,"frame_count":1,"size":7798,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":124,"height":128,"frame_count":1,"size":9486,"format":"WEBP"}]}}},{"id":"60af0da312d7701491c6f071","name":"donkRun","flags":0,"timestamp":1677986119219,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60af0da312d7701491c6f071","name":"donkRun","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60aeee9ba564afa26e0f6f43","username":"bigggestbelly","display_name":"BigggestBelly","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/c38cedf7-7b6a-4f8a-bb4f-7016300032b4-profile_image-70x70.jpeg","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60af0da312d7701491c6f071","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":6,"size":7507,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":6,"size":7000,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":6,"size":14534,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":6,"size":14400,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":6,"size":22405,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":6,"size":23262,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":6,"size":30203,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":6,"size":27394,"format":"WEBP"}]}}},{"id":"60a439fbb36c6d95937ba56e","name":"ForsenLookingAtYou","flags":0,"timestamp":1677986119520,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60a439fbb36c6d95937ba56e","name":"ForsenLookingAtYou","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"609ef692b55466cf07525c1f","username":"supernoahtv","display_name":"SupernoahTV","avatar_url":"//cdn.7tv.app/pp/609ef692b55466cf07525c1f/59d9c67280094d24b913ecdbd48c7d10","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60a439fbb36c6d95937ba56e","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1032,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1261,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2408,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2204,"format":"AVIF"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3221,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4114,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4333,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":4516,"format":"WEBP"}]}}},{"id":"60b231dafdd2d7d7bd7d5d9d","name":"peepoFlute","flags":0,"timestamp":1678023556495,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60b231dafdd2d7d7bd7d5d9d","name":"peepoFlute","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60b22c4ab1b03f6c99497d9b","username":"moneyhoarder","display_name":"MoneyHoarder","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/90b964fa-58b3-426a-b914-c968eee0f57e-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60b231dafdd2d7d7bd7d5d9d","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":5,"size":6499,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":5,"size":5624,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":5,"size":13142,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":5,"size":13616,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":5,"size":21026,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":5,"size":23570,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":5,"size":28468,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":5,"size":28034,"format":"WEBP"}]}}},{"id":"62992d7a9f69ab702281d45a","name":"nymnSNIFFA","flags":0,"timestamp":1678036394943,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"62992d7a9f69ab702281d45a","name":"nymnSNIFFA","flags":0,"tags":["sniffa","nymn","yabbe","sniff","pepe","feet"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60b6746f64faf92496330d3c","username":"littlescampi","display_name":"LittleScampi","avatar_url":"//cdn.7tv.app/pp/60b6746f64faf92496330d3c/a1ff043123944a119d69cf9a7062a442","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/62992d7a9f69ab702281d45a","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":140,"size":16631,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":139,"size":137348,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":140,"size":37007,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":140,"size":314970,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":140,"size":73785,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":140,"size":530186,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":140,"size":165048,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":140,"size":718648,"format":"WEBP"}]}}},{"id":"60f4e98ce57bec02166796a5","name":"peepoCute","flags":0,"timestamp":1678191996591,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"60f4e98ce57bec02166796a5","name":"peepoCute","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60b7f8f555c320f0e8c6a27e","username":"vinn700","display_name":"vinn700","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/8d225acd-892b-4ef8-bb06-00cde9018ab2-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60f4e98ce57bec02166796a5","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":36,"height":32,"frame_count":1,"size":1647,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":36,"height":32,"frame_count":1,"size":1356,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":72,"height":64,"frame_count":1,"size":3058,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":72,"height":64,"frame_count":1,"size":3120,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":108,"height":96,"frame_count":1,"size":4571,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":108,"height":96,"frame_count":1,"size":5144,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":144,"height":128,"frame_count":1,"size":6148,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":144,"height":128,"frame_count":1,"size":7564,"format":"WEBP"}]}}},{"id":"634aef3b9e9ae5efe290e3a4","name":"GIGA","flags":0,"timestamp":1678196671553,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"634aef3b9e9ae5efe290e3a4","name":"GIGA","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"6053853cb4d31e459fdaa2dc","username":"laden","display_name":"Laden","avatar_url":"//cdn.7tv.app/pp/6053853cb4d31e459fdaa2dc/a94c67d7736940feb543e42024b740ef","style":{"color":849892095},"roles":["60724f65e93d828bf8858789","6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/634aef3b9e9ae5efe290e3a4","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1199,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":2106,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2432,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":6662,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3728,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":12580,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4902,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":19796,"format":"WEBP"}]}}},{"id":"617ad142e0801fb98788432c","name":"nymnBOOBA","flags":0,"timestamp":1678202118330,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"617ad142e0801fb98788432c","name":"nymnBOOBA","flags":0,"tags":["nymn","booba","lamonting"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae3cb1b2ecb0150521fa1f","username":"waterboiledpizza","display_name":"WaterBoiledPizza","avatar_url":"//cdn.7tv.app/user/60ae3cb1b2ecb0150521fa1f/av_652806843e9323c51e05082e/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/617ad142e0801fb98788432c","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":64,"size":14871,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":64,"size":55156,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":64,"size":48036,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":64,"size":140002,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":64,"size":93940,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":64,"size":234602,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":64,"size":173963,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":64,"size":200156,"format":"WEBP"}]}}},{"id":"610e61f3900cbd77c695815a","name":"BillySmoke","flags":0,"timestamp":1678202118666,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"610e61f3900cbd77c695815a","name":"BillySmoke","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60f8d2e7e57bec021669e176","username":"derxiatos","display_name":"DerXiatos","avatar_url":"//static-cdn.jtvnw.net/user-default-pictures-uv/998f01ae-def8-11e9-b95c-784f43822e80-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/610e61f3900cbd77c695815a","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":263,"size":67635,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":263,"size":220400,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":263,"size":218669,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":263,"size":529960,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":263,"size":404061,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":263,"size":890546,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":263,"size":742381,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":263,"size":1173820,"format":"WEBP"}]}}},{"id":"6388785d0d4985e50d524483","name":"Stare","flags":0,"timestamp":1678202118996,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"6388785d0d4985e50d524483","name":"Stare","flags":0,"tags":["apollo","aploplo","apluplu","nymn","hugo","bozz"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"6054fb35b4d31e459f7cde73","username":"21mtd","display_name":"21mtd","avatar_url":"//cdn.7tv.app/user/6054fb35b4d31e459f7cde73/av_657c1e5fa8b03226340b14d7/3x_static.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6388785d0d4985e50d524483","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":37,"height":32,"frame_count":1,"size":1182,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":37,"height":32,"frame_count":1,"size":1838,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":74,"height":64,"frame_count":1,"size":2121,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":74,"height":64,"frame_count":1,"size":4828,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":111,"height":96,"frame_count":1,"size":3048,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":111,"height":96,"frame_count":1,"size":9046,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":148,"height":128,"frame_count":1,"size":3970,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":148,"height":128,"frame_count":1,"size":13036,"format":"WEBP"}]}}},{"id":"6216d2f73808dfe5c465bc4a","name":"ALERT","flags":1,"timestamp":1678202119745,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"6216d2f73808dfe5c465bc4a","name":"ALERT","flags":256,"tags":["clickbait","arrow","red","circle","bruh","boom"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"61ccf4795fa851bfdf0f3da8","username":"pen15827","display_name":"pen15827","avatar_url":"//static-cdn.jtvnw.net/user-default-pictures-uv/ce57700a-def9-11e9-842d-784f43822e80-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6216d2f73808dfe5c465bc4a","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":34,"height":32,"frame_count":2,"size":2843,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":34,"height":32,"frame_count":2,"size":1116,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":68,"height":64,"frame_count":2,"size":4082,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":68,"height":64,"frame_count":2,"size":2410,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":102,"height":96,"frame_count":2,"size":5224,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":102,"height":96,"frame_count":2,"size":4142,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":136,"height":128,"frame_count":2,"size":6456,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":136,"height":128,"frame_count":2,"size":5144,"format":"WEBP"}]}}},{"id":"623c29a61aeb248de8494e7c","name":"angy","flags":0,"timestamp":1678364637566,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"623c29a61aeb248de8494e7c","name":"angy","flags":0,"tags":["mad","peepo"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"6121288754434411dd0d1fdc","username":"mardexgaming","display_name":"Mardexgaming","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/70ead9d8-9a1a-4703-b49d-15050f91c3c1-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/623c29a61aeb248de8494e7c","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":790,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1121,"format":"AVIF"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1847,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":1856,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":2622,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":2844,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":3434,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":4164,"format":"WEBP"}]}}},{"id":"60ae3ca4aee2aa553822c4a6","name":"docOkay","flags":0,"timestamp":1678418118907,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60ae3ca4aee2aa553822c4a6","name":"docOkay","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60772a85a807bed00612d1ee","username":"lnsc","display_name":"LNSc","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/4207f38c-73f0-4487-a7b2-07ccb27667d1-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60ae3ca4aee2aa553822c4a6","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1313,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":976,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2683,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2460,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4279,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4278,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5866,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":5230,"format":"WEBP"}]}}},{"id":"60baf728f34d2d2acec74711","name":"Phone","flags":0,"timestamp":1678418119662,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60baf728f34d2d2acec74711","name":"Phone","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60ae3df2aee2aa55382ba24d","username":"khaltour","display_name":"Khaltour","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/f39529e7-1d39-4e94-8a60-d4097c3ec31a-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60baf728f34d2d2acec74711","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1092,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":832,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2046,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2111,"format":"AVIF"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3057,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":3348,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4020,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":3736,"format":"WEBP"}]}}},{"id":"60b0c3c86a06830c3de91bba","name":"monkaGIGA","flags":0,"timestamp":1678418120060,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60b0c3c86a06830c3de91bba","name":"monkaGIGA","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60b0c24b7ec2882b3dd6489d","username":"pulshly","display_name":"pulshly","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/7c893266-4d63-4c1b-9b5e-54af97af8b16-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60b0c3c86a06830c3de91bba","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1328,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1080,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2569,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2622,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3966,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4428,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5203,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":6156,"format":"WEBP"}]}}},{"id":"603cb600c20d020014423c6e","name":"forsenSWA","flags":0,"timestamp":1678418120390,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"603cb600c20d020014423c6e","name":"forsenSWA","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"603cb1c696832ffa78cc3bc2","username":"clyvere","display_name":"clyverE","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/3ff40972-0188-4cfc-adbf-8db119d7cf2a-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/603cb600c20d020014423c6e","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":3,"size":3577,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":3,"size":3232,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":3,"size":6389,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":3,"size":7110,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":3,"size":10340,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":3,"size":12454,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":3,"size":20721,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":3,"size":19450,"format":"WEBP"}]}}},{"id":"60af0382b38361ea91337096","name":"!rebel","flags":0,"timestamp":1678544756520,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60af0382b38361ea91337096","name":"peepoRiot","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae49350e35477634486602","username":"justrogan","display_name":"JustRogan","avatar_url":"//cdn.7tv.app/pp/60ae49350e35477634486602/88d6e3c4265f4be0a452c812c146da50","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60af0382b38361ea91337096","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":8,"size":6045,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":8,"size":7868,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":8,"size":12690,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":8,"size":18234,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":8,"size":19890,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":8,"size":31674,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":8,"size":29071,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":8,"size":37002,"format":"WEBP"}]}}},{"id":"619ad4b970bd9959879595f9","name":"!vote","flags":0,"timestamp":1678544762723,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"619ad4b970bd9959879595f9","name":"SOCIALCREDITING","flags":0,"tags":["social","credits","china","lemao"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae5b8d0e354776345188c1","username":"en_djinn","display_name":"En_Djinn","avatar_url":"//cdn.7tv.app/pp/60ae5b8d0e354776345188c1/0c71ab67a84141ed9185919c432137d7","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/619ad4b970bd9959879595f9","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":80,"size":14178,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":80,"size":55796,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":80,"size":30040,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":80,"size":122566,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":80,"size":53764,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":80,"size":194826,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":80,"size":94876,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":80,"size":223152,"format":"WEBP"}]}}},{"id":"62495a74fba06b9273b2de7f","name":"Medievalge","flags":0,"timestamp":1678544767075,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"62495a74fba06b9273b2de7f","name":"Medievalge","flags":0,"tags":["evilge","dge"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"610301ee44c8b7eb9204ff9b","username":"limecookii","display_name":"limecookii","avatar_url":"//cdn.7tv.app/pp/610301ee44c8b7eb9204ff9b/3a308d4c0fe040dd9d02783df9a4ae8a","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/62495a74fba06b9273b2de7f","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":43,"height":32,"frame_count":1,"size":1680,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":43,"height":32,"frame_count":1,"size":1410,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":86,"height":64,"frame_count":1,"size":3548,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":86,"height":64,"frame_count":1,"size":3792,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":129,"height":96,"frame_count":1,"size":5782,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":129,"height":96,"frame_count":1,"size":6826,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":172,"height":128,"frame_count":1,"size":8012,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":172,"height":128,"frame_count":1,"size":10388,"format":"WEBP"}]}}},{"id":"610ebd56900cbd77c695858a","name":"peepoKing","flags":0,"timestamp":1678544770123,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"610ebd56900cbd77c695858a","name":"peepoKing","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60f244dfc07d1ac193c6314e","username":"kushala_0001","display_name":"Kushala_0001","avatar_url":"//cdn.7tv.app/user/60f244dfc07d1ac193c6314e/av_640be52567f0badff748097e/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/610ebd56900cbd77c695858a","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1808,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1342,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":4241,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":3782,"format":"WEBP"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":7002,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":7244,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":10980,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":10876,"format":"WEBP"}]}}},{"id":"61957f6e6467596b1d624a02","name":"peepoQueen","flags":0,"timestamp":1678553855456,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"61957f6e6467596b1d624a02","name":"peepoQueen","flags":0,"tags":["peepoqueen"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"618bd6bad34608492cc29b9b","username":"tomato_es","display_name":"tomato_es","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/fb464f3e372e54f2-profile_image-70x70.jpeg","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61957f6e6467596b1d624a02","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1483,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1218,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":3073,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2990,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4829,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":5306,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":6330,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":7760,"format":"WEBP"}]}}},{"id":"60ae3028b2ecb01505cf58f1","name":"gachiPRIDE","flags":0,"timestamp":1678634148451,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60ae3028b2ecb01505cf58f1","name":"gachiPRIDE","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60635b50452cea4685f26b34","username":"hecrzy","display_name":"heCrzy","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/583dd5ac-2fe8-4ead-a20d-e10770118c5f-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60ae3028b2ecb01505cf58f1","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":90,"size":40875,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":90,"size":98430,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":90,"size":129956,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":90,"size":239976,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":90,"size":232304,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":90,"size":416644,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":90,"size":377466,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":90,"size":455352,"format":"WEBP"}]}}},{"id":"60ae39d5b2ecb0150511882d","name":"Painsge","flags":0,"timestamp":1678634148783,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60ae39d5b2ecb0150511882d","name":"Painsge","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60635b50452cea4685f26b34","username":"hecrzy","display_name":"heCrzy","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/583dd5ac-2fe8-4ead-a20d-e10770118c5f-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60ae39d5b2ecb0150511882d","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1270,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":922,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2321,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2274,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3545,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":3858,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4733,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":4942,"format":"WEBP"}]}}},{"id":"61da356b600369a98b38a1f0","name":"nymnNerd","flags":0,"timestamp":1678634149106,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"61da356b600369a98b38a1f0","name":"nymnNerd","flags":0,"tags":["nymn"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae485c0e35477634456d8b","username":"imdor_","display_name":"ImDor_","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/71a13ff9-ac92-432e-9577-1f5d3809a5dc-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61da356b600369a98b38a1f0","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1044,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1290,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2596,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2517,"format":"AVIF"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4068,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4562,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5774,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":7280,"format":"WEBP"}]}}},{"id":"60b0f5b2824db02a4103592d","name":"FEELSWAYTOOGOOD","flags":0,"timestamp":1678634149429,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60b0f5b2824db02a4103592d","name":"FEELSWAYTOOGOOD","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae4a8b9986a00349629473","username":"riier","display_name":"riIer","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/50fb833d-3f45-4675-903e-129848483bd1-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60b0f5b2824db02a4103592d","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":80,"size":116810,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":80,"size":106800,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":80,"size":332281,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":80,"size":293560,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":80,"size":568421,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":80,"size":547814,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":80,"size":867670,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":80,"size":746256,"format":"WEBP"}]}}},{"id":"633b03457858907ca876a17f","name":"henrE","flags":0,"timestamp":1678634149760,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"633b03457858907ca876a17f","name":"henrE","flags":0,"tags":["lit","bussin","swag","awesome","nymn"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"633b02fb7858907ca876a17b","username":"videogamesfan1234","display_name":"videogamesfan1234","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/3639c0c0-084c-4071-b4ed-30ed58d2dee2-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/633b03457858907ca876a17f","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":902,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1536,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":4614,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1357,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":8698,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":1839,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":2255,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":13620,"format":"WEBP"}]}}},{"id":"60ae2887b2ecb01505a44861","name":"HONEYDETECTED","flags":0,"timestamp":1678718349439,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60ae2887b2ecb01505a44861","name":"HONEYDETECTED","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"6090833b38a7d4c606e25413","username":"oskar5oskar","display_name":"oskar5oskar","avatar_url":"//cdn.7tv.app/pp/6090833b38a7d4c606e25413/dcaf75045ff94869b50ff27da406c3de","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60ae2887b2ecb01505a44861","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":27,"height":32,"frame_count":1,"size":1656,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":27,"height":32,"frame_count":1,"size":1334,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":54,"height":64,"frame_count":1,"size":3253,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":54,"height":64,"frame_count":1,"size":3304,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":81,"height":96,"frame_count":1,"size":4997,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":81,"height":96,"frame_count":1,"size":5556,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":108,"height":128,"frame_count":1,"size":6736,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":108,"height":128,"frame_count":1,"size":7470,"format":"WEBP"}]}}},{"id":"64106f45d85152e6648cacd9","name":"Barons","flags":0,"timestamp":1678798669784,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"64106f45d85152e6648cacd9","name":"Barons","flags":0,"tags":["kingofthecastle","kotc"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae3e3eb2ecb01505346ae9","username":"fawcan","display_name":"Fawcan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38051bdb-83c6-4716-95e0-731462e02b45-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64106f45d85152e6648cacd9","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":5,"size":4964,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":5,"size":3958,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":5,"size":9400,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":5,"size":8280,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":5,"size":14883,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":5,"size":13082,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":5,"size":23926,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":5,"size":17600,"format":"WEBP"}]}}},{"id":"64106ea9a1db99f27d318959","name":"Patricians","flags":0,"timestamp":1678798670658,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"64106ea9a1db99f27d318959","name":"Patricians","flags":0,"tags":["kingofthecastle","kotc"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae3e3eb2ecb01505346ae9","username":"fawcan","display_name":"Fawcan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38051bdb-83c6-4716-95e0-731462e02b45-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64106ea9a1db99f27d318959","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":5,"size":4927,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":5,"size":3832,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":5,"size":7998,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":5,"size":9045,"format":"AVIF"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":5,"size":14422,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":5,"size":12716,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":5,"size":23013,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":5,"size":17108,"format":"WEBP"}]}}},{"id":"64106eb764170c9686329cd2","name":"Counts","flags":0,"timestamp":1678798671636,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"64106eb764170c9686329cd2","name":"Counts","flags":0,"tags":["kingofthecastle","kotc"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae3e3eb2ecb01505346ae9","username":"fawcan","display_name":"Fawcan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38051bdb-83c6-4716-95e0-731462e02b45-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64106eb764170c9686329cd2","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":5,"size":4806,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":5,"size":3840,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":5,"size":9029,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":5,"size":8016,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":5,"size":14417,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":5,"size":12772,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":5,"size":22958,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":5,"size":17186,"format":"WEBP"}]}}},{"id":"64106ee61db1890a6196de57","name":"Chiefs","flags":0,"timestamp":1678798672578,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"64106ee61db1890a6196de57","name":"Chiefs","flags":0,"tags":["kingofthecastle","kotc"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae3e3eb2ecb01505346ae9","username":"fawcan","display_name":"Fawcan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38051bdb-83c6-4716-95e0-731462e02b45-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64106ee61db1890a6196de57","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":5,"size":5060,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":5,"size":3918,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":5,"size":9512,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":5,"size":8264,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":5,"size":15100,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":5,"size":13070,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":5,"size":24316,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":5,"size":17622,"format":"WEBP"}]}}},{"id":"64106eb0dba31716800e6455","name":"Grandees","flags":0,"timestamp":1678798673622,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"64106eb0dba31716800e6455","name":"Grandees","flags":0,"tags":["kingofthecastle","kotc"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae3e3eb2ecb01505346ae9","username":"fawcan","display_name":"Fawcan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38051bdb-83c6-4716-95e0-731462e02b45-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64106eb0dba31716800e6455","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":5,"size":5139,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":5,"size":3974,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":5,"size":9729,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":5,"size":8420,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":5,"size":15483,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":5,"size":13424,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":5,"size":24804,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":5,"size":17752,"format":"WEBP"}]}}},{"id":"60ae9acf3c27a8b79c9be9bf","name":"BigHardo","flags":0,"timestamp":1678875695082,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"60ae9acf3c27a8b79c9be9bf","name":"BigHardo","flags":0,"tags":["widehardo","hardo","wide","big","trihard","trihex"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae759bdf5735e04acb69d9","username":"hotbear1110","display_name":"HotBear1110","avatar_url":"//cdn.7tv.app/pp/60ae759bdf5735e04acb69d9/80e2b49378c14dc6914fde8cb72fa673","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60ae9acf3c27a8b79c9be9bf","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":96,"height":32,"frame_count":1,"size":2024,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":96,"height":32,"frame_count":1,"size":1910,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":192,"height":64,"frame_count":1,"size":3705,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":192,"height":64,"frame_count":1,"size":4492,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":288,"height":96,"frame_count":1,"size":5170,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":288,"height":96,"frame_count":1,"size":7012,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":384,"height":128,"frame_count":1,"size":6740,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":384,"height":128,"frame_count":1,"size":9384,"format":"WEBP"}]}}},{"id":"6362de54b4cd96c8459b4dfa","name":"SwagOff","flags":0,"timestamp":1678884462066,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"6362de54b4cd96c8459b4dfa","name":"SwagOff","flags":0,"tags":["emoney","erobb221"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60ae3e98b2ecb0150535c6b7","username":"gempir","display_name":"gempir","avatar_url":"//cdn.7tv.app/pp/60ae3e98b2ecb0150535c6b7/4aa1786cec024098be20d7b0683bae72","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6362de54b4cd96c8459b4dfa","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":34,"height":32,"frame_count":1,"size":1045,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":34,"height":32,"frame_count":1,"size":1816,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":68,"height":64,"frame_count":1,"size":1910,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":68,"height":64,"frame_count":1,"size":5510,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":102,"height":96,"frame_count":1,"size":2921,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":102,"height":96,"frame_count":1,"size":10950,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":136,"height":128,"frame_count":1,"size":4045,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":136,"height":128,"frame_count":1,"size":18066,"format":"WEBP"}]}}},{"id":"63995b353a424911dcde9df7","name":"catEat","flags":0,"timestamp":1678893348492,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"63995b353a424911dcde9df7","name":"catEat","flags":0,"tags":["eating","kitty","psp1g","silly","cat"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"624476c4b22b98fd69bad800","username":"rementin","display_name":"Rementin","avatar_url":"//cdn.7tv.app/user/624476c4b22b98fd69bad800/av_639613dac5d3143d0fa9317a/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63995b353a424911dcde9df7","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":14,"size":6629,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":14,"size":7468,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":14,"size":12571,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":14,"size":13466,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":14,"size":19480,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":14,"size":20158,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":14,"size":25952,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":14,"size":26610,"format":"WEBP"}]}}},{"id":"60ae2e3db2ecb01505c6f69d","name":"ViolinTime","flags":0,"timestamp":1678893348864,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60ae2e3db2ecb01505c6f69d","name":"ViolinTime","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60635b50452cea4685f26b34","username":"hecrzy","display_name":"heCrzy","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/583dd5ac-2fe8-4ead-a20d-e10770118c5f-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60ae2e3db2ecb01505c6f69d","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":25,"size":7519,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":25,"size":25176,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":25,"size":12324,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":25,"size":45762,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":25,"size":20771,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":25,"size":78378,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":25,"size":25504,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":25,"size":83278,"format":"WEBP"}]}}},{"id":"61c1b7ba857d3a3442b21ce0","name":"GivenUp","flags":0,"timestamp":1678893349210,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"61c1b7ba857d3a3442b21ce0","name":"GivenUp","flags":0,"tags":["despair","troll","sadge","boobs","sexy","booba"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"61056ca46ec21e98cdd05532","username":"takideezy","display_name":"TAKIDEEZY","avatar_url":"//cdn.7tv.app/pp/61056ca46ec21e98cdd05532/b5a16c68dea64472a042214a3b6811f1","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61c1b7ba857d3a3442b21ce0","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":36,"height":32,"frame_count":1,"size":985,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":36,"height":32,"frame_count":1,"size":752,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":72,"height":64,"frame_count":1,"size":1870,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":72,"height":64,"frame_count":1,"size":1926,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":108,"height":96,"frame_count":1,"size":2893,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":108,"height":96,"frame_count":1,"size":3240,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":144,"height":128,"frame_count":1,"size":4014,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":144,"height":128,"frame_count":1,"size":4758,"format":"WEBP"}]}}},{"id":"63583ee508a02a66a37dcd9d","name":"peepoRiot","flags":0,"timestamp":1678893349586,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"63583ee508a02a66a37dcd9d","name":"peepoRiot","flags":0,"tags":["angry","rage","showcock","mad","madge"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60aff6d6fd9839f62de3d3c0","username":"tanoshi13","display_name":"Tanoshi13","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63583ee508a02a66a37dcd9d","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":96,"height":32,"frame_count":21,"size":27038,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":96,"height":32,"frame_count":21,"size":43058,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":192,"height":64,"frame_count":21,"size":80369,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":192,"height":64,"frame_count":21,"size":120762,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":288,"height":96,"frame_count":21,"size":142101,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":288,"height":96,"frame_count":21,"size":225374,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":384,"height":128,"frame_count":21,"size":213679,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":384,"height":128,"frame_count":21,"size":368994,"format":"WEBP"}]}}},{"id":"603cb5a2c20d020014423c65","name":"KKomrade","flags":0,"timestamp":1678893349938,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"603cb5a2c20d020014423c65","name":"KKomrade","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"603cb1c696832ffa78cc3bc2","username":"clyvere","display_name":"clyverE","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/3ff40972-0188-4cfc-adbf-8db119d7cf2a-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/603cb5a2c20d020014423c65","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1521,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1182,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":3470,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":3002,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":5457,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":5456,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":8433,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":8088,"format":"WEBP"}]}}},{"id":"60b10835f12983cd1da8eae3","name":"POGCRAZY","flags":0,"timestamp":1678958782918,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60b10835f12983cd1da8eae3","name":"POGCRAZY","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60b3bb1039cff46b8b65d389","username":"nakomaru","display_name":"nakomaru","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/49b33784-b455-4327-bb58-5be4976152b2-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60b10835f12983cd1da8eae3","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":2,"size":1440,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":2,"size":2622,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":2,"size":2964,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":2,"size":3531,"format":"AVIF"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":2,"size":4537,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":2,"size":4498,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":2,"size":5590,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":2,"size":4736,"format":"WEBP"}]}}},{"id":"60fb3c105b7deb3de0233db8","name":"peepoSweden","flags":0,"timestamp":1679062362260,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60fb3c105b7deb3de0233db8","name":"peepoSweden","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae79fd2d2ea639d67cbe33","username":"mr0lle","display_name":"Mr0lle","avatar_url":"//cdn.7tv.app/user/60ae79fd2d2ea639d67cbe33/av_6482018e80691d3bb17755cc/3x.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60fb3c105b7deb3de0233db8","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":4,"size":4436,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":4,"size":5595,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":4,"size":9754,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":4,"size":10224,"format":"AVIF"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":4,"size":15599,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":4,"size":15812,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":4,"size":21277,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":4,"size":16104,"format":"WEBP"}]}}},{"id":"60439ac01d4963000d9dae44","name":"WEEBSDETECTED","flags":0,"timestamp":1679152562394,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60439ac01d4963000d9dae44","name":"WEEBSDETECTED","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"6042ac8796832ffa7887f10d","username":"jirabruar","display_name":"JirabruaR","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/c0004f23-ca18-4f81-82d8-c647b202096a-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60439ac01d4963000d9dae44","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":29,"height":32,"frame_count":9,"size":5810,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":29,"height":32,"frame_count":9,"size":6954,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":58,"height":64,"frame_count":9,"size":12032,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":58,"height":64,"frame_count":9,"size":15504,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":87,"height":96,"frame_count":9,"size":18804,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":87,"height":96,"frame_count":9,"size":23846,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":116,"height":128,"frame_count":9,"size":32944,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":116,"height":128,"frame_count":9,"size":37134,"format":"WEBP"}]}}},{"id":"60aab50dd9ec9c6fc5eef7a0","name":"SOYING","flags":0,"timestamp":1679152562762,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60aab50dd9ec9c6fc5eef7a0","name":"SOYING","flags":0,"lifecycle":3,"state":["PERSONAL","LISTED"],"listed":true,"animated":true,"owner":{"id":"6053853cb4d31e459fdaa2dc","username":"laden","display_name":"Laden","avatar_url":"//cdn.7tv.app/pp/6053853cb4d31e459fdaa2dc/a94c67d7736940feb543e42024b740ef","style":{"color":849892095},"roles":["60724f65e93d828bf8858789","6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60aab50dd9ec9c6fc5eef7a0","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":150,"size":46889,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":150,"size":118366,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":150,"size":109738,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":150,"size":248834,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":150,"size":193248,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":150,"size":384494,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":150,"size":291964,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":150,"size":359740,"format":"WEBP"}]}}},{"id":"613ede4e7b14fdf700b8b3d2","name":"ElNoSabe","flags":0,"timestamp":1679152563147,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"613ede4e7b14fdf700b8b3d2","name":"ElNoSabe","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae659b86fc40d4886293d2","username":"vicesmile_6407","display_name":"vicesmile_6407","avatar_url":"//cdn.7tv.app/pp/60ae659b86fc40d4886293d2/407a4925ae9c40f8bffe20dbb2020276","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/613ede4e7b14fdf700b8b3d2","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":4,"size":5619,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":4,"size":4516,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":4,"size":11922,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":4,"size":11957,"format":"AVIF"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":4,"size":19061,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":4,"size":20656,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":4,"size":27490,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":4,"size":25512,"format":"WEBP"}]}}},{"id":"61eff87af933d586cdda6b2d","name":"apolloPain","flags":0,"timestamp":1679152564084,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"61eff87af933d586cdda6b2d","name":"apolloPain","flags":0,"tags":["pain","apollo","yabbe","nymn","sad"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60b6746f64faf92496330d3c","username":"littlescampi","display_name":"LittleScampi","avatar_url":"//cdn.7tv.app/pp/60b6746f64faf92496330d3c/a1ff043123944a119d69cf9a7062a442","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61eff87af933d586cdda6b2d","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":853,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":586,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1472,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":1534,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":2056,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":2556,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":3133,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":4026,"format":"WEBP"}]}}},{"id":"6063c87af4dc10001426b915","name":"LULW","flags":0,"timestamp":1679152766674,"actor_id":"60ae8fc0ea50f43c9e3ae255","data":{"id":"6063c87af4dc10001426b915","name":"LULW","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"000000000000000000000000","username":"","display_name":"","style":{}},"host":{"url":"//cdn.7tv.app/emote/6063c87af4dc10001426b915","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":28,"height":32,"frame_count":1,"size":1071,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":28,"height":32,"frame_count":1,"size":762,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":56,"height":64,"frame_count":1,"size":1920,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":56,"height":64,"frame_count":1,"size":1797,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":84,"height":96,"frame_count":1,"size":2964,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":84,"height":96,"frame_count":1,"size":2486,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":112,"height":128,"frame_count":1,"size":3008,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":112,"height":128,"frame_count":1,"size":3978,"format":"WEBP"}]}}},{"id":"63b41fe98dbb71dedfddd46a","name":"AlienFeel","flags":0,"timestamp":1679247317678,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"63b41fe98dbb71dedfddd46a","name":"AlienFeel","flags":0,"tags":["alienlag","alienpls"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60b39e943e203cc169dfc106","username":"nerixyz","display_name":"nerixyz","avatar_url":"//cdn.7tv.app/user/60b39e943e203cc169dfc106/av_64c262f0577886a8184c8a55/3x.webp","style":{"color":401323775},"roles":["60b3f1ea886e63449c5263b1","6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63b41fe98dbb71dedfddd46a","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":374,"size":110700,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":374,"size":204432,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":374,"size":243079,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":374,"size":399628,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":374,"size":411672,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":374,"size":580264,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":374,"size":549883,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":374,"size":749212,"format":"WEBP"}]}}},{"id":"60be95e560f498310bd13d2c","name":"donkAim","flags":0,"timestamp":1679322828499,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60be95e560f498310bd13d2c","name":"donkAim","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60be9049412138e6fa758c6d","username":"b0de","display_name":"b0de","avatar_url":"//cdn.7tv.app/user/60be9049412138e6fa758c6d/av_657d636683833b99670d41a1/3x_static.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60be95e560f498310bd13d2c","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":39,"size":18883,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":39,"size":32340,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":39,"size":45634,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":39,"size":67204,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":39,"size":73812,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":39,"size":109344,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":39,"size":103568,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":39,"size":122614,"format":"WEBP"}]}}},{"id":"6418a27761181dac3d98386c","name":"unbased0","flags":1,"timestamp":1679337473513,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"6418a27761181dac3d98386c","name":"unbased0","flags":256,"tags":["based","sunglasses"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60532ec2b4d31e459f7293dc","username":"marrryanx","display_name":"Marrryanx","avatar_url":"//cdn.7tv.app/user/60532ec2b4d31e459f7293dc/av_6570dc7f834e0a119031a679/3x_static.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6418a27761181dac3d98386c","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":93,"size":22856,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":93,"size":37632,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":93,"size":43720,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":93,"size":55312,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":93,"size":75924,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":93,"size":87364,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":93,"size":99848,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":93,"size":94246,"format":"WEBP"}]}}},{"id":"6418c4d336447ca8cfe1c1ad","name":"DiabloIV","flags":0,"timestamp":1679344869038,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"6418c4d336447ca8cfe1c1ad","name":"DiabloIV","flags":0,"tags":["diablo","circle","div","diabloiv","diablo4"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae3e3eb2ecb01505346ae9","username":"fawcan","display_name":"Fawcan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38051bdb-83c6-4716-95e0-731462e02b45-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6418c4d336447ca8cfe1c1ad","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":15,"size":7647,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":15,"size":13040,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":15,"size":17801,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":15,"size":30096,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":15,"size":32635,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":15,"size":50892,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":15,"size":49005,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":15,"size":71356,"format":"WEBP"}]}}},{"id":"63ee331a612066cf56f72eda","name":"Aplonk","flags":0,"timestamp":1679411226483,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"63ee331a612066cf56f72eda","name":"Aplonk","flags":0,"tags":["plink","plonk","apollo","nymn","cat"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae409daee2aa55383ebb4b","username":"tolatos","display_name":"tolatos","avatar_url":"//cdn.7tv.app/user/60ae409daee2aa55383ebb4b/av_657b75e8b0d945ef35823739/3x.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63ee331a612066cf56f72eda","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":80,"height":32,"frame_count":119,"size":26798,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":80,"height":32,"frame_count":119,"size":73464,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":160,"height":64,"frame_count":119,"size":59886,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":160,"height":64,"frame_count":119,"size":164570,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":240,"height":96,"frame_count":119,"size":103277,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":240,"height":96,"frame_count":119,"size":265288,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":320,"height":128,"frame_count":119,"size":456597,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":320,"height":128,"frame_count":119,"size":453864,"format":"WEBP"}]}}},{"id":"6287979263c9f9d7aaa39c73","name":"TriAmigos","flags":0,"timestamp":1679411762865,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"6287979263c9f9d7aaa39c73","name":"TriAmigos","flags":0,"tags":["trihex","trihard","hombre","hermano","amigos"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"611bc87d5f3378ee33500b98","username":"sotiris_ael","display_name":"Sotiris_Ael","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38a2b8d2-5fe5-47ab-9726-4c79e568d6fe-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6287979263c9f9d7aaa39c73","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":33,"height":32,"frame_count":1,"size":1606,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":33,"height":32,"frame_count":1,"size":1162,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":66,"height":64,"frame_count":1,"size":3542,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":66,"height":64,"frame_count":1,"size":3106,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":99,"height":96,"frame_count":1,"size":5905,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":99,"height":96,"frame_count":1,"size":5576,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":132,"height":128,"frame_count":1,"size":9334,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":132,"height":128,"frame_count":1,"size":8968,"format":"WEBP"}]}}},{"id":"61fe77cf69acfa0715e1f7ef","name":"Chatge","flags":0,"timestamp":1679411763427,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"61fe77cf69acfa0715e1f7ef","name":"Chatge","flags":0,"tags":["chat","pepege","okayge","pepe","nymn"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60b6746f64faf92496330d3c","username":"littlescampi","display_name":"LittleScampi","avatar_url":"//cdn.7tv.app/pp/60b6746f64faf92496330d3c/a1ff043123944a119d69cf9a7062a442","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61fe77cf69acfa0715e1f7ef","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":83,"height":32,"frame_count":1,"size":1571,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":83,"height":32,"frame_count":1,"size":1546,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":166,"height":64,"frame_count":1,"size":3636,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":166,"height":64,"frame_count":1,"size":3273,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":249,"height":96,"frame_count":1,"size":5756,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":249,"height":96,"frame_count":1,"size":5062,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":332,"height":128,"frame_count":1,"size":6905,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":332,"height":128,"frame_count":1,"size":8238,"format":"WEBP"}]}}},{"id":"60b2654203982475b83b1346","name":"1984","flags":0,"timestamp":1679411763841,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60b2654203982475b83b1346","name":"1984","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60b0f5b9726e10b664c44bb5","username":"treoluas","display_name":"treoluas","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/0269f924-627e-46ff-8a23-7c599103d8b7-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60b2654203982475b83b1346","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":43,"height":32,"frame_count":30,"size":10626,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":43,"height":32,"frame_count":30,"size":27930,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":86,"height":64,"frame_count":30,"size":23861,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":86,"height":64,"frame_count":30,"size":67176,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":129,"height":96,"frame_count":30,"size":44184,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":129,"height":96,"frame_count":30,"size":110942,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":172,"height":128,"frame_count":30,"size":68653,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":172,"height":128,"frame_count":30,"size":121988,"format":"WEBP"}]}}},{"id":"61fd55b2690425de3c63eae0","name":"BrickTime","flags":0,"timestamp":1679411764311,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"61fd55b2690425de3c63eae0","name":"BrickTime","flags":0,"tags":["nymn","fuzer","brick","tomatotime","boo"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60b6746f64faf92496330d3c","username":"littlescampi","display_name":"LittleScampi","avatar_url":"//cdn.7tv.app/pp/60b6746f64faf92496330d3c/a1ff043123944a119d69cf9a7062a442","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61fd55b2690425de3c63eae0","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":27,"size":11196,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":27,"size":16600,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":27,"size":19147,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":27,"size":34460,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":27,"size":29876,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":27,"size":54710,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":27,"size":39191,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":27,"size":59616,"format":"WEBP"}]}}},{"id":"61474a7845d00846a86eb11e","name":"Turteg","flags":0,"timestamp":1679413283767,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"61474a7845d00846a86eb11e","name":"Turteg","flags":0,"tags":["okayeg","turtle"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60f3577ac07d1ac1939168a5","username":"svenin_","display_name":"svenin_","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/4096983f-68f5-48b4-ab5c-6c5e084f3b03-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61474a7845d00846a86eb11e","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1444,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1018,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2735,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2490,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4243,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4366,"format":"WEBP"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":6456,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5916,"format":"AVIF"}]}}},{"id":"640676b06feb26d14dc341a9","name":"FIRE","flags":1,"timestamp":1679429894599,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"640676b06feb26d14dc341a9","name":"onFire","flags":256,"lifecycle":3,"state":["NO_PERSONAL","LISTED"],"listed":true,"animated":true,"owner":{"id":"60f9b978c07d1ac1939608e2","username":"ja77man","display_name":"Ja77Man","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/7bbef0bf-7cc2-4ebe-9652-fa08e08ef2d0-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/640676b06feb26d14dc341a9","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":32,"size":19353,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":32,"size":18984,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":32,"size":24776,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":32,"size":26358,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":32,"size":42718,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":32,"size":43962,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":32,"size":40027,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":32,"size":38694,"format":"WEBP"}]}}},{"id":"614098ba0969108b6718d263","name":"pepeL","flags":0,"timestamp":1679592187659,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"614098ba0969108b6718d263","name":"pepeL","flags":0,"tags":["pepe","pepel"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae3e3eb2ecb01505346ae9","username":"fawcan","display_name":"Fawcan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38051bdb-83c6-4716-95e0-731462e02b45-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/614098ba0969108b6718d263","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":528,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":813,"format":"AVIF"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1073,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":816,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":1384,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":1082,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":1585,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":1298,"format":"WEBP"}]}}},{"id":"641c8e06b180e3592d9a0d6c","name":"TimeToLie","flags":0,"timestamp":1679593064081,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"641c8e06b180e3592d9a0d6c","name":"TimeToLie","flags":0,"tags":["lie","lying","ditch","pinocchio","nymn","nime"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60b6746f64faf92496330d3c","username":"littlescampi","display_name":"LittleScampi","avatar_url":"//cdn.7tv.app/pp/60b6746f64faf92496330d3c/a1ff043123944a119d69cf9a7062a442","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/641c8e06b180e3592d9a0d6c","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":55,"height":32,"frame_count":30,"size":5005,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":55,"height":32,"frame_count":30,"size":6694,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":110,"height":64,"frame_count":30,"size":8072,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":110,"height":64,"frame_count":30,"size":29216,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":165,"height":96,"frame_count":30,"size":10908,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":165,"height":96,"frame_count":30,"size":48348,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":220,"height":128,"frame_count":30,"size":17787,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":220,"height":128,"frame_count":30,"size":70912,"format":"WEBP"}]}}},{"id":"60b41ac425b841c0d879fc99","name":"SmugTime","flags":0,"timestamp":1679661660720,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60b41ac425b841c0d879fc99","name":"SmugTime","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"608bd85b99dcd148faf02644","username":"tehargi_","display_name":"tehargi_","avatar_url":"//static-cdn.jtvnw.net/user-default-pictures-uv/294c98b5-e34d-42cd-a8f0-140b72fba9b0-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60b41ac425b841c0d879fc99","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":106,"size":18601,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":106,"size":75518,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":106,"size":32618,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":106,"size":151030,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":106,"size":57292,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":106,"size":240336,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":106,"size":88695,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":106,"size":252196,"format":"WEBP"}]}}},{"id":"63ab1545c29011fd71be1cb5","name":"peepoPag","flags":0,"timestamp":1679663442636,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"63ab1545c29011fd71be1cb5","name":"peepoPag","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae409daee2aa55383ebb4b","username":"tolatos","display_name":"tolatos","avatar_url":"//cdn.7tv.app/user/60ae409daee2aa55383ebb4b/av_657b75e8b0d945ef35823739/3x.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63ab1545c29011fd71be1cb5","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1215,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1722,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1857,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":4822,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":2700,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":8828,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":3355,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":13146,"format":"WEBP"}]}}},{"id":"613150f0a77c17adb4479f1c","name":"SALAMIhand","flags":1,"timestamp":1679670962319,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"613150f0a77c17adb4479f1c","name":"SALAMIhand","flags":256,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60b561cc04283ab952bfd4e0","username":"on_a_stack","display_name":"On_a_stack","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/1ed36b65-71c8-4eb2-a6a7-83ad2bb7566a-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/613150f0a77c17adb4479f1c","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":4,"size":2863,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":4,"size":1944,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":4,"size":4000,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":4,"size":3484,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":4,"size":5652,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":4,"size":4984,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":4,"size":6773,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":4,"size":4960,"format":"WEBP"}]}}},{"id":"62dc3e4c72884f6d6e8b52f2","name":"PotFriendWave","flags":0,"timestamp":1679670962664,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"62dc3e4c72884f6d6e8b52f2","name":"PotFriendWave","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"62111f1c21b4d5de14470046","username":"bassasas","display_name":"bassasas","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/62dc3e4c72884f6d6e8b52f2","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":2,"size":2936,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":2,"size":1976,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":2,"size":4363,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":2,"size":4646,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":2,"size":5813,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":2,"size":7118,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":2,"size":7072,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":2,"size":9664,"format":"WEBP"}]}}},{"id":"60ae759df7c927fad171a104","name":"ppCrazy","flags":0,"timestamp":1679670962993,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60ae759df7c927fad171a104","name":"ppCrazy","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae461c9986a003493358f3","username":"gaib_","display_name":"gaib_","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/ce7e4c60-a008-4e7e-8972-e905ffc54e71-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60ae759df7c927fad171a104","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":34,"size":6621,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":34,"size":10318,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":34,"size":8721,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":34,"size":17040,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":34,"size":12673,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":34,"size":24126,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":34,"size":14453,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":34,"size":24896,"format":"WEBP"}]}}},{"id":"60aeaf8b98f4291470c8e64b","name":"COCKA","flags":0,"timestamp":1679752063073,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60aeaf8b98f4291470c8e64b","name":"COCKA","flags":0,"lifecycle":3,"state":["NO_PERSONAL","LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae9207ac03cad607d3980f","username":"onkel_jodok","display_name":"onkel_jodok","avatar_url":"//cdn.7tv.app/pp/60ae9207ac03cad607d3980f/a7bb23d0f0ad46bb8105768c642ce6a1","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60aeaf8b98f4291470c8e64b","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":8,"size":5766,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":8,"size":8046,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":8,"size":10715,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":8,"size":16452,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":8,"size":16642,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":8,"size":25640,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":8,"size":22394,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":8,"size":28178,"format":"WEBP"}]}}},{"id":"63ee3374ae545fa8d4208337","name":"WEIRDO","flags":0,"timestamp":1679785642323,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"63ee3374ae545fa8d4208337","name":"MyHonestReaction","flags":0,"tags":["weird","okay","cat","apollo","nymn"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae409daee2aa55383ebb4b","username":"tolatos","display_name":"tolatos","avatar_url":"//cdn.7tv.app/user/60ae409daee2aa55383ebb4b/av_657b75e8b0d945ef35823739/3x.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63ee3374ae545fa8d4208337","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":209,"size":27739,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":209,"size":75952,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":209,"size":55019,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":209,"size":161924,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":209,"size":93338,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":209,"size":253128,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":209,"size":240555,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":209,"size":410700,"format":"WEBP"}]}}},{"id":"633dd7eba855bd5c6ce87a37","name":"EDM","flags":0,"timestamp":1679790561792,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"633dd7eba855bd5c6ce87a37","name":"EDM","flags":0,"tags":["dance","bass","xar2edm","party","jam","rave"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60f5e290e57bec021618c4a4","username":"ansonx10","display_name":"AnsonX10","avatar_url":"//cdn.7tv.app/user/60f5e290e57bec021618c4a4/av_63617cc39018da6429bc0298/3x_static.webp","style":{"color":401323775},"roles":["60b3f1ea886e63449c5263b1","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/633dd7eba855bd5c6ce87a37","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":200,"size":25028,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":200,"size":39732,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":200,"size":35404,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":200,"size":54108,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":200,"size":62056,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":200,"size":72538,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":200,"size":61875,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":200,"size":69646,"format":"WEBP"}]}}},{"id":"614b1f0e5765a48b24d95906","name":"SoyJam","flags":0,"timestamp":1679867551976,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"614b1f0e5765a48b24d95906","name":"SoyJam","flags":0,"tags":["nymn","soying","rave","pls","forsen"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae81ff0bf2ee96aea05247","username":"snortexx","display_name":"snortexx","avatar_url":"//cdn.7tv.app/pp/60ae81ff0bf2ee96aea05247/183b9b6ab7624a53966fb782ec0963e0","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/614b1f0e5765a48b24d95906","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":151,"size":72986,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":151,"size":120624,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":151,"size":163958,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":151,"size":259012,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":151,"size":281242,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":151,"size":420734,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":151,"size":386303,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":151,"size":404650,"format":"WEBP"}]}}},{"id":"6134f99b434bb87c45d91b58","name":"emoteApprove","flags":1,"timestamp":1679930162780,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"6134f99b434bb87c45d91b58","name":"emoteApprove","flags":256,"tags":["billy","gachiapprove","billyapprove","zerowidth","monkass","batmanschest"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae21c0aee2aa55388ba741","username":"farbrorbarbro","display_name":"FarbrorBarbro","avatar_url":"//cdn.7tv.app/pp/60ae21c0aee2aa55388ba741/2aa45e11e4474bf8a04c74fe9157bd53","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6134f99b434bb87c45d91b58","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":48,"size":16797,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":48,"size":30122,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":48,"size":60244,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":48,"size":33656,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":48,"size":98182,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":48,"size":58079,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":48,"size":82686,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":48,"size":100660,"format":"WEBP"}]}}},{"id":"63c326a919eab0d59a02bb9c","name":"nymnEmo","flags":0,"timestamp":1679930163301,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"63c326a919eab0d59a02bb9c","name":"nymnEmo","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60e8b3593c5b87437a8cd7a4","username":"cybo_","display_name":"Cybo_","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/0c870390-5220-4704-8c6e-57cfb9120695-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63c326a919eab0d59a02bb9c","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1254,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":2086,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2551,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":6824,"format":"WEBP"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":12340,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4074,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":16956,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5607,"format":"AVIF"}]}}},{"id":"60aee9d5361b0164e60d02c2","name":"WICKED","flags":0,"timestamp":1679930163788,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60aee9d5361b0164e60d02c2","name":"WICKED","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae7682f7c927fad17bc447","username":"pandabene_","display_name":"Pandabene_","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/e87ae0b4-fc7c-406d-8e02-67922b54fa15-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60aee9d5361b0164e60d02c2","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":36,"height":32,"frame_count":24,"size":7396,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":36,"height":32,"frame_count":24,"size":25554,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":72,"height":64,"frame_count":24,"size":18899,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":72,"height":64,"frame_count":24,"size":56806,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":108,"height":96,"frame_count":24,"size":32920,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":108,"height":96,"frame_count":24,"size":90470,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":144,"height":128,"frame_count":24,"size":52163,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":144,"height":128,"frame_count":24,"size":112870,"format":"WEBP"}]}}},{"id":"60afe5c26fdef7f55180c7b1","name":"POGGERS","flags":0,"timestamp":1680001598113,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60afe5c26fdef7f55180c7b1","name":"POGGERS","flags":0,"tags":["verypog","shroud","poggers"],"lifecycle":3,"state":["PERSONAL","LISTED"],"listed":true,"animated":true,"owner":{"id":"60afe336e5a5795611c9c482","username":"sleepi3r","display_name":"sleepi3r","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60afe5c26fdef7f55180c7b1","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":142,"size":28380,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":142,"size":120388,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":142,"size":97690,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":142,"size":299126,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":142,"size":181716,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":142,"size":475696,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":142,"size":316728,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":142,"size":373448,"format":"WEBP"}]}}},{"id":"63eeb99e1471b8a35936a509","name":"OOOO","flags":0,"timestamp":1680011852493,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"63eeb99e1471b8a35936a509","name":"OOOO","flags":0,"tags":["rogan","joerogan","pog","ufc"],"lifecycle":3,"state":["PERSONAL","LISTED"],"listed":true,"animated":true,"owner":{"id":"613bdc8feef63902454a4f2d","username":"maxdaxx","display_name":"maxdaxx","avatar_url":"//cdn.7tv.app/user/613bdc8feef63902454a4f2d/av_63b31dc35aa77eca61ec84b8/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63eeb99e1471b8a35936a509","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":45,"height":32,"frame_count":249,"size":96404,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":45,"height":32,"frame_count":249,"size":177032,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":90,"height":64,"frame_count":249,"size":213441,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":90,"height":64,"frame_count":249,"size":348008,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":135,"height":96,"frame_count":249,"size":346695,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":135,"height":96,"frame_count":249,"size":525880,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":180,"height":128,"frame_count":249,"size":545159,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":180,"height":128,"frame_count":249,"size":687234,"format":"WEBP"}]}}},{"id":"6420b1dd268d3cdbcb82433b","name":"emiNymN","flags":0,"timestamp":1680020921999,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"6420b1dd268d3cdbcb82433b","name":"emiNymN","flags":0,"tags":["nymn","eminem","jam","forsen","bald"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"612fad78a77c17adb4478f0f","username":"rexinus1","display_name":"Rexinus1","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/47acff52-7ea8-4d2e-8f40-4c050b1d360c-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6420b1dd268d3cdbcb82433b","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":24,"size":8960,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":21,"size":10868,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":24,"size":19514,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":24,"size":31714,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":24,"size":35576,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":24,"size":49188,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":24,"size":56457,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":24,"size":66736,"format":"WEBP"}]}}},{"id":"60df1f34dd6a810dd4399544","name":"TrollGlad","flags":0,"timestamp":1680022153779,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60df1f34dd6a810dd4399544","name":"TrollGlad","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60af909f12f90fadd689522a","username":"baseddex","display_name":"BasedDex","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/36396175-d036-4354-b264-f8bfd8c0142d-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60df1f34dd6a810dd4399544","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1209,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1032,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2545,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2640,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4325,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4708,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":6331,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":7420,"format":"WEBP"}]}}},{"id":"60afdd3ad2e19045eec9be59","name":"BLAPBLAP","flags":0,"timestamp":1680185850385,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60afdd3ad2e19045eec9be59","name":"BLAPBLAP","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae653c9627f9aff4f5ccd1","username":"xoo_6119","display_name":"xoo_6119","avatar_url":"//cdn.7tv.app/user/60ae653c9627f9aff4f5ccd1/av_63ca0eccdedb49b24383ae5c/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60afdd3ad2e19045eec9be59","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":49,"size":12608,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":49,"size":30148,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":49,"size":26483,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":49,"size":65318,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":49,"size":40972,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":49,"size":110304,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":49,"size":68522,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":49,"size":128098,"format":"WEBP"}]}}},{"id":"6426dab77a943d5766ab5582","name":"RoyaltE","flags":0,"timestamp":1680268654848,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"6426dab77a943d5766ab5582","name":"RoyaltE","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60ae409daee2aa55383ebb4b","username":"tolatos","display_name":"tolatos","avatar_url":"//cdn.7tv.app/user/60ae409daee2aa55383ebb4b/av_657b75e8b0d945ef35823739/3x.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6426dab77a943d5766ab5582","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1333,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":2286,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2770,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":7266,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4047,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":13496,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5460,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":21342,"format":"WEBP"}]}}},{"id":"61a24b98e9684edbbc371602","name":"donkTalk","flags":0,"timestamp":1680362192911,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"61a24b98e9684edbbc371602","name":"donkTalk","flags":0,"tags":["donk","talk","peepotalk","donktalk","nymn"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60b6746f64faf92496330d3c","username":"littlescampi","display_name":"LittleScampi","avatar_url":"//cdn.7tv.app/pp/60b6746f64faf92496330d3c/a1ff043123944a119d69cf9a7062a442","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61a24b98e9684edbbc371602","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":16,"size":5900,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":16,"size":15926,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":16,"size":11222,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":16,"size":32298,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":16,"size":18227,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":16,"size":50868,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":16,"size":25421,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":16,"size":61266,"format":"WEBP"}]}}},{"id":"6397e6789b33c1d4937155e6","name":"ReallyGunPull","flags":0,"timestamp":1680524220660,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"6397e6789b33c1d4937155e6","name":"ReallyGunPull","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"61d0b944c29ed2aed7544871","username":"deividmartini","display_name":"DeividMartini","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/ff790138-083a-49c3-8456-52562077e823-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6397e6789b33c1d4937155e6","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":54,"height":32,"frame_count":86,"size":29514,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":54,"height":32,"frame_count":86,"size":81004,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":108,"height":64,"frame_count":86,"size":79674,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":108,"height":64,"frame_count":86,"size":164944,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":162,"height":96,"frame_count":86,"size":153516,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":162,"height":96,"frame_count":86,"size":258144,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":216,"height":128,"frame_count":86,"size":283645,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":216,"height":128,"frame_count":86,"size":326764,"format":"WEBP"}]}}},{"id":"60df2685dd6a810dd4aad2d3","name":"PPirate","flags":0,"timestamp":1680528534174,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60df2685dd6a810dd4aad2d3","name":"PPirate","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60d8697d28fb0b0a5466371b","username":"bahkiroff","display_name":"BahkirOFF","avatar_url":"//cdn.7tv.app/pp/60d8697d28fb0b0a5466371b/8dd4341b657447e1af832c71cd496bc6","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60df2685dd6a810dd4aad2d3","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":31,"height":32,"frame_count":1,"size":1444,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":31,"height":32,"frame_count":1,"size":1202,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":62,"height":64,"frame_count":1,"size":2962,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":62,"height":64,"frame_count":1,"size":3036,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":93,"height":96,"frame_count":1,"size":4669,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":93,"height":96,"frame_count":1,"size":5506,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":124,"height":128,"frame_count":1,"size":6651,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":124,"height":128,"frame_count":1,"size":8402,"format":"WEBP"}]}}},{"id":"63ffc5cef2915b442ca959bb","name":"SCATTER","flags":0,"timestamp":1680714242791,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"63ffc5cef2915b442ca959bb","name":"SCATTER","flags":0,"tags":["nowfullyscattering"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60afc2ab566c3e1fc9dabe16","username":"goodpostureisgoodforyou","display_name":"GoodPostureIsGoodForYou","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/1889c255-13e3-4730-86b4-5769c5d5105c-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63ffc5cef2915b442ca959bb","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":96,"height":24,"frame_count":17,"size":16728,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":96,"height":24,"frame_count":17,"size":16170,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":192,"height":48,"frame_count":17,"size":38997,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":192,"height":48,"frame_count":17,"size":35418,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":288,"height":72,"frame_count":17,"size":70183,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":288,"height":72,"frame_count":17,"size":58848,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":384,"height":96,"frame_count":17,"size":117561,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":384,"height":96,"frame_count":17,"size":81076,"format":"WEBP"}]}}},{"id":"63ac69cee1acc51cb86c81c3","name":"Interesting","flags":0,"timestamp":1680797133292,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"63ac69cee1acc51cb86c81c3","name":"Fascinating","flags":0,"lifecycle":3,"state":["NO_PERSONAL","LISTED"],"listed":true,"animated":true,"owner":{"id":"61938ebbd34608492cc37ff1","username":"fluxenis","display_name":"fluxenis","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63ac69cee1acc51cb86c81c3","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":33,"height":32,"frame_count":509,"size":147373,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":33,"height":32,"frame_count":509,"size":285268,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":66,"height":64,"frame_count":509,"size":382870,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":66,"height":64,"frame_count":509,"size":572458,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":99,"height":96,"frame_count":509,"size":647630,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":99,"height":96,"frame_count":509,"size":800520,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":132,"height":128,"frame_count":509,"size":882153,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":132,"height":128,"frame_count":509,"size":1059398,"format":"WEBP"}]}}},{"id":"6417322147e749c2a990e6eb","name":"VeryPog","flags":0,"timestamp":1680865675601,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"6417322147e749c2a990e6eb","name":"VeryPog","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"6053a0e8b4d31e459f0133ab","username":"devonanimation","display_name":"DevonAnimation","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/e52a289c-9d07-405e-96bb-9bc3b60dbf9f-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6417322147e749c2a990e6eb","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":96,"size":25695,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":96,"size":43076,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":96,"size":60702,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":96,"size":101444,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":96,"size":107577,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":96,"size":159426,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":96,"size":170784,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":96,"size":219934,"format":"WEBP"}]}}},{"id":"61cfbce5c267a9d0bb2b84e2","name":"PogO","flags":0,"timestamp":1680878160478,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"61cfbce5c267a9d0bb2b84e2","name":"PogO","flags":0,"tags":["kripp"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60b030c1b3e1671e27940d52","username":"mellen","display_name":"mellen","avatar_url":"//cdn.7tv.app/user/60b030c1b3e1671e27940d52/av_64f429b5592e30195214ca0b/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61cfbce5c267a9d0bb2b84e2","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1063,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":816,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1900,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2000,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":2594,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":2952,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":3440,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":4270,"format":"WEBP"}]}}},{"id":"628b4e31836261e3f0a6caa4","name":"JoiJam","flags":0,"timestamp":1680981637565,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"628b4e31836261e3f0a6caa4","name":"JoiJam","flags":0,"tags":["dance","bladerunner","holo","glow","joi"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"61aafd10e9684edbbc38eb42","username":"higherthanstarfire","display_name":"higherthanstarfire","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/d1709ed1-9054-491f-93f1-a8367cf3106b-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/628b4e31836261e3f0a6caa4","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":29,"height":32,"frame_count":39,"size":20291,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":29,"height":32,"frame_count":39,"size":36174,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":58,"height":64,"frame_count":39,"size":62374,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":58,"height":64,"frame_count":39,"size":97694,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":87,"height":96,"frame_count":39,"size":107565,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":87,"height":96,"frame_count":39,"size":174230,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":116,"height":128,"frame_count":39,"size":182893,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":116,"height":128,"frame_count":39,"size":278282,"format":"WEBP"}]}}},{"id":"60ae36ecb2ecb01505fcc586","name":"KKool","flags":0,"timestamp":1681072126529,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"60ae36ecb2ecb01505fcc586","name":"KKool","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60635b50452cea4685f26b34","username":"hecrzy","display_name":"heCrzy","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/583dd5ac-2fe8-4ead-a20d-e10770118c5f-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60ae36ecb2ecb01505fcc586","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":25,"height":32,"frame_count":13,"size":6424,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":25,"height":32,"frame_count":13,"size":11550,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":50,"height":64,"frame_count":13,"size":10330,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":50,"height":64,"frame_count":13,"size":24698,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":75,"height":96,"frame_count":13,"size":19040,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":75,"height":96,"frame_count":13,"size":40658,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":100,"height":128,"frame_count":13,"size":24392,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":100,"height":128,"frame_count":13,"size":53032,"format":"WEBP"}]}}},{"id":"64349305872aced0e7d7a28f","name":"yo","flags":0,"timestamp":1681167139177,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"64349305872aced0e7d7a28f","name":"yo","flags":0,"tags":["hey","peepohey","cute","cat","hello","greet"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae434b5d3fdae58382926a","username":"ayyybubu","display_name":"ayyybubu","avatar_url":"//cdn.7tv.app/user/60ae434b5d3fdae58382926a/av_636fa88735a18fd8b17e7399/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","62d86a8419fdcf401421c5ae","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64349305872aced0e7d7a28f","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":88,"size":38429,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":88,"size":55674,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":88,"size":83846,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":88,"size":107982,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":88,"size":140946,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":88,"size":166706,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":88,"size":202614,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":88,"size":200830,"format":"WEBP"}]}}},{"id":"614241707b14fdf700b90758","name":"residentCD","flags":0,"timestamp":1681226231672,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"614241707b14fdf700b90758","name":"residentCD","flags":0,"tags":["docpls","docspin","this","that","and","forsencd"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae81ff0bf2ee96aea05247","username":"snortexx","display_name":"snortexx","avatar_url":"//cdn.7tv.app/pp/60ae81ff0bf2ee96aea05247/183b9b6ab7624a53966fb782ec0963e0","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/614241707b14fdf700b90758","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":56,"size":20397,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":56,"size":42340,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":56,"size":41619,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":56,"size":80644,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":56,"size":74381,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":56,"size":130132,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":56,"size":108009,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":56,"size":123798,"format":"WEBP"}]}}},{"id":"6266d32530c35f39c8f85bcf","name":"ejik","flags":0,"timestamp":1681387645516,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"6266d32530c35f39c8f85bcf","name":"ejik","flags":0,"tags":["hedgehog"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"626547496616fad25e4cfab1","username":"poezdezz","display_name":"POEZDEZZ","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/3283f5be-a81b-457d-a239-7115932d00a6-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6266d32530c35f39c8f85bcf","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":30,"size":11519,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":30,"size":19354,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":30,"size":28851,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":30,"size":54372,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":30,"size":47873,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":30,"size":92848,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":30,"size":79236,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":30,"size":157664,"format":"WEBP"}]}}},{"id":"63c4b0f3d98b878bc84c3c19","name":"Teleport","flags":0,"timestamp":1681398167041,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"63c4b0f3d98b878bc84c3c19","name":"Teleport","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"63262f75e6defda92400164c","username":"s65_amg","display_name":"s65_amg","avatar_url":"//static-cdn.jtvnw.net/user-default-pictures-uv/13e5fa74-defa-11e9-809c-784f43822e80-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63c4b0f3d98b878bc84c3c19","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":31,"height":32,"frame_count":16,"size":4500,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":31,"height":32,"frame_count":16,"size":2756,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":62,"height":64,"frame_count":16,"size":6599,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":62,"height":64,"frame_count":16,"size":4578,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":93,"height":96,"frame_count":16,"size":9405,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":93,"height":96,"frame_count":16,"size":6740,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":124,"height":128,"frame_count":16,"size":12122,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":124,"height":128,"frame_count":16,"size":8966,"format":"WEBP"}]}}},{"id":"641f611ab5af37fa0622e15b","name":"HISS","flags":0,"timestamp":1681490797977,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"641f611ab5af37fa0622e15b","name":"apolloHISS","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"613bdc8feef63902454a4f2d","username":"maxdaxx","display_name":"maxdaxx","avatar_url":"//cdn.7tv.app/user/613bdc8feef63902454a4f2d/av_63b31dc35aa77eca61ec84b8/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/641f611ab5af37fa0622e15b","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":51,"size":10567,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":51,"size":18394,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":51,"size":22878,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":51,"size":36514,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":51,"size":40844,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":51,"size":55658,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":51,"size":110646,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":51,"size":89046,"format":"WEBP"}]}}},{"id":"6318a71129a5627b71e306d1","name":"missLounge","flags":0,"timestamp":1681575867572,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"6318a71129a5627b71e306d1","name":"missLounge","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"6159fb1a93686fbfe7fc1c38","username":"yabbe","display_name":"Yabbe","avatar_url":"//cdn.7tv.app/user/6159fb1a93686fbfe7fc1c38/av_634b3ae0d6ba45f7f103a8ca/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6318a71129a5627b71e306d1","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":40,"height":32,"frame_count":1,"size":1495,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":40,"height":32,"frame_count":1,"size":2168,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":80,"height":64,"frame_count":1,"size":3386,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":80,"height":64,"frame_count":1,"size":6958,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":120,"height":96,"frame_count":1,"size":5943,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":120,"height":96,"frame_count":1,"size":14156,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":160,"height":128,"frame_count":1,"size":8699,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":160,"height":128,"frame_count":1,"size":23600,"format":"WEBP"}]}}},{"id":"639b95f1172c1ebed121c965","name":"minusWOT","flags":0,"timestamp":1681575952254,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"639b95f1172c1ebed121c965","name":"minusWOT","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60af8846a3648f409a124ee4","username":"kniteort","display_name":"Kniteort","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/05070f7c-ec6a-47cf-a274-54e062b11bf7-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/639b95f1172c1ebed121c965","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1027,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1842,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1852,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":5652,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":2724,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":10580,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":3761,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":16972,"format":"WEBP"}]}}},{"id":"643ad08339b7f46dd13db217","name":"missSit","flags":0,"timestamp":1681576077668,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"643ad08339b7f46dd13db217","name":"missSit","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60ae434b5d3fdae58382926a","username":"ayyybubu","display_name":"ayyybubu","avatar_url":"//cdn.7tv.app/user/60ae434b5d3fdae58382926a/av_636fa88735a18fd8b17e7399/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","62d86a8419fdcf401421c5ae","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/643ad08339b7f46dd13db217","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":31,"height":32,"frame_count":1,"size":1373,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":31,"height":32,"frame_count":1,"size":1790,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":62,"height":64,"frame_count":1,"size":2978,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":62,"height":64,"frame_count":1,"size":5610,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":93,"height":96,"frame_count":1,"size":4966,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":93,"height":96,"frame_count":1,"size":11054,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":124,"height":128,"frame_count":1,"size":6925,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":124,"height":128,"frame_count":1,"size":17760,"format":"WEBP"}]}}},{"id":"643ada8331e0e987c69fbc9f","name":"apolloArrive","flags":0,"timestamp":1681589011233,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"643ada8331e0e987c69fbc9f","name":"apolloArrive","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"613bdc8feef63902454a4f2d","username":"maxdaxx","display_name":"maxdaxx","avatar_url":"//cdn.7tv.app/user/613bdc8feef63902454a4f2d/av_63b31dc35aa77eca61ec84b8/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/643ada8331e0e987c69fbc9f","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":37,"height":32,"frame_count":91,"size":18232,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":37,"height":32,"frame_count":82,"size":23818,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":74,"height":64,"frame_count":91,"size":31094,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":74,"height":64,"frame_count":89,"size":32528,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":111,"height":96,"frame_count":91,"size":43210,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":111,"height":96,"frame_count":90,"size":47094,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":148,"height":128,"frame_count":91,"size":50735,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":148,"height":128,"frame_count":91,"size":51436,"format":"WEBP"}]}}},{"id":"6217c7eb87952f3b8e5a272b","name":"hiss","flags":0,"timestamp":1681642348854,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"6217c7eb87952f3b8e5a272b","name":"hiss","flags":0,"tags":["nymn","apollo","minus"],"lifecycle":3,"state":["PERSONAL","LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae3e3eb2ecb01505346ae9","username":"fawcan","display_name":"Fawcan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38051bdb-83c6-4716-95e0-731462e02b45-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6217c7eb87952f3b8e5a272b","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":21,"size":5929,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":21,"size":14052,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":21,"size":12280,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":21,"size":29724,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":21,"size":20719,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":21,"size":46562,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":21,"size":32058,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":21,"size":35980,"format":"WEBP"}]}}},{"id":"6136809e4d2b0b266210f84f","name":"GIMME","flags":1,"timestamp":1681658231717,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"6136809e4d2b0b266210f84f","name":"GIMME","flags":256,"tags":["hands","gimme","handl","handr"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae8eec229664e8662a0910","username":"fapparamoar","display_name":"FapParaMoar","avatar_url":"//cdn.7tv.app/user/60ae8eec229664e8662a0910/av_6420fc305c90d67918584c61/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6136809e4d2b0b266210f84f","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":69,"height":32,"frame_count":1,"size":1755,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":69,"height":32,"frame_count":1,"size":1418,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":138,"height":64,"frame_count":1,"size":2978,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":138,"height":64,"frame_count":1,"size":3189,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":207,"height":96,"frame_count":1,"size":5134,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":207,"height":96,"frame_count":1,"size":4596,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":276,"height":128,"frame_count":1,"size":5811,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":276,"height":128,"frame_count":1,"size":7210,"format":"WEBP"}]}}},{"id":"63ffb102f20c15fd1676c1f6","name":"minusGardening","flags":0,"timestamp":1681988878445,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"63ffb102f20c15fd1676c1f6","name":"minusGardening","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae3d92259ac5a73e532901","username":"ninaturbo","display_name":"NinaTurbo","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/4d6af24a-f321-495c-9130-5fd5ded77596-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63ffb102f20c15fd1676c1f6","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":40,"height":32,"frame_count":32,"size":11023,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":40,"height":32,"frame_count":32,"size":27620,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":80,"height":64,"frame_count":32,"size":26892,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":80,"height":64,"frame_count":32,"size":70446,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":120,"height":96,"frame_count":32,"size":47805,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":120,"height":96,"frame_count":32,"size":120970,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":160,"height":128,"frame_count":32,"size":62970,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":160,"height":128,"frame_count":32,"size":163824,"format":"WEBP"}]}}},{"id":"614e96dd6251d7e000da7d22","name":"reeferSad","flags":0,"timestamp":1682090261635,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"614e96dd6251d7e000da7d22","name":"ReeferSad","flags":0,"tags":["reefer","reefersad","sad","cry"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"61044fdad85f2a3d49a157c7","username":"vizzkie","display_name":"Vizzkie","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/0cd3d724-ddf2-4f14-86d0-809ccac4d78b-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/614e96dd6251d7e000da7d22","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":158,"size":60650,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":158,"size":142990,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":158,"size":171683,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":158,"size":350388,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":158,"size":311510,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":158,"size":598160,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":158,"size":498217,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":158,"size":700246,"format":"WEBP"}]}}},{"id":"64453b0b661895a026776e76","name":"peepoYELLING","flags":0,"timestamp":1682259531820,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"64453b0b661895a026776e76","name":"peepoYELLING","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"6131dab2af9287c4eb609268","username":"vicneeel","display_name":"vicneeel","avatar_url":"//cdn.7tv.app/user/6131dab2af9287c4eb609268/av_6520576332b1db5b90ef6b24/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64453b0b661895a026776e76","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":46,"height":32,"frame_count":17,"size":6388,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":46,"height":32,"frame_count":17,"size":13736,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":92,"height":64,"frame_count":17,"size":10627,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":92,"height":64,"frame_count":17,"size":28208,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":138,"height":96,"frame_count":17,"size":16540,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":138,"height":96,"frame_count":17,"size":42312,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":184,"height":128,"frame_count":17,"size":26757,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":184,"height":128,"frame_count":17,"size":54476,"format":"WEBP"}]}}},{"id":"63a637a0294e12e5ef413f68","name":"HUHH","flags":0,"timestamp":1682334651103,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"63a637a0294e12e5ef413f68","name":"HUHH","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae3ce6aee2aa5538247a88","username":"crunt","display_name":"Crunt","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/291d7d61-b7de-4ffb-8962-aced6ce28602-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63a637a0294e12e5ef413f68","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":93,"height":32,"frame_count":1,"size":2897,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":93,"height":32,"frame_count":1,"size":6214,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":186,"height":64,"frame_count":1,"size":6784,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":186,"height":64,"frame_count":1,"size":18918,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":279,"height":96,"frame_count":1,"size":10370,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":279,"height":96,"frame_count":1,"size":35498,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":372,"height":128,"frame_count":1,"size":14676,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":372,"height":128,"frame_count":1,"size":57202,"format":"WEBP"}]}}},{"id":"61857d3d4ea2f24e50097d5d","name":"CleanTheNymN","flags":0,"timestamp":1682349551406,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"61857d3d4ea2f24e50097d5d","name":"CleanTheNymN","flags":0,"tags":["nymn","apollo","clean"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae3e3eb2ecb01505346ae9","username":"fawcan","display_name":"Fawcan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38051bdb-83c6-4716-95e0-731462e02b45-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61857d3d4ea2f24e50097d5d","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":58,"height":32,"frame_count":25,"size":9602,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":58,"height":32,"frame_count":25,"size":32046,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":116,"height":64,"frame_count":25,"size":25234,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":116,"height":64,"frame_count":25,"size":76266,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":174,"height":96,"frame_count":25,"size":46219,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":174,"height":96,"frame_count":25,"size":125652,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":232,"height":128,"frame_count":25,"size":76815,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":232,"height":128,"frame_count":25,"size":146588,"format":"WEBP"}]}}},{"id":"6446c4ec42fcbe26c8558e32","name":"Tickpollo","flags":0,"timestamp":1682359538117,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"6446c4ec42fcbe26c8558e32","name":"Tickpollo","flags":0,"tags":["apollo","tick"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae3e3eb2ecb01505346ae9","username":"fawcan","display_name":"Fawcan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38051bdb-83c6-4716-95e0-731462e02b45-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6446c4ec42fcbe26c8558e32","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":69,"height":32,"frame_count":1,"size":2090,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":69,"height":32,"frame_count":1,"size":3052,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":138,"height":64,"frame_count":1,"size":9232,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":138,"height":64,"frame_count":1,"size":4721,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":207,"height":96,"frame_count":1,"size":18420,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":207,"height":96,"frame_count":1,"size":7865,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":276,"height":128,"frame_count":1,"size":11704,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":276,"height":128,"frame_count":1,"size":30226,"format":"WEBP"}]}}},{"id":"612e446401327e773335b0d4","name":"peepoKnife","flags":0,"timestamp":1682421571078,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"612e446401327e773335b0d4","name":"peepoKnife","flags":0,"tags":["peepo","peepoknife"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60e66af0840f3a5701e0ef9d","username":"j_reality","display_name":"J_ReAlity","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/09a014b4-cf2e-4bbc-94c9-37e228e2f87e-profile_image-70x70.jpg","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/612e446401327e773335b0d4","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":16,"size":6573,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":16,"size":10062,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":16,"size":9939,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":16,"size":19716,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":16,"size":15790,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":16,"size":32176,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":16,"size":19802,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":16,"size":37960,"format":"WEBP"}]}}},{"id":"60e4fd747fa83e524442eff8","name":"HenryLookingAtYou","flags":0,"timestamp":1682522261962,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60e4fd747fa83e524442eff8","name":"HenryLookingAtYou","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60c268941388fc30a258f20e","username":"charontheferrym8","display_name":"CharonTheFerrym8","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/93efe6b8-ad0c-417f-9a31-f7004fc5dc07-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60e4fd747fa83e524442eff8","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":24,"height":32,"frame_count":1,"size":1135,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":24,"height":32,"frame_count":1,"size":836,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":48,"height":64,"frame_count":1,"size":1944,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":48,"height":64,"frame_count":1,"size":1906,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":72,"height":96,"frame_count":1,"size":2940,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":72,"height":96,"frame_count":1,"size":3288,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":96,"height":128,"frame_count":1,"size":3872,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":96,"height":128,"frame_count":1,"size":4766,"format":"WEBP"}]}}},{"id":"6365c687fe662b90f8c09b63","name":"MeAndTheBoysWatchingTwitchDotTVSlashNlUnderscoreKripp","flags":0,"timestamp":1682530549955,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"6365c687fe662b90f8c09b63","name":"MeAndTheBoysWatchingTwitchDotTVSlashNlUnderscoreKripp","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60b030c1b3e1671e27940d52","username":"mellen","display_name":"mellen","avatar_url":"//cdn.7tv.app/user/60b030c1b3e1671e27940d52/av_64f429b5592e30195214ca0b/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6365c687fe662b90f8c09b63","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":57,"height":32,"frame_count":297,"size":124911,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":57,"height":32,"frame_count":297,"size":267942,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":114,"height":64,"frame_count":297,"size":372372,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":114,"height":64,"frame_count":297,"size":693540,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":171,"height":96,"frame_count":297,"size":686095,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":171,"height":96,"frame_count":297,"size":1124492,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":228,"height":128,"frame_count":297,"size":1262095,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":228,"height":128,"frame_count":297,"size":1655902,"format":"WEBP"}]}}},{"id":"643f02a139b7f46dd13dfba4","name":"catmakingpizza","flags":0,"timestamp":1682530858294,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"643f02a139b7f46dd13dfba4","name":"catmakingpizza","flags":0,"tags":["italian","chef","cat","pizza","pamba"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60afe2d16fdef7f551687f1b","username":"tomkaishere","display_name":"tomkaishere","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/6fe16bb2-13b4-4386-8c54-4e00812aee64-profile_image-70x70.jpeg","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/643f02a139b7f46dd13dfba4","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":57,"height":32,"frame_count":248,"size":91877,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":57,"height":32,"frame_count":227,"size":150898,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":114,"height":64,"frame_count":248,"size":218564,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":114,"height":64,"frame_count":248,"size":388608,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":171,"height":96,"frame_count":248,"size":393711,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":171,"height":96,"frame_count":248,"size":640796,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":228,"height":128,"frame_count":248,"size":593007,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":228,"height":128,"frame_count":248,"size":869160,"format":"WEBP"}]}}},{"id":"616a0ffac52da56cd4908ffa","name":"PeepoPoglin","flags":0,"timestamp":1682537806920,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"616a0ffac52da56cd4908ffa","name":"PeepoPenis","flags":0,"tags":["peepo","pepe"],"lifecycle":3,"state":["NO_PERSONAL"],"listed":false,"animated":true,"owner":{"id":"612b9e52faec51694bc4e97d","username":"lofe____","display_name":"LOFE____","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/ce66c9e8-0a09-434a-bcfd-90cc04bcab90-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/616a0ffac52da56cd4908ffa","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":34,"height":32,"frame_count":6,"size":4323,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":34,"height":32,"frame_count":6,"size":7184,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":68,"height":64,"frame_count":6,"size":7600,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":68,"height":64,"frame_count":6,"size":17736,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":102,"height":96,"frame_count":6,"size":12021,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":102,"height":96,"frame_count":6,"size":30204,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":136,"height":128,"frame_count":6,"size":18217,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":136,"height":128,"frame_count":6,"size":41398,"format":"WEBP"}]}}},{"id":"614f426a20eaf897465a5b96","name":"Grabge","flags":0,"timestamp":1682612383467,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"614f426a20eaf897465a5b96","name":"Grabge","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60e45f7ae2a51883fc0779c7","username":"sylwekm10","display_name":"SylwekM10","avatar_url":"//cdn.7tv.app/pp/60e45f7ae2a51883fc0779c7/1478bd91e9454191b279d7c45a63580e","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/614f426a20eaf897465a5b96","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1296,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":952,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2556,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2400,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4086,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4382,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5751,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":6586,"format":"WEBP"}]}}},{"id":"64022890a27fda24e8076a4b","name":"Ogress","flags":0,"timestamp":1682617570780,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"64022890a27fda24e8076a4b","name":"Ogress","flags":0,"tags":["ogre","erobb221","lamont","lamonting","clm","booba"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"61afba20ffa9aba101bd4aaa","username":"ketchungus","display_name":"ketchungus","avatar_url":"//cdn.7tv.app/user/61afba20ffa9aba101bd4aaa/av_63ab295c867b3336f4a6706e/3x_static.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64022890a27fda24e8076a4b","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":53,"height":32,"frame_count":1,"size":1755,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":53,"height":32,"frame_count":1,"size":3044,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":106,"height":64,"frame_count":1,"size":3662,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":106,"height":64,"frame_count":1,"size":9412,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":159,"height":96,"frame_count":1,"size":5639,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":159,"height":96,"frame_count":1,"size":17726,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":212,"height":128,"frame_count":1,"size":8451,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":212,"height":128,"frame_count":1,"size":27206,"format":"WEBP"}]}}},{"id":"6442d2d7a9f9fb7c5f27ca5b","name":"NAILSMan","flags":0,"timestamp":1682680237095,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"6442d2d7a9f9fb7c5f27ca5b","name":"NAILSMan","flags":0,"tags":["hyru","youtube","pagman"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"6042058896832ffa785800fe","username":"zhark","display_name":"Zhark","avatar_url":"//cdn.7tv.app/pp/6042058896832ffa785800fe/37ee95ffaa9846b286cb5554ff0716c5","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6442d2d7a9f9fb7c5f27ca5b","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":24,"height":24,"frame_count":1,"size":1096,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":24,"height":24,"frame_count":1,"size":774,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":48,"height":48,"frame_count":1,"size":1675,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":48,"height":48,"frame_count":1,"size":884,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":72,"height":72,"frame_count":1,"size":1968,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":72,"height":72,"frame_count":1,"size":958,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":96,"height":96,"frame_count":1,"size":1757,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":96,"height":96,"frame_count":1,"size":1014,"format":"WEBP"}]}}},{"id":"613b2f73e92aa8cd4ed0f83c","name":"POGU","flags":0,"timestamp":1682680266476,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"613b2f73e92aa8cd4ed0f83c","name":"POGU","flags":0,"tags":["pogu","poggers","pagman","emoji"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60aecd486cfcffe15f431dca","username":"nozav","display_name":"NOZAV","avatar_url":"//cdn.7tv.app/pp/60aecd486cfcffe15f431dca/63b0f4c0f619477b9cb54f5548c8c1f0","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/613b2f73e92aa8cd4ed0f83c","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":60,"size":21223,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":60,"size":49684,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":60,"size":52035,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":60,"size":111058,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":60,"size":91450,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":60,"size":190054,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":60,"size":133585,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":60,"size":230742,"format":"WEBP"}]}}},{"id":"633d886ca177344f521f91e3","name":"deadass","flags":0,"timestamp":1682771762075,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"633d886ca177344f521f91e3","name":"deadass","flags":0,"tags":["aintnoway","lilbro","skull","nahhh","noo","way"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae759bdf5735e04acb69d9","username":"hotbear1110","display_name":"HotBear1110","avatar_url":"//cdn.7tv.app/pp/60ae759bdf5735e04acb69d9/80e2b49378c14dc6914fde8cb72fa673","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/633d886ca177344f521f91e3","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":100,"size":28916,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":100,"size":42822,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":100,"size":70116,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":100,"size":106492,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":100,"size":117505,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":100,"size":172716,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":100,"size":165648,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":100,"size":235626,"format":"WEBP"}]}}},{"id":"6390d948ac9ce9260e2c4b26","name":"deadassPls","flags":0,"timestamp":1682771780233,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"6390d948ac9ce9260e2c4b26","name":"deadassPls","flags":0,"tags":["nahhh","skeleton","dead","deadass","bruh","aintnoway"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"62cfc134100c47ee28369f87","username":"abehamm","display_name":"Abehamm","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6390d948ac9ce9260e2c4b26","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":65,"size":17110,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":65,"size":28240,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":65,"size":42102,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":65,"size":66708,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":65,"size":78904,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":65,"size":107888,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":65,"size":127986,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":65,"size":153164,"format":"WEBP"}]}}},{"id":"63ae4d85da447d37105b6c82","name":"NAHHH","flags":0,"timestamp":1682771869204,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"63ae4d85da447d37105b6c82","name":"NAHHH","flags":0,"tags":["nahhhhhhhhh","deadass","skeleton","skull","aintnoway","diesofcringe"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"626247264f320c1b8cf58d7e","username":"izeeh","display_name":"iZeeh","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63ae4d85da447d37105b6c82","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":31,"height":32,"frame_count":38,"size":18104,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":31,"height":32,"frame_count":38,"size":24548,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":62,"height":64,"frame_count":38,"size":41127,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":62,"height":64,"frame_count":38,"size":55162,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":93,"height":96,"frame_count":38,"size":69529,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":93,"height":96,"frame_count":38,"size":87180,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":124,"height":128,"frame_count":38,"size":101220,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":124,"height":128,"frame_count":38,"size":120246,"format":"WEBP"}]}}},{"id":"6041e1b789b604000d19fbbb","name":"DonkChat","flags":0,"timestamp":1682940664734,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"6041e1b789b604000d19fbbb","name":"DonkChat","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"603cbe6396832ffa78dea3df","username":"msun_","display_name":"msun_","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/ed68f02b-4930-4c5e-9f0c-87e407542391-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6041e1b789b604000d19fbbb","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":3,"size":3015,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":3,"size":2720,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":3,"size":4493,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":3,"size":6162,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":3,"size":6702,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":3,"size":10378,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":3,"size":9163,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":3,"size":14094,"format":"WEBP"}]}}},{"id":"6430122891e79e6cc532ffa4","name":"Sludge","flags":0,"timestamp":1682954602995,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"6430122891e79e6cc532ffa4","name":"Sludge","flags":0,"tags":["pepe","sad","sludge"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"62c5a668004dd4ed9b4bf5a1","username":"einglas","display_name":"Einglas","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/da0de763-9529-4ee6-8a00-c2dd0766210a-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6430122891e79e6cc532ffa4","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":58,"height":32,"frame_count":1,"size":1792,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":58,"height":32,"frame_count":1,"size":2538,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":116,"height":64,"frame_count":1,"size":3454,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":116,"height":64,"frame_count":1,"size":5542,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":174,"height":96,"frame_count":1,"size":5279,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":174,"height":96,"frame_count":1,"size":8610,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":232,"height":128,"frame_count":1,"size":6895,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":232,"height":128,"frame_count":1,"size":12226,"format":"WEBP"}]}}},{"id":"6451088a0b18ef5a58cc9df4","name":"ApolloStairs","flags":0,"timestamp":1683032237584,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"6451088a0b18ef5a58cc9df4","name":"ApolloStairs","flags":0,"tags":["apollo","nymn","cat","stairs"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae3e3eb2ecb01505346ae9","username":"fawcan","display_name":"Fawcan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38051bdb-83c6-4716-95e0-731462e02b45-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6451088a0b18ef5a58cc9df4","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":52,"size":7178,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":51,"size":8864,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":52,"size":11650,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":51,"size":20852,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":52,"size":18585,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":52,"size":32936,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":52,"size":24602,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":52,"size":46434,"format":"WEBP"}]}}},{"id":"60baca0a3285d8b0b8a051c9","name":"FLASHBANG","flags":0,"timestamp":1683033176480,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60baca0a3285d8b0b8a051c9","name":"FLASHBANG","flags":0,"tags":["bright","blind","blinding","flash","cat"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60b97c811c4540cdd1d226e0","username":"sakamotokenji","display_name":"sakamotokenji","avatar_url":"//cdn.7tv.app/pp/60b97c811c4540cdd1d226e0/31996f04b338455d94e9817f1f104411","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60baca0a3285d8b0b8a051c9","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":74,"height":32,"frame_count":25,"size":8397,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":74,"height":32,"frame_count":25,"size":24392,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":148,"height":64,"frame_count":25,"size":19206,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":148,"height":64,"frame_count":25,"size":49342,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":222,"height":96,"frame_count":25,"size":34529,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":222,"height":96,"frame_count":25,"size":76548,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":296,"height":128,"frame_count":25,"size":51386,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":296,"height":128,"frame_count":25,"size":79586,"format":"WEBP"}]}}},{"id":"6115f1a57c2d738bc1045050","name":"DonkSass","flags":0,"timestamp":1683294719897,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"6115f1a57c2d738bc1045050","name":"DonkSass","flags":0,"tags":["sass","donksass","donk","slay","periodt"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"61156f4747935f36575c7358","username":"sarcasthicc_","display_name":"Sarcasthicc_","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/a5d59712-6c0b-49bf-a010-989b363933ed-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6115f1a57c2d738bc1045050","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1688,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1176,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2810,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":3344,"format":"AVIF"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":5025,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4836,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":6946,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":7372,"format":"WEBP"}]}}},{"id":"64024a2249184efad4d9da94","name":"Legsy","flags":0,"timestamp":1683372846032,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"64024a2249184efad4d9da94","name":"Legsy","flags":0,"tags":["sexy","erobb221","sahajjj","breedable"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60fbf4081153aea242e88c24","username":"norevivanux","display_name":"Norevivanux","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/fc6b05c3-067e-4af5-b5aa-094a8a462e10-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64024a2249184efad4d9da94","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":53,"height":32,"frame_count":1,"size":1843,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":53,"height":32,"frame_count":1,"size":2858,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":106,"height":64,"frame_count":1,"size":3980,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":106,"height":64,"frame_count":1,"size":8184,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":159,"height":96,"frame_count":1,"size":6016,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":159,"height":96,"frame_count":1,"size":15860,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":212,"height":128,"frame_count":1,"size":8921,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":212,"height":128,"frame_count":1,"size":25868,"format":"WEBP"}]}}},{"id":"60c9faaaaef6df2115249ad9","name":"FeelsAyayaMan","flags":0,"timestamp":1683398607787,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60c9faaaaef6df2115249ad9","name":"FeelsAyayaMan","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60b0faaa8fb21a01bc3c0385","username":"enzo_supercraftz","display_name":"Enzo_SuperCraftZ","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60c9faaaaef6df2115249ad9","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1378,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1052,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2779,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2744,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4267,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4604,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5644,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":5536,"format":"WEBP"}]}}},{"id":"641dba3b170518c061883185","name":"SOLO","flags":0,"timestamp":1683559572933,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"641dba3b170518c061883185","name":"SOLO","flags":0,"tags":["blanka","solo","poland","eurovision"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae3e3eb2ecb01505346ae9","username":"fawcan","display_name":"Fawcan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38051bdb-83c6-4716-95e0-731462e02b45-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/641dba3b170518c061883185","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":118,"size":37109,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":118,"size":50384,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":118,"size":98535,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":118,"size":136876,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":118,"size":178153,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":118,"size":247916,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":118,"size":265719,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":118,"size":374060,"format":"WEBP"}]}}},{"id":"60ae6cdc117ec68ca46650b4","name":"borpaSpin","flags":0,"timestamp":1683629894564,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60ae6cdc117ec68ca46650b4","name":"borpaSpin","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae3d75aee2aa55382883c2","username":"victorbaya","display_name":"victorbaya","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/4f4c5649-c2f3-4837-a46f-486df3dde891-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60ae6cdc117ec68ca46650b4","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":10,"size":6144,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":10,"size":8326,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":10,"size":12000,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":10,"size":15822,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":10,"size":16073,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":10,"size":25796,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":10,"size":22440,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":10,"size":28804,"format":"WEBP"}]}}},{"id":"61e438ea77175547b4257e8b","name":"CUM","flags":0,"timestamp":1683646027039,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"61e438ea77175547b4257e8b","name":"CUM","flags":0,"tags":["badlands","chug","milk","mommy"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae81ff0bf2ee96aea05247","username":"snortexx","display_name":"snortexx","avatar_url":"//cdn.7tv.app/pp/60ae81ff0bf2ee96aea05247/183b9b6ab7624a53966fb782ec0963e0","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61e438ea77175547b4257e8b","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":206,"size":49501,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":206,"size":169384,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":206,"size":115606,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":206,"size":367160,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":206,"size":212165,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":206,"size":589842,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":206,"size":339697,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":206,"size":663026,"format":"WEBP"}]}}},{"id":"645aae88c3aa93c6583ad0f5","name":"karija","flags":0,"timestamp":1683666288435,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"645aae88c3aa93c6583ad0f5","name":"karija","flags":0,"tags":["eurovision","nymn"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"6151ed9d6251d7e000dacffc","username":"404tintin","display_name":"404tintin","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/afb3a8bd-db21-426e-8a84-cda3e9e66590-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/645aae88c3aa93c6583ad0f5","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1381,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":2094,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2892,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":6430,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4884,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":12488,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5791,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":18616,"format":"WEBP"}]}}},{"id":"643a067823e9c459eb14df32","name":"WeebShaker","flags":0,"timestamp":1683901140910,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"643a067823e9c459eb14df32","name":"Thugshaker","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60aed1d9440f48624dfe523d","username":"notoriousbob69","display_name":"Notoriousbob69","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/18003435-63fd-43cd-a856-e7ebcd5013a2-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/643a067823e9c459eb14df32","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":16,"size":9552,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":16,"size":11210,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":16,"size":19812,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":16,"size":23238,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":16,"size":32208,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":16,"size":37210,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":16,"size":45257,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":16,"size":51938,"format":"WEBP"}]}}},{"id":"645fd8be20a7827537905488","name":"Fembaj","flags":0,"timestamp":1684003128708,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"645fd8be20a7827537905488","name":"Fembaj","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"613bdc8feef63902454a4f2d","username":"maxdaxx","display_name":"maxdaxx","avatar_url":"//cdn.7tv.app/user/613bdc8feef63902454a4f2d/av_63b31dc35aa77eca61ec84b8/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/645fd8be20a7827537905488","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1394,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":2248,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2727,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":6548,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4155,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":12078,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5905,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":20110,"format":"WEBP"}]}}},{"id":"63e58ed3fc3c5fd739a18e88","name":"3Lass","flags":0,"timestamp":1684007414947,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"63e58ed3fc3c5fd739a18e88","name":"3Lass","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"613bdc8feef63902454a4f2d","username":"maxdaxx","display_name":"maxdaxx","avatar_url":"//cdn.7tv.app/user/613bdc8feef63902454a4f2d/av_63b31dc35aa77eca61ec84b8/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63e58ed3fc3c5fd739a18e88","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":58,"size":20027,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":58,"size":33314,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":58,"size":45217,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":58,"size":61030,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":58,"size":79484,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":58,"size":95104,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":58,"size":210863,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":58,"size":121442,"format":"WEBP"}]}}},{"id":"644bd932ecb88decde5c220e","name":"CHACHACHACHA","flags":0,"timestamp":1684018017540,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"644bd932ecb88decde5c220e","name":"CHACHACHACHA","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"618be1c5d34608492cc29d8a","username":"deanfluence","display_name":"deanfluence","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/e1afc82a-a096-4aba-bfa8-9a84e2733d67-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/644bd932ecb88decde5c220e","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":110,"size":53471,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":110,"size":49700,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":110,"size":132957,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":110,"size":119614,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":110,"size":239052,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":110,"size":203386,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":110,"size":362853,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":110,"size":295256,"format":"WEBP"}]}}},{"id":"63d193c9814f64e4b4840b7d","name":"OverSlept","flags":0,"timestamp":1684078011241,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"63d193c9814f64e4b4840b7d","name":"OverSlept","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"617646dbffc7244d797d1e2e","username":"zeudern","display_name":"zeudern","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/a5848694-a404-41fc-987e-329a61197a3b-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63d193c9814f64e4b4840b7d","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":68,"size":30506,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":68,"size":41928,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":68,"size":76369,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":68,"size":79926,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":68,"size":139476,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":68,"size":122880,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":68,"size":223345,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":68,"size":157406,"format":"WEBP"}]}}},{"id":"610436bfd85f2a3d49a15627","name":"apolloSleep","flags":0,"timestamp":1684078037508,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"610436bfd85f2a3d49a15627","name":"apolloSleep","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"6103188eaf7a0dae1038a521","username":"ratge","display_name":"Ratge","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/2cd4f383-3062-40d7-927c-245cd9d73455-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/610436bfd85f2a3d49a15627","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":40,"size":6545,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":40,"size":16256,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":40,"size":14022,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":40,"size":34712,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":40,"size":24610,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":40,"size":54078,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":40,"size":38640,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":40,"size":40988,"format":"WEBP"}]}}},{"id":"63ab71f6fc86875adb76bec2","name":"SoyLounge","flags":0,"timestamp":1684078167169,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"63ab71f6fc86875adb76bec2","name":"SoyLounge","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60ae434b5d3fdae58382926a","username":"ayyybubu","display_name":"ayyybubu","avatar_url":"//cdn.7tv.app/user/60ae434b5d3fdae58382926a/av_636fa88735a18fd8b17e7399/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","62d86a8419fdcf401421c5ae","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63ab71f6fc86875adb76bec2","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":66,"height":32,"frame_count":1,"size":2167,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":66,"height":32,"frame_count":1,"size":4152,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":132,"height":64,"frame_count":1,"size":5395,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":132,"height":64,"frame_count":1,"size":13912,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":198,"height":96,"frame_count":1,"size":9671,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":198,"height":96,"frame_count":1,"size":28104,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":264,"height":128,"frame_count":1,"size":13046,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":264,"height":128,"frame_count":1,"size":43520,"format":"WEBP"}]}}},{"id":"63c06b4f37038b884c0bcca7","name":"WAIT","flags":0,"timestamp":1684184420364,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"63c06b4f37038b884c0bcca7","name":"WAIT","flags":0,"tags":["uhm","huh","waiting","pausechamp","what","erm"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"613c5c8d2d7724a96175bfd5","username":"fixlation","display_name":"Fixlation","avatar_url":"//cdn.7tv.app/pp/613c5c8d2d7724a96175bfd5/2a55acd787cd4d6fa8db45899ef88e4b","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63c06b4f37038b884c0bcca7","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":47,"height":32,"frame_count":1,"size":1504,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":47,"height":32,"frame_count":1,"size":2524,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":94,"height":64,"frame_count":1,"size":7200,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":94,"height":64,"frame_count":1,"size":2829,"format":"AVIF"},{"name":"3x.avif","static_name":"3x_static.avif","width":141,"height":96,"frame_count":1,"size":4147,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":141,"height":96,"frame_count":1,"size":13290,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":188,"height":128,"frame_count":1,"size":5405,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":188,"height":128,"frame_count":1,"size":20544,"format":"WEBP"}]}}},{"id":"639e46ae601476c069424284","name":"minusAUGH","flags":0,"timestamp":1684341245057,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"639e46ae601476c069424284","name":"AUGH","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"613bdc8feef63902454a4f2d","username":"maxdaxx","display_name":"maxdaxx","avatar_url":"//cdn.7tv.app/user/613bdc8feef63902454a4f2d/av_63b31dc35aa77eca61ec84b8/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/639e46ae601476c069424284","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":920,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1814,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1723,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":5966,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3099,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":13000,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4939,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":22602,"format":"WEBP"}]}}},{"id":"63cd9a282ba67946677a26eb","name":"LMAAAOOO","flags":0,"timestamp":1684405823582,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"63cd9a282ba67946677a26eb","name":"LMAO","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"613bdc8feef63902454a4f2d","username":"maxdaxx","display_name":"maxdaxx","avatar_url":"//cdn.7tv.app/user/613bdc8feef63902454a4f2d/av_63b31dc35aa77eca61ec84b8/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63cd9a282ba67946677a26eb","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":31,"height":32,"frame_count":99,"size":29606,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":31,"height":32,"frame_count":99,"size":62298,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":62,"height":64,"frame_count":99,"size":64407,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":62,"height":64,"frame_count":99,"size":109012,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":93,"height":96,"frame_count":99,"size":105067,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":93,"height":96,"frame_count":99,"size":148242,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":124,"height":128,"frame_count":99,"size":154211,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":124,"height":128,"frame_count":99,"size":193786,"format":"WEBP"}]}}},{"id":"60420dbe77137b000de9e674","name":"nymnCringe","flags":0,"timestamp":1684442894331,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60420dbe77137b000de9e674","name":"nymnCringe","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"603ca8f696832ffa78c01eb4","username":"mauriplss","display_name":"Mauriplss","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/a428f0f0-bdd4-4c93-ac4b-ed174244cb66-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60420dbe77137b000de9e674","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1351,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1104,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2549,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2562,"format":"WEBP"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4412,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3771,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":6326,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5272,"format":"AVIF"}]}}},{"id":"6467756958d599a0419f50f1","name":"nymnQ","flags":0,"timestamp":1684501873484,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"6467756958d599a0419f50f1","name":"nymnQ","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae759bdf5735e04acb69d9","username":"hotbear1110","display_name":"HotBear1110","avatar_url":"//cdn.7tv.app/pp/60ae759bdf5735e04acb69d9/80e2b49378c14dc6914fde8cb72fa673","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6467756958d599a0419f50f1","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":907,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1670,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1492,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":4896,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":2006,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":9762,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":2752,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":15712,"format":"WEBP"}]}}},{"id":"632362a09e7e06ed5371bce3","name":"OsoPopoToto","flags":0,"timestamp":1684537597610,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"632362a09e7e06ed5371bce3","name":"OsoPopoToto","flags":0,"tags":["awwww","osopopototo","yabbe"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60aeab21229664e8663345dd","username":"barricade0_","display_name":"BARRICADE0_","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/948ce321-c188-4c7a-90c0-16169e190ac2-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/632362a09e7e06ed5371bce3","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":160,"size":24126,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":160,"size":73082,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":160,"size":58284,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":160,"size":180192,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":160,"size":98535,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":160,"size":276716,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":160,"size":514051,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":160,"size":465244,"format":"WEBP"}]}}},{"id":"6465f7c458d599a0419f07cf","name":"OkeyArrive","flags":0,"timestamp":1684580765726,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"6465f7c458d599a0419f07cf","name":"OkeyArrive","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae9be6ac03cad60784bd05","username":"cowsareamazing","display_name":"CowSArEAmazinG","avatar_url":"//static-cdn.jtvnw.net/user-default-pictures-uv/13e5fa74-defa-11e9-809c-784f43822e80-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6465f7c458d599a0419f07cf","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":25,"size":11435,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":25,"size":12066,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":25,"size":23021,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":25,"size":26238,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":25,"size":35323,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":25,"size":42488,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":25,"size":53935,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":25,"size":58250,"format":"WEBP"}]}}},{"id":"60ae31b5259ac5a73efa8dc0","name":"peepoWTF","flags":0,"timestamp":1684595120209,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60ae31b5259ac5a73efa8dc0","name":"peepoWTF","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"609daaf8bc1178732630e0ac","username":"pynput","display_name":"Pynput","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/f2dfd7d1-d373-4ab2-a114-1efa81ede4b7-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60ae31b5259ac5a73efa8dc0","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":36,"height":32,"frame_count":1,"size":1268,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":36,"height":32,"frame_count":1,"size":1112,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":72,"height":64,"frame_count":1,"size":2562,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":72,"height":64,"frame_count":1,"size":2268,"format":"AVIF"},{"name":"3x.avif","static_name":"3x_static.avif","width":108,"height":96,"frame_count":1,"size":3224,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":108,"height":96,"frame_count":1,"size":4174,"format":"WEBP"},{"name":"4x.webp","static_name":"4x_static.webp","width":144,"height":128,"frame_count":1,"size":5194,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":144,"height":128,"frame_count":1,"size":4058,"format":"AVIF"}]}}},{"id":"6468ad6743c2a132decc24cb","name":"nymnT","flags":0,"timestamp":1684623926083,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"6468ad6743c2a132decc24cb","name":"nymnT","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae8fc0ea50f43c9e3ae255","username":"agenttud","display_name":"agenttud","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/273db808-d42f-4dab-9b39-9780ef2777b0-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6468ad6743c2a132decc24cb","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":897,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1538,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1364,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":4462,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":1803,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":8342,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":2160,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":13060,"format":"WEBP"}]}}},{"id":"60ae473c5d3fdae583ac6d43","name":"KKonaW","flags":0,"timestamp":1684624140566,"actor_id":"60ae8fc0ea50f43c9e3ae255","data":{"id":"60ae473c5d3fdae583ac6d43","name":"KKonaW","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae3d75aee2aa55382883c2","username":"victorbaya","display_name":"victorbaya","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/4f4c5649-c2f3-4837-a46f-486df3dde891-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60ae473c5d3fdae583ac6d43","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1033,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":722,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":1764,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1757,"format":"AVIF"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":2324,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":2724,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":3239,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":2838,"format":"WEBP"}]}}},{"id":"603cbeee73d7a5001441f9e0","name":"BBaper","flags":0,"timestamp":1684678368476,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"603cbeee73d7a5001441f9e0","name":"BBaper","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"603cb87696832ffa78d57767","username":"obscurelambda","display_name":"obscurelambda","avatar_url":"//cdn.7tv.app/user/603cb87696832ffa78d57767/av_647a68a9804ca09ebf23e890/3x.webp","style":{"color":849892095},"roles":["60724f65e93d828bf8858789","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/603cbeee73d7a5001441f9e0","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":35,"size":14722,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":34,"size":28872,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":35,"size":24046,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":35,"size":61326,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":35,"size":39388,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":35,"size":102360,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":35,"size":39424,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":35,"size":127750,"format":"WEBP"}]}}},{"id":"646a6dcfba7e8c5faec2524b","name":"MYAAA","flags":0,"timestamp":1684696550562,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"646a6dcfba7e8c5faec2524b","name":"MYAAA","flags":0,"tags":["meow","apollo","myaaa"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae3e3eb2ecb01505346ae9","username":"fawcan","display_name":"Fawcan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38051bdb-83c6-4716-95e0-731462e02b45-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/646a6dcfba7e8c5faec2524b","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":45,"size":9290,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":45,"size":16816,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":45,"size":18824,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":45,"size":36522,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":45,"size":32160,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":45,"size":60486,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":45,"size":45478,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":45,"size":89710,"format":"WEBP"}]}}},{"id":"6323a8a720f2964c27fdba03","name":"Bubbles0","flags":1,"timestamp":1684855814425,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"6323a8a720f2964c27fdba03","name":"Bubbles0","flags":256,"tags":["bubbles","underwater","zerowidth","0width","water","swimming"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae434b5d3fdae58382926a","username":"ayyybubu","display_name":"ayyybubu","avatar_url":"//cdn.7tv.app/user/60ae434b5d3fdae58382926a/av_636fa88735a18fd8b17e7399/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","62d86a8419fdcf401421c5ae","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6323a8a720f2964c27fdba03","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":40,"size":8429,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":40,"size":20198,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":40,"size":23551,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":40,"size":46534,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":40,"size":46480,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":40,"size":77366,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":40,"size":71135,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":40,"size":96130,"format":"WEBP"}]}}},{"id":"635dd4ef3d712bde34e6c6af","name":"peepoExplore","flags":0,"timestamp":1684859333530,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"635dd4ef3d712bde34e6c6af","name":"peepoExplore","flags":0,"tags":["peepo","explore","subnautica","seamoth"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"613ba9cd90c03f6155d420b1","username":"vetricci","display_name":"Vetricci","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/7a797196-3b09-4a7f-8cff-ad832afde1a0-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/635dd4ef3d712bde34e6c6af","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":51,"height":32,"frame_count":37,"size":12290,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":51,"height":32,"frame_count":37,"size":19672,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":102,"height":64,"frame_count":37,"size":24491,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":102,"height":64,"frame_count":37,"size":40810,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":153,"height":96,"frame_count":37,"size":36735,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":153,"height":96,"frame_count":37,"size":64442,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":204,"height":128,"frame_count":37,"size":49538,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":204,"height":128,"frame_count":37,"size":89234,"format":"WEBP"}]}}},{"id":"6458075cf9eb48e0c55b3d45","name":"glebeGlebe","flags":0,"timestamp":1685013071948,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"6458075cf9eb48e0c55b3d45","name":"glebeGlebe","flags":0,"tags":["tenetene","nymn"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60afecb374d234a969633cf7","username":"tenetener","display_name":"tenetener","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/655b86e0-bd3f-49f6-bca7-ac32b8983f0d-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6458075cf9eb48e0c55b3d45","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1255,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":2194,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2363,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":6736,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3524,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":12668,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4574,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":19848,"format":"WEBP"}]}}},{"id":"61575de4b785e05aa26c0b9e","name":"KEKW","flags":0,"timestamp":1685278630544,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"61575de4b785e05aa26c0b9e","name":"JebaitedW","flags":0,"lifecycle":3,"state":["PERSONAL","LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae2af4aee2aa5538ab2144","username":"sunred_","display_name":"SunRed_","avatar_url":"//cdn.7tv.app/pp/60ae2af4aee2aa5538ab2144/acc28924022046e3b790ccaf4c7b4c53","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61575de4b785e05aa26c0b9e","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1197,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":888,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2019,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2080,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":2974,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":3348,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":3738,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":4624,"format":"WEBP"}]}}},{"id":"62288a7fb027edd02c8bf948","name":"DonkWall","flags":0,"timestamp":1685287940605,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"62288a7fb027edd02c8bf948","name":"DonkWall","flags":0,"tags":["donkwall","modwall","donowall","donk","feelsdonkman","nymn"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60b6746f64faf92496330d3c","username":"littlescampi","display_name":"LittleScampi","avatar_url":"//cdn.7tv.app/pp/60b6746f64faf92496330d3c/a1ff043123944a119d69cf9a7062a442","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/62288a7fb027edd02c8bf948","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":227,"size":49185,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":227,"size":207794,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":227,"size":213612,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":227,"size":554986,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":227,"size":461843,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":227,"size":991608,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":227,"size":877620,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":227,"size":1032878,"format":"WEBP"}]}}},{"id":"60b282e4bc491a6063da7a34","name":"Dogege","flags":0,"timestamp":1685434350041,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"60b282e4bc491a6063da7a34","name":"Dogege","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae5bdb5d3fdae583bd854e","username":"krodogs","display_name":"Krodogs","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/9bee3bc79edb7d0e-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60b282e4bc491a6063da7a34","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1265,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":922,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2268,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2441,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":3940,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3712,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4902,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":5120,"format":"WEBP"}]}}},{"id":"63b0aa4708b9976dac16f9e3","name":"meandyou","flags":0,"timestamp":1685442925200,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"63b0aa4708b9976dac16f9e3","name":"meandyou","flags":0,"tags":["homies"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"6143f893962a60904864c969","username":"panshui","display_name":"Panshui","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/9ae4a6f6-c985-4df5-bd3a-39e6445c4ff2-profile_image-70x70.png","style":{"color":-1857617921},"roles":["6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63b0aa4708b9976dac16f9e3","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1200,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":2022,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2127,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":5740,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3363,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":10642,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4119,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":15672,"format":"WEBP"}]}}},{"id":"60ae37e4b2ecb01505043293","name":"OFFLINECHAT","flags":0,"timestamp":1685474173175,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60ae37e4b2ecb01505043293","name":"OFFLINECHAT","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60ae3149aee2aa5538cfd30c","username":"pinappl_","display_name":"pinappl_","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/e73e6876-e3b9-4ed6-9a31-2cb3f8845651-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60ae37e4b2ecb01505043293","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1104,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1337,"format":"AVIF"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2301,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2404,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3341,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":3864,"format":"WEBP"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":4574,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4168,"format":"AVIF"}]}}},{"id":"6473e92e48f3329685a8f7ad","name":"catBoom","flags":0,"timestamp":1685483951767,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"6473e92e48f3329685a8f7ad","name":"catBoom","flags":0,"tags":["boom","bomb","cat","kitty"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"63fdacc50fd141cefb0944c3","username":"venu_0001","display_name":"venu_0001","avatar_url":"//cdn.7tv.app/user/63fdacc50fd141cefb0944c3/av_64306ace8df6d27e332c94c8/3x.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6473e92e48f3329685a8f7ad","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":33,"height":32,"frame_count":39,"size":11398,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":33,"height":32,"frame_count":38,"size":14040,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":66,"height":64,"frame_count":39,"size":20529,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":66,"height":64,"frame_count":39,"size":28574,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":99,"height":96,"frame_count":39,"size":31160,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":99,"height":96,"frame_count":39,"size":43930,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":132,"height":128,"frame_count":39,"size":43779,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":132,"height":128,"frame_count":39,"size":59246,"format":"WEBP"}]}}},{"id":"64771f38cc463d3ee4a97578","name":"nymnCyclops","flags":0,"timestamp":1685528452621,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"64771f38cc463d3ee4a97578","name":"nymnCyclops","flags":0,"tags":["cyclops","subnautica"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae3e3eb2ecb01505346ae9","username":"fawcan","display_name":"Fawcan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38051bdb-83c6-4716-95e0-731462e02b45-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64771f38cc463d3ee4a97578","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":17,"size":9730,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":17,"size":9126,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":17,"size":21478,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":17,"size":18724,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":17,"size":35373,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":17,"size":29754,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":17,"size":49102,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":17,"size":40504,"format":"WEBP"}]}}},{"id":"64771f490abc2f913ca35a06","name":"nymnPrawn","flags":0,"timestamp":1685528455266,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"64771f490abc2f913ca35a06","name":"nymnPrawn","flags":0,"tags":["prawn","subnautica"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae3e3eb2ecb01505346ae9","username":"fawcan","display_name":"Fawcan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38051bdb-83c6-4716-95e0-731462e02b45-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64771f490abc2f913ca35a06","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":17,"size":11209,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":17,"size":10374,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":17,"size":23944,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":17,"size":20374,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":17,"size":38633,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":17,"size":31434,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":17,"size":54912,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":17,"size":42048,"format":"WEBP"}]}}},{"id":"64771ff5cde3496c39845303","name":"nymnSeamoth","flags":0,"timestamp":1685528601480,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"64771ff5cde3496c39845303","name":"nymnSeamoth","flags":0,"tags":["subnautica","seamoth"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae3e3eb2ecb01505346ae9","username":"fawcan","display_name":"Fawcan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38051bdb-83c6-4716-95e0-731462e02b45-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64771ff5cde3496c39845303","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":17,"size":10298,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":17,"size":9232,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":17,"size":21693,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":17,"size":18614,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":17,"size":35534,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":17,"size":29476,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":17,"size":49925,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":17,"size":38774,"format":"WEBP"}]}}},{"id":"629b803d0e9a57f274bef680","name":"ULLE","flags":0,"timestamp":1685634254785,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"629b803d0e9a57f274bef680","name":"ULLE","flags":0,"tags":["lule","lulw","forsen"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"61b24584d8d026b1a66b1bd7","username":"3blanche","display_name":"3blanche","avatar_url":"//static-cdn.jtvnw.net/user-default-pictures-uv/13e5fa74-defa-11e9-809c-784f43822e80-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/629b803d0e9a57f274bef680","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1186,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":918,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2383,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2382,"format":"WEBP"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4120,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3601,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":6458,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5337,"format":"AVIF"}]}}},{"id":"6173d83d5ff09767de2a030f","name":"Kissahomie","flags":0,"timestamp":1685654295797,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"6173d83d5ff09767de2a030f","name":"Kissahomie","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60b14a727a157a7f3360fb0a","username":"mellowoke","display_name":"mellowoke","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/054be650-0eef-4c5b-82de-72c108a5b6d2-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6173d83d5ff09767de2a030f","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":57,"size":15154,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":57,"size":49690,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":57,"size":44747,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":57,"size":127360,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":57,"size":90347,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":57,"size":223178,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":57,"size":160951,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":57,"size":241314,"format":"WEBP"}]}}},{"id":"619fcbe215b3ff4a5bb7ab65","name":"BANANA","flags":0,"timestamp":1685654445576,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"619fcbe215b3ff4a5bb7ab65","name":"BANANA","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae3c29b2ecb015051f8f9a","username":"nymn","display_name":"NymN","avatar_url":"//cdn.7tv.app/pp/60ae3c29b2ecb015051f8f9a/71f269555aeb44c29100cae8aa59b56b","style":{"color":-1857617921},"roles":["6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/619fcbe215b3ff4a5bb7ab65","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":76,"size":19597,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":76,"size":59798,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":76,"size":63198,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":76,"size":155226,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":76,"size":120750,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":76,"size":263960,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":76,"size":217737,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":76,"size":223914,"format":"WEBP"}]}}},{"id":"6264401ba456cdaf745fa2ba","name":"MovieNight","flags":0,"timestamp":1685696256004,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"6264401ba456cdaf745fa2ba","name":"MovieNight","flags":0,"tags":["movie","popcorn","peepohappy","movienight"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"6241e637db3ed5fb67b4692e","username":"makkusu","display_name":"MAKKUSU","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/f3eb5473-7e41-4b3a-b4f0-4086279f9a27-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6264401ba456cdaf745fa2ba","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":29,"size":8606,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":29,"size":12956,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":29,"size":14745,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":29,"size":27300,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":29,"size":21910,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":29,"size":45270,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":29,"size":28608,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":29,"size":61478,"format":"WEBP"}]}}},{"id":"6320fd57b5b851e2f4c1ecc2","name":"ClueLookingAtYou","flags":0,"timestamp":1685704375400,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"6320fd57b5b851e2f4c1ecc2","name":"ClueLookingAtYou","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"630d1668e6de78f1ac792e55","username":"lovcen","display_name":"lovcen","avatar_url":"//static-cdn.jtvnw.net/user-default-pictures-uv/13e5fa74-defa-11e9-809c-784f43822e80-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6320fd57b5b851e2f4c1ecc2","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1133,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":868,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1991,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2100,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3057,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4384,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":3718,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":6384,"format":"WEBP"}]}}},{"id":"60538edf9d9e96000d244fa4","name":"TANTIES","flags":0,"timestamp":1685787581780,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60538edf9d9e96000d244fa4","name":"TANTIES","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60538de8b4d31e459fe6f49f","username":"moonmoon_has_tiny_teeth","display_name":"moonmoon_has_tiny_teeth","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/49d3f80c-d15a-467c-a644-ed28f8c69806-profile_image-70x70.jpg","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60538edf9d9e96000d244fa4","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":10,"size":6527,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":10,"size":7350,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":10,"size":10988,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":10,"size":16372,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":10,"size":16252,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":10,"size":28084,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":10,"size":15439,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":10,"size":34364,"format":"WEBP"}]}}},{"id":"60b03ce6fd9839f62d261658","name":"GAMBA","flags":0,"timestamp":1685975719932,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60b03ce6fd9839f62d261658","name":"GAMBA","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae99afac03cad6076c6cf1","username":"sushiguh","display_name":"sushiguh","avatar_url":"//cdn.7tv.app/user/60ae99afac03cad6076c6cf1/av_656b9d423d10142edc61c2eb/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60b03ce6fd9839f62d261658","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":59,"size":14857,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":59,"size":38020,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":59,"size":39011,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":59,"size":89448,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":59,"size":69524,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":59,"size":141586,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":59,"size":104342,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":59,"size":114222,"format":"WEBP"}]}}},{"id":"647a4a8a804ca09ebf23e032","name":"ThugShaker","flags":0,"timestamp":1685981520695,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"647a4a8a804ca09ebf23e032","name":"ThugShaker","flags":0,"tags":["ritz","bussers","zesty","gyat","dance","wiggle"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60f39e8bc07d1ac193652def","username":"shmovy","display_name":"Shmovy","avatar_url":"//cdn.7tv.app/user/60f39e8bc07d1ac193652def/av_63a4e84f5c2aba9b3b60bf46/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/647a4a8a804ca09ebf23e032","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":73,"size":26679,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":73,"size":47720,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":73,"size":50704,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":73,"size":102064,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":73,"size":78921,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":73,"size":160386,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":73,"size":101776,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":73,"size":219922,"format":"WEBP"}]}}},{"id":"61d8ee5e57c70f633ebcff76","name":"YesThisIsTheTwoTime","flags":0,"timestamp":1685987625062,"actor_id":"60ae518c0e35477634c151f1","data":{"id":"61d8ee5e57c70f633ebcff76","name":"YesThisIsTheTwoTime","flags":0,"tags":["forsen","doc","drdisrespect","forsencd","yeahbut7tv"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"616b10e8474b9b7b59a39c9f","username":"shungite_dealer_","display_name":"shungite_dealer_","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/afefb8f7-f546-449d-a4e3-b645cfd2f784-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61d8ee5e57c70f633ebcff76","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":62,"height":32,"frame_count":277,"size":162766,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":62,"height":32,"frame_count":277,"size":391436,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":124,"height":64,"frame_count":277,"size":517308,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":124,"height":64,"frame_count":277,"size":1014996,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":186,"height":96,"frame_count":277,"size":963411,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":186,"height":96,"frame_count":277,"size":1819204,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":248,"height":128,"frame_count":277,"size":1561393,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":248,"height":128,"frame_count":277,"size":2318738,"format":"WEBP"}]}}},{"id":"646e860b0dd3d25472875145","name":"hobovibi","flags":0,"timestamp":1686056809424,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"646e860b0dd3d25472875145","name":"forsenmonkeyPls","flags":0,"tags":["monkeypls","forsen","hobo"],"lifecycle":3,"state":["PERSONAL","LISTED"],"listed":true,"animated":true,"owner":{"id":"63b584154e0501de67f7a6f3","username":"hansworthelias","display_name":"HansworthElias","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/ee633431-905e-4666-bd3c-f59820cf78a4-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/646e860b0dd3d25472875145","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":30,"size":9386,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":30,"size":16162,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":30,"size":12377,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":30,"size":26710,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":30,"size":18499,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":30,"size":32820,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":30,"size":23370,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":30,"size":40614,"format":"WEBP"}]}}},{"id":"647f8d3828b72684e1227725","name":"$$ryba","flags":0,"timestamp":1686083342615,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"647f8d3828b72684e1227725","name":"plFishing","flags":0,"tags":["fishing","poland","polska","nymn"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60e736ef375879d78f881291","username":"jozefbrzeczyszczykiewicz","display_name":"JozefBrzeczyszczykiewicz","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/603e7851-8726-4745-a2c2-4d8bc3c9bdb0-profile_image-70x70.png","style":{"color":849892095},"roles":["60724f65e93d828bf8858789","6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/647f8d3828b72684e1227725","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":43,"height":32,"frame_count":1,"size":1473,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":43,"height":32,"frame_count":1,"size":1640,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":86,"height":64,"frame_count":1,"size":2782,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":86,"height":64,"frame_count":1,"size":4304,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":129,"height":96,"frame_count":1,"size":4320,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":129,"height":96,"frame_count":1,"size":7762,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":172,"height":128,"frame_count":1,"size":5633,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":172,"height":128,"frame_count":1,"size":11586,"format":"WEBP"}]}}},{"id":"60b305cd0616dd61564fa042","name":"NotSure","flags":0,"timestamp":1686222280306,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60b305cd0616dd61564fa042","name":"NotSure","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"6053853cb4d31e459fdaa2dc","username":"laden","display_name":"Laden","avatar_url":"//cdn.7tv.app/pp/6053853cb4d31e459fdaa2dc/a94c67d7736940feb543e42024b740ef","style":{"color":849892095},"roles":["60724f65e93d828bf8858789","6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60b305cd0616dd61564fa042","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1332,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1080,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2732,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2610,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4235,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4272,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5783,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":4878,"format":"WEBP"}]}}},{"id":"60f604d931ba6ae622bc8e15","name":"Copege","flags":0,"timestamp":1686222310319,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60f604d931ba6ae622bc8e15","name":"Copege","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60b0f4cbe726e379899b12d1","username":"sasquatchofny","display_name":"SasquatchOfNY","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/89d63582-48fa-45ce-9e6b-78f63c682d00-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60f604d931ba6ae622bc8e15","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":131,"size":21320,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":131,"size":138172,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":131,"size":60556,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":131,"size":341944,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":131,"size":125477,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":131,"size":593606,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":131,"size":287681,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":131,"size":725906,"format":"WEBP"}]}}},{"id":"62c8b1a1a7ffd3f6119c6797","name":"pog","flags":0,"timestamp":1686254101279,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"62c8b1a1a7ffd3f6119c6797","name":"pog","flags":0,"tags":["pog","cat"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"6104e29e57aa97350b9aa26d","username":"bitsbyalyx","display_name":"bitsbyalyx","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/3a048b86-81ea-40e3-b90e-516da431be4f-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/62c8b1a1a7ffd3f6119c6797","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":72,"size":21316,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":72,"size":39694,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":72,"size":42976,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":72,"size":80974,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":72,"size":70603,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":72,"size":126992,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":72,"size":100385,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":72,"size":175748,"format":"WEBP"}]}}},{"id":"64332e46a23ea3270ac76ae7","name":"wrrr","flags":0,"timestamp":1686254137008,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"64332e46a23ea3270ac76ae7","name":"wrrr","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"633604c93521130ec1bf2413","username":"shrekautisticfan_ref","display_name":"shrekautisticfan_ref","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/d1e74566-6d24-4efa-b55b-bbd582faa2bc-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64332e46a23ea3270ac76ae7","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":51,"height":32,"frame_count":92,"size":52694,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":51,"height":32,"frame_count":92,"size":59684,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":102,"height":64,"frame_count":92,"size":105765,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":102,"height":64,"frame_count":92,"size":119994,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":153,"height":96,"frame_count":92,"size":169762,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":153,"height":96,"frame_count":92,"size":178446,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":204,"height":128,"frame_count":92,"size":243876,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":204,"height":128,"frame_count":92,"size":242486,"format":"WEBP"}]}}},{"id":"615eafea03c9e8ba70eb65b1","name":"docStay","flags":0,"timestamp":1686307406603,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"615eafea03c9e8ba70eb65b1","name":"docStay","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60f3ebef15758a7f9a913b04","username":"betterthanbrooklyn","display_name":"BetterThanBrooklyn","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/13be7f5e-7c2b-45f8-9232-f85e0fd30f9a-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/615eafea03c9e8ba70eb65b1","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1227,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":918,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2575,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2354,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3985,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4068,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5661,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":6574,"format":"WEBP"}]}}},{"id":"6139b13bf7977b64f644cbe2","name":"vp","flags":1,"timestamp":1686307423096,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"6139b13bf7977b64f644cbe2","name":"vp","flags":256,"tags":["very","pog","verypog"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae7069b351b8d1c045ed12","username":"csinhache","display_name":"csinhache","avatar_url":"//cdn.7tv.app/pp/60ae7069b351b8d1c045ed12/aa2c2d8b508045208a12527c072dd1d2","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6139b13bf7977b64f644cbe2","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":139,"size":35130,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":139,"size":65274,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":139,"size":81194,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":139,"size":145230,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":139,"size":137420,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":139,"size":240182,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":139,"size":209019,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":139,"size":308608,"format":"WEBP"}]}}},{"id":"6484ad4252950de0e19436b3","name":"CatHandshake","flags":0,"timestamp":1686430629970,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"6484ad4252950de0e19436b3","name":"CatHandshake","flags":0,"tags":["cat"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"6238c0a274dba55ab3b3b42f","username":"jeipey","display_name":"JEIPEY","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/ee3cd8df-c1c2-47d9-848b-fb49ef231891-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6484ad4252950de0e19436b3","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":64,"height":32,"frame_count":1,"size":1778,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":64,"height":32,"frame_count":1,"size":2710,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":128,"height":64,"frame_count":1,"size":3920,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":128,"height":64,"frame_count":1,"size":7362,"format":"WEBP"},{"name":"3x.webp","static_name":"3x_static.webp","width":192,"height":96,"frame_count":1,"size":14066,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":192,"height":96,"frame_count":1,"size":6625,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":256,"height":128,"frame_count":1,"size":9771,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":256,"height":128,"frame_count":1,"size":22156,"format":"WEBP"}]}}},{"id":"60bf23fd3d4a065567751810","name":"PETLEMONKE","flags":0,"timestamp":1686430716136,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"60bf23fd3d4a065567751810","name":"PETLEMONKE","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60be9daef21e6ca0a8750609","username":"liquite_","display_name":"liquite_","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/6f787c6f-a7ee-4a0c-af4a-8dbe7f177cd7-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60bf23fd3d4a065567751810","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":5,"size":3884,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":5,"size":3768,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":5,"size":6487,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":5,"size":7610,"format":"WEBP"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":5,"size":11342,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":5,"size":9219,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":5,"size":12502,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":5,"size":12434,"format":"WEBP"}]}}},{"id":"643c96089137f98b004c8d5f","name":"happie","flags":0,"timestamp":1686585467060,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"643c96089137f98b004c8d5f","name":"happie","flags":0,"tags":["cat","catpls","happyhappyhappy","hypercat","happycat","catdance"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"641d6068268d3cdbcb81ee87","username":"spartan_puppyy","display_name":"Spartan_Puppyy","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/149a191c-e2d0-4aac-adb4-1235c35275ac-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/643c96089137f98b004c8d5f","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":33,"height":32,"frame_count":120,"size":35058,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":33,"height":32,"frame_count":120,"size":56046,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":66,"height":64,"frame_count":120,"size":64983,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":66,"height":64,"frame_count":120,"size":101792,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":99,"height":96,"frame_count":120,"size":100942,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":99,"height":96,"frame_count":120,"size":143152,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":132,"height":128,"frame_count":120,"size":130666,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":132,"height":128,"frame_count":120,"size":179356,"format":"WEBP"}]}}},{"id":"641f9d0dd9a6d7994925cbe6","name":"SoScared","flags":0,"timestamp":1686593334115,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"641f9d0dd9a6d7994925cbe6","name":"Scared","flags":0,"tags":["kitten","scaredcat","fear","scared","scawy","cute"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"614cb75a6251d7e000da4ce7","username":"eljugay","display_name":"eljuGay","avatar_url":"//cdn.7tv.app/user/614cb75a6251d7e000da4ce7/av_648f957bb3fdb6379f1e9b9b/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/641f9d0dd9a6d7994925cbe6","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":29,"height":32,"frame_count":55,"size":15657,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":29,"height":32,"frame_count":55,"size":39586,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":58,"height":64,"frame_count":55,"size":34218,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":58,"height":64,"frame_count":55,"size":82102,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":87,"height":96,"frame_count":55,"size":58281,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":87,"height":96,"frame_count":55,"size":132460,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":116,"height":128,"frame_count":55,"size":86135,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":116,"height":128,"frame_count":55,"size":184290,"format":"WEBP"}]}}},{"id":"64650c366989b9b0d46a6c25","name":"BOOBAPEEK","flags":0,"timestamp":1686927092927,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"64650c366989b9b0d46a6c25","name":"BOOBAPEEK","flags":0,"tags":["booba","peepo","frog","eye","peek"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"616ecd1b5ff09767de298fd8","username":"itzmist","display_name":"Itzmist","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/1997dc55-1d90-47bb-8f91-b1449633fc83-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64650c366989b9b0d46a6c25","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":2,"size":2775,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":2,"size":1320,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":2,"size":4188,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":2,"size":2762,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":2,"size":6031,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":2,"size":4464,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":2,"size":8933,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":2,"size":6170,"format":"WEBP"}]}}},{"id":"648f0fd5b3fdb6379f1e7a8a","name":"DiedOfHeat","flags":0,"timestamp":1687260016597,"actor_id":"60ae518c0e35477634c151f1","data":{"id":"648f0fd5b3fdb6379f1e7a8a","name":"DiedOfHeat","flags":0,"tags":["dead","sleep","summer","cat","cute"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"61d72ec157c70f633ebc7afe","username":"ovrht","display_name":"ovrhT","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/648f0fd5b3fdb6379f1e7a8a","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":43,"height":32,"frame_count":1,"size":1074,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":43,"height":32,"frame_count":1,"size":2008,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":86,"height":64,"frame_count":1,"size":2278,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":86,"height":64,"frame_count":1,"size":6542,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":129,"height":96,"frame_count":1,"size":4010,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":129,"height":96,"frame_count":1,"size":13342,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":172,"height":128,"frame_count":1,"size":5949,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":172,"height":128,"frame_count":1,"size":22436,"format":"WEBP"}]}}},{"id":"63cf25c8ec685e58d1732f53","name":"PoroSadder","flags":0,"timestamp":1687294345326,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"63cf25c8ec685e58d1732f53","name":"PoroSadder","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"61b65817d3f1830abc23d4e1","username":"sohchill","display_name":"SohChill","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/a7ea4658-5603-4ef1-9333-0dce14728915-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63cf25c8ec685e58d1732f53","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1443,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1982,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":3062,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":5996,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":5168,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":11588,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":7221,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":18404,"format":"WEBP"}]}}},{"id":"6476a00f804ca09ebf232de0","name":"Hypnime","flags":0,"timestamp":1687345367852,"actor_id":"60ae518c0e35477634c151f1","data":{"id":"6476a00f804ca09ebf232de0","name":"Hypnime","flags":0,"tags":["hypno","hypnosis","nymn","nime"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"623bb7e3bc7636f2937da399","username":"papacristobal","display_name":"PapaCristobal","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/20dda529-d361-48aa-a593-d56d6c93dd22-profile_image-70x70.jpg","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6476a00f804ca09ebf232de0","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":34,"size":13637,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":34,"size":24676,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":34,"size":27581,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":34,"size":45778,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":34,"size":46813,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":34,"size":66386,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":34,"size":65068,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":34,"size":87448,"format":"WEBP"}]}}},{"id":"62dc43a7b98f078c8a422e41","name":"Sneak","flags":0,"timestamp":1687345683660,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"62dc43a7b98f078c8a422e41","name":"Sneak","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"615a6806535ab0eaceab9cca","username":"tintillo_5754","display_name":"Tintillo_5754","avatar_url":"//cdn.7tv.app/user/615a6806535ab0eaceab9cca/av_657b9d69d3e43608a9aa455c/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/62dc43a7b98f078c8a422e41","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":14,"size":9592,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":14,"size":13170,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":14,"size":20384,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":14,"size":29088,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":14,"size":34894,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":14,"size":45998,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":14,"size":48112,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":14,"size":65488,"format":"WEBP"}]}}},{"id":"63ea6d470276acdc6138300c","name":"Versus","flags":0,"timestamp":1687346150337,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"63ea6d470276acdc6138300c","name":"VS","flags":0,"tags":["box","brawl","versus","1v1","fight","punch"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"6042058896832ffa785800fe","username":"zhark","display_name":"Zhark","avatar_url":"//cdn.7tv.app/pp/6042058896832ffa785800fe/37ee95ffaa9846b286cb5554ff0716c5","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63ea6d470276acdc6138300c","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":43,"height":32,"frame_count":75,"size":52725,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":43,"height":32,"frame_count":75,"size":73236,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":86,"height":64,"frame_count":75,"size":104256,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":86,"height":64,"frame_count":75,"size":156730,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":129,"height":96,"frame_count":75,"size":159991,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":129,"height":96,"frame_count":75,"size":247366,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":172,"height":128,"frame_count":75,"size":198725,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":172,"height":128,"frame_count":75,"size":331516,"format":"WEBP"}]}}},{"id":"64142ee86b843cb8a70022fa","name":"lava","flags":0,"timestamp":1687353081958,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"64142ee86b843cb8a70022fa","name":"lava","flags":0,"tags":["minecraft","lava","cat"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"634d21589f07e970e1f27bf8","username":"vladimirvr","display_name":"VladimirVR","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/51aa0de1-1bae-4bf4-bb80-a51e9e8a048c-profile_image-70x70.jpg","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64142ee86b843cb8a70022fa","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":30,"height":32,"frame_count":123,"size":41608,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":30,"height":32,"frame_count":121,"size":71054,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":60,"height":64,"frame_count":123,"size":100505,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":60,"height":64,"frame_count":122,"size":161044,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":90,"height":96,"frame_count":123,"size":168727,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":90,"height":96,"frame_count":123,"size":270572,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":120,"height":128,"frame_count":123,"size":229139,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":120,"height":128,"frame_count":123,"size":381194,"format":"WEBP"}]}}},{"id":"62e13e0472db7f79757f9b6a","name":"re","flags":0,"timestamp":1687380327832,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"62e13e0472db7f79757f9b6a","name":"re","flags":0,"tags":["cute","kitty","cat","hello"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"61ebf2d51a1b2a6e7324d938","username":"one94our","display_name":"one94our","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/656f9f5d-05f8-49aa-bf6a-382a253dcf80-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/62e13e0472db7f79757f9b6a","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":34,"height":32,"frame_count":61,"size":15232,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":34,"height":32,"frame_count":60,"size":44332,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":68,"height":64,"frame_count":61,"size":29462,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":68,"height":64,"frame_count":61,"size":105054,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":102,"height":96,"frame_count":61,"size":44636,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":102,"height":96,"frame_count":61,"size":167756,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":136,"height":128,"frame_count":61,"size":61417,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":136,"height":128,"frame_count":61,"size":223714,"format":"WEBP"}]}}},{"id":"63ad718b4dd0f31dfdc365ad","name":"WHATDOYOUWANTTOEAT","flags":0,"timestamp":1687430438657,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"63ad718b4dd0f31dfdc365ad","name":"WHATDOYOUWANTTOEAT","flags":0,"tags":["dumpy","trizze"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60b878c15d373afbd6addf87","username":"aiterace","display_name":"AIterAce","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/4b7896e6-7896-491c-8431-ec9c378dba3f-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63ad718b4dd0f31dfdc365ad","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":53,"size":11206,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":53,"size":16102,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":53,"size":20543,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":53,"size":34080,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":53,"size":32150,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":53,"size":52258,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":53,"size":52760,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":53,"size":81000,"format":"WEBP"}]}}},{"id":"633affecf89e2afddea7cbd6","name":"reveal0","flags":1,"timestamp":1687465768941,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"633affecf89e2afddea7cbd6","name":"Reveal","flags":256,"tags":["minecraft","speedrun","dream","forsencd"],"lifecycle":3,"state":["PERSONAL","LISTED"],"listed":true,"animated":true,"owner":{"id":"6042058896832ffa785800fe","username":"zhark","display_name":"Zhark","avatar_url":"//cdn.7tv.app/pp/6042058896832ffa785800fe/37ee95ffaa9846b286cb5554ff0716c5","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/633affecf89e2afddea7cbd6","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":43,"height":32,"frame_count":59,"size":15965,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":43,"height":32,"frame_count":49,"size":28588,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":86,"height":64,"frame_count":59,"size":29824,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":86,"height":64,"frame_count":54,"size":53838,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":129,"height":96,"frame_count":59,"size":49369,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":129,"height":96,"frame_count":59,"size":88362,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":172,"height":128,"frame_count":59,"size":55189,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":172,"height":128,"frame_count":59,"size":119408,"format":"WEBP"}]}}},{"id":"60b5abd62b064112660d3795","name":"Flowerge","flags":0,"timestamp":1687511668054,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"60b5abd62b064112660d3795","name":"Flowerge","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60b561cc04283ab952bfd4e0","username":"on_a_stack","display_name":"On_a_stack","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/1ed36b65-71c8-4eb2-a6a7-83ad2bb7566a-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60b5abd62b064112660d3795","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":950,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1334,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2354,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2521,"format":"AVIF"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3832,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4006,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5075,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":5138,"format":"WEBP"}]}}},{"id":"63de797c1d40a5212f9a5f9b","name":"xqcL","flags":0,"timestamp":1687517608748,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"63de797c1d40a5212f9a5f9b","name":"xqcL","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"631c97894f3e0f1fc59f8a43","username":"heavenice416","display_name":"HeavenIce416","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/6e73ff96-4c24-425e-aeae-f9b5837c16e2-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63de797c1d40a5212f9a5f9b","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":63,"height":32,"frame_count":1,"size":2033,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":63,"height":32,"frame_count":1,"size":4320,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":126,"height":64,"frame_count":1,"size":5375,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":126,"height":64,"frame_count":1,"size":15046,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":189,"height":96,"frame_count":1,"size":9686,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":189,"height":96,"frame_count":1,"size":30032,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":252,"height":128,"frame_count":1,"size":15155,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":252,"height":128,"frame_count":1,"size":49584,"format":"WEBP"}]}}},{"id":"63e925edad0bbe6a9c23d867","name":"NOTIFICATIONS","flags":0,"timestamp":1687607384879,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"63e925edad0bbe6a9c23d867","name":"NOTIFICATIONS","flags":0,"tags":["drdisrespect","disrespect","docing","howdoweturnoffnotifications","doc"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"61e1b61e6e676399a0ffab36","username":"pendrive","display_name":"PendrivE","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63e925edad0bbe6a9c23d867","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":64,"size":32025,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":64,"size":35794,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":64,"size":89462,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":64,"size":86194,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":64,"size":189401,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":64,"size":147378,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":64,"size":312249,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":64,"size":216968,"format":"WEBP"}]}}},{"id":"6468dcb6a8c78dc467371865","name":"xQcL","flags":0,"timestamp":1687693334110,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"6468dcb6a8c78dc467371865","name":"xQcL","flags":0,"tags":["xqcl","ticket","forsen","juicer","juicercheck","sus"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"64319f6845034e12788d219b","username":"cubedude20","display_name":"CubeDude20","avatar_url":"//cdn.7tv.app/user/64319f6845034e12788d219b/av_6470c813361980b8f005af52/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6468dcb6a8c78dc467371865","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":68,"height":32,"frame_count":1,"size":2808,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":68,"height":32,"frame_count":1,"size":4394,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":136,"height":64,"frame_count":1,"size":6565,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":136,"height":64,"frame_count":1,"size":11644,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":204,"height":96,"frame_count":1,"size":10801,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":204,"height":96,"frame_count":1,"size":20208,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":272,"height":128,"frame_count":1,"size":14408,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":272,"height":128,"frame_count":1,"size":26620,"format":"WEBP"}]}}},{"id":"60f9bd1231ba6ae6228eb8b8","name":"SadCat","flags":0,"timestamp":1687695639764,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60f9bd1231ba6ae6228eb8b8","name":"SadCat","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60d0f7a159c20115e2c6df82","username":"ojoaoh","display_name":"ojoaoh","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/5ecf5bba-4ca3-4c3a-9e04-8261bef3b04c-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60f9bd1231ba6ae6228eb8b8","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":95,"size":22857,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":95,"size":83044,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":95,"size":72759,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":95,"size":203964,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":95,"size":139637,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":95,"size":339020,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":95,"size":228459,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":95,"size":255020,"format":"WEBP"}]}}},{"id":"616ecf08b6d21adaffbe8f3f","name":"docFaint","flags":0,"timestamp":1687707226514,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"616ecf08b6d21adaffbe8f3f","name":"docFaint","flags":0,"tags":["bruhfaint","docpls","these","forsencd","bruh","faint"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae81ff0bf2ee96aea05247","username":"snortexx","display_name":"snortexx","avatar_url":"//cdn.7tv.app/pp/60ae81ff0bf2ee96aea05247/183b9b6ab7624a53966fb782ec0963e0","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/616ecf08b6d21adaffbe8f3f","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":42,"size":27874,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":42,"size":16706,"format":"AVIF"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":42,"size":35649,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":42,"size":56356,"format":"WEBP"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":42,"size":89500,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":42,"size":59465,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":42,"size":80786,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":42,"size":89162,"format":"WEBP"}]}}},{"id":"64407ade6fab698d27de11c3","name":"docReadyToFaint","flags":0,"timestamp":1687708016652,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"64407ade6fab698d27de11c3","name":"docReadyToFaint","flags":0,"tags":["scary","docfaint","drdisrespect","fainting"],"lifecycle":3,"state":["NO_PERSONAL","LISTED"],"listed":true,"animated":false,"owner":{"id":"61ce216cf644a864b441c7fb","username":"fistymart","display_name":"FistyMart","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/fistymart-profile_image-63bb6503cd5238a7-70x70.jpeg","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64407ade6fab698d27de11c3","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1189,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1686,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2182,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":4846,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3439,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":9814,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5232,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":7936,"format":"WEBP"}]}}},{"id":"6372c6a88d49e69277e3eb79","name":"Poland","flags":0,"timestamp":1687869853382,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"6372c6a88d49e69277e3eb79","name":"Poland","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"6136dba730c4a960d9509f33","username":"oastria","display_name":"oAstria","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/bdb404d6-f73d-4aef-9590-94a7a705de0a-profile_image-70x70.png","style":{"color":849892095},"roles":["60724f65e93d828bf8858789","6076a86b09a4c63a38ebe801","62d86a8419fdcf401421c5ae","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6372c6a88d49e69277e3eb79","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":47,"size":8170,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":47,"size":12326,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":47,"size":15466,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":47,"size":38252,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":47,"size":25693,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":47,"size":55744,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":47,"size":44385,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":47,"size":77524,"format":"WEBP"}]}}},{"id":"62462de10489806c448c86c2","name":"NikoBellicCrashingThroughTheWindshieldButHesASuperSoldierSoHeNeverFuckingDies","flags":0,"timestamp":1687870937328,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"62462de10489806c448c86c2","name":"NikoBellicCrashingThroughTheWindshieldButHesASuperSoldierSoHeNeverFuckingDies","flags":0,"tags":["gtaiv"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"61eaf44b009502d85be4cedd","username":"carp1g","display_name":"carp1g","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/578da9b7-edf4-403c-80ec-21c3205bedac-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/62462de10489806c448c86c2","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":57,"height":32,"frame_count":45,"size":30285,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":57,"height":32,"frame_count":45,"size":51038,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":114,"height":64,"frame_count":45,"size":93519,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":114,"height":64,"frame_count":45,"size":146776,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":171,"height":96,"frame_count":45,"size":189160,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":171,"height":96,"frame_count":45,"size":282658,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":228,"height":128,"frame_count":45,"size":355653,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":228,"height":128,"frame_count":45,"size":441576,"format":"WEBP"}]}}},{"id":"63d5db60202fab2614b784e0","name":"ThisIDidNotExpect","flags":0,"timestamp":1687870939260,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"63d5db60202fab2614b784e0","name":"ThisIDidNotExpect","flags":0,"tags":["niko","gtaiv","gta4","despair","droosy"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"624cc42d0df4d672d207030f","username":"droosy","display_name":"Droosy","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/62d3ccbe-15b3-4725-a23c-27dcbbc5970b-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63d5db60202fab2614b784e0","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":994,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1214,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1575,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2972,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":2157,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":5306,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":2710,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":7778,"format":"WEBP"}]}}},{"id":"638a709db2c8f430c66898f3","name":"FeelsNikoMan","flags":0,"timestamp":1687870945970,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"638a709db2c8f430c66898f3","name":"FeelsNikoMan","flags":0,"tags":["niko","gta","gta4","war"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae8fc0ea50f43c9e3ae255","username":"agenttud","display_name":"agenttud","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/273db808-d42f-4dab-9b39-9780ef2777b0-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/638a709db2c8f430c66898f3","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1245,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1814,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2351,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":5468,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3563,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":10478,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4534,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":16242,"format":"WEBP"}]}}},{"id":"646890d358d599a0419f882f","name":"miniPoro","flags":0,"timestamp":1687950957553,"actor_id":"60ae518c0e35477634c151f1","data":{"id":"646890d358d599a0419f882f","name":"miniPoro","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60d27440c110a67b0f772489","username":"thetoomm","display_name":"THETOOMM","avatar_url":"//cdn.7tv.app/user/60d27440c110a67b0f772489/av_652a6c93b79e22d8df9023a4/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/646890d358d599a0419f882f","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":17,"height":17,"frame_count":1,"size":838,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":17,"height":17,"frame_count":1,"size":870,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":34,"height":34,"frame_count":1,"size":992,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":34,"height":34,"frame_count":1,"size":1165,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":51,"height":51,"frame_count":1,"size":1060,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":51,"height":51,"frame_count":1,"size":1558,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":68,"height":68,"frame_count":1,"size":1338,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":68,"height":68,"frame_count":1,"size":1116,"format":"WEBP"}]}}},{"id":"63d701f28d2ddb55395c1f36","name":"stopbeingFrench","flags":0,"timestamp":1688162254457,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"63d701f28d2ddb55395c1f36","name":"stopbeingFrench","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"63d701ab6e40981149587713","username":"jdoa","display_name":"jdoa","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/d95cc91a-a4dc-4a52-851f-1fda2265c961-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63d701f28d2ddb55395c1f36","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":33,"height":32,"frame_count":1,"size":1659,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":33,"height":32,"frame_count":1,"size":2458,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":66,"height":64,"frame_count":1,"size":3263,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":66,"height":64,"frame_count":1,"size":7228,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":99,"height":96,"frame_count":1,"size":4739,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":99,"height":96,"frame_count":1,"size":13486,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":132,"height":128,"frame_count":1,"size":6220,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":132,"height":128,"frame_count":1,"size":21012,"format":"WEBP"}]}}},{"id":"612a803421ca87d781a04fd2","name":"!fund","flags":0,"timestamp":1688217858135,"actor_id":"60ae518c0e35477634c151f1","data":{"id":"612a803421ca87d781a04fd2","name":"Corpa","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"6053853cb4d31e459fdaa2dc","username":"laden","display_name":"Laden","avatar_url":"//cdn.7tv.app/pp/6053853cb4d31e459fdaa2dc/a94c67d7736940feb543e42024b740ef","style":{"color":849892095},"roles":["60724f65e93d828bf8858789","6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/612a803421ca87d781a04fd2","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1424,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1152,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2719,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2744,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4209,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4558,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5646,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":6384,"format":"WEBP"}]}}},{"id":"60d0c76d226e3fcff8421acc","name":"ForsenAgreeingWithYou","flags":0,"timestamp":1688225675759,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"60d0c76d226e3fcff8421acc","name":"ForsenAgreeingWithYou","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"6042058896832ffa785800fe","username":"zhark","display_name":"Zhark","avatar_url":"//cdn.7tv.app/pp/6042058896832ffa785800fe/37ee95ffaa9846b286cb5554ff0716c5","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60d0c76d226e3fcff8421acc","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":87,"size":21495,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":87,"size":74942,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":87,"size":47909,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":87,"size":146546,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":87,"size":86360,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":87,"size":236502,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":87,"size":134261,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":87,"size":257828,"format":"WEBP"}]}}},{"id":"64a0529cecdb531b02a2e378","name":"buh","flags":0,"timestamp":1688228562825,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"64a0529cecdb531b02a2e378","name":"buh","flags":0,"tags":["cat","wut","buh","wuh"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae434b5d3fdae58382926a","username":"ayyybubu","display_name":"ayyybubu","avatar_url":"//cdn.7tv.app/user/60ae434b5d3fdae58382926a/av_636fa88735a18fd8b17e7399/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","62d86a8419fdcf401421c5ae","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64a0529cecdb531b02a2e378","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":40,"height":32,"frame_count":119,"size":26555,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":40,"height":32,"frame_count":119,"size":77144,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":80,"height":64,"frame_count":119,"size":74295,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":80,"height":64,"frame_count":119,"size":138560,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":120,"height":96,"frame_count":119,"size":144871,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":120,"height":96,"frame_count":119,"size":214064,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":160,"height":128,"frame_count":119,"size":283480,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":160,"height":128,"frame_count":119,"size":275200,"format":"WEBP"}]}}},{"id":"60b0221a4d83b66c448ce06e","name":"SchubertWalk","flags":0,"timestamp":1688244733230,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"60b0221a4d83b66c448ce06e","name":"SchubertWalk","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60aee53ea564afa26ea9e726","username":"patrickmaybe","display_name":"PatrickMaybe","avatar_url":"//cdn.7tv.app/pp/60aee53ea564afa26ea9e726/5ddc6552878941e29bca02ef70362a2e","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60b0221a4d83b66c448ce06e","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":12,"size":11253,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":12,"size":12098,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":12,"size":24762,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":12,"size":22653,"format":"AVIF"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":12,"size":34034,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":12,"size":39212,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":12,"size":46630,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":12,"size":43548,"format":"WEBP"}]}}},{"id":"60ae43455d3fdae5838274e6","name":"noxWhat","flags":0,"timestamp":1688312557164,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"60ae43455d3fdae5838274e6","name":"noxWhat","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"6053934bb4d31e459fee53e5","username":"pilzkman","display_name":"pilzkman","avatar_url":"//cdn.7tv.app/pp/6053934bb4d31e459fee53e5/85079bb2587f4cada4dc9d3d31b5a47b","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60ae43455d3fdae5838274e6","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":43,"size":12314,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":43,"size":36076,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":43,"size":24609,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":43,"size":74864,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":43,"size":42828,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":43,"size":121610,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":43,"size":63675,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":43,"size":128584,"format":"WEBP"}]}}},{"id":"613512b7849caeb8a393d8c2","name":"spilledGlue","flags":1,"timestamp":1688366093106,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"613512b7849caeb8a393d8c2","name":"spilledGlue","flags":256,"tags":["spilledglue","zero","width","glue","coomer"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60532ec2b4d31e459f7293dc","username":"marrryanx","display_name":"Marrryanx","avatar_url":"//cdn.7tv.app/user/60532ec2b4d31e459f7293dc/av_6570dc7f834e0a119031a679/3x_static.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/613512b7849caeb8a393d8c2","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":13,"size":6850,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":13,"size":8172,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":13,"size":10965,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":13,"size":14764,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":13,"size":18146,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":13,"size":24662,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":13,"size":19628,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":13,"size":20614,"format":"WEBP"}]}}},{"id":"6361b8e5be540ad2374e701b","name":"FeelsYabbeMan","flags":0,"timestamp":1688386423600,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"6361b8e5be540ad2374e701b","name":"FeelsYabbeMan","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60af8846a3648f409a124ee4","username":"kniteort","display_name":"Kniteort","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/05070f7c-ec6a-47cf-a274-54e062b11bf7-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6361b8e5be540ad2374e701b","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1377,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1580,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2441,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":4276,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3565,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":7670,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4445,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":11846,"format":"WEBP"}]}}},{"id":"64a2bfc8d66481a52b5f256d","name":"mean0","flags":1,"timestamp":1688387617198,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"64a2bfc8d66481a52b5f256d","name":"mean0","flags":256,"tags":["stopbeingmean","mean","weirdge"],"lifecycle":3,"state":["NO_PERSONAL","LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae434b5d3fdae58382926a","username":"ayyybubu","display_name":"ayyybubu","avatar_url":"//cdn.7tv.app/user/60ae434b5d3fdae58382926a/av_636fa88735a18fd8b17e7399/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","62d86a8419fdcf401421c5ae","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64a2bfc8d66481a52b5f256d","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":47,"height":32,"frame_count":18,"size":7440,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":47,"height":32,"frame_count":18,"size":8502,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":94,"height":64,"frame_count":18,"size":14307,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":94,"height":64,"frame_count":18,"size":15648,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":141,"height":96,"frame_count":18,"size":23415,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":141,"height":96,"frame_count":18,"size":23052,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":188,"height":128,"frame_count":18,"size":33536,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":188,"height":128,"frame_count":18,"size":27804,"format":"WEBP"}]}}},{"id":"64a3e66d6b69e1b647fd38b5","name":"Programming","flags":0,"timestamp":1688462969905,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"64a3e66d6b69e1b647fd38b5","name":"Programming","flags":0,"tags":["cat","programming"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae3e3eb2ecb01505346ae9","username":"fawcan","display_name":"Fawcan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38051bdb-83c6-4716-95e0-731462e02b45-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64a3e66d6b69e1b647fd38b5","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":39,"size":11673,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":39,"size":22194,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":39,"size":27435,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":39,"size":55354,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":39,"size":49266,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":39,"size":97040,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":39,"size":84812,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":39,"size":151438,"format":"WEBP"}]}}},{"id":"64a3fa8a4a9e393297845c61","name":"Kot","flags":0,"timestamp":1688468113986,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"64a3fa8a4a9e393297845c61","name":"Kot","flags":0,"tags":["cat","polish","kot"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae3e3eb2ecb01505346ae9","username":"fawcan","display_name":"Fawcan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38051bdb-83c6-4716-95e0-731462e02b45-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64a3fa8a4a9e393297845c61","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1169,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1390,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2197,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":4026,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3556,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":7774,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5012,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":12242,"format":"WEBP"}]}}},{"id":"63371ac69af9b93dad7ba9e5","name":"ewpert","flags":0,"timestamp":1688483316246,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"63371ac69af9b93dad7ba9e5","name":"ewpert","flags":0,"tags":["robert","philip","cat","ewpert","meow"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"61064d0462ceea408a681b33","username":"rakutrash","display_name":"rakutrash","avatar_url":"//cdn.7tv.app/pp/61064d0462ceea408a681b33/dee7b58948224a8a9b288b753cf949f0","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63371ac69af9b93dad7ba9e5","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":25,"height":32,"frame_count":1,"size":1055,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":25,"height":32,"frame_count":1,"size":1328,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":50,"height":64,"frame_count":1,"size":1695,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":50,"height":64,"frame_count":1,"size":3622,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":75,"height":96,"frame_count":1,"size":2561,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":75,"height":96,"frame_count":1,"size":6690,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":100,"height":128,"frame_count":1,"size":3411,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":100,"height":128,"frame_count":1,"size":10516,"format":"WEBP"}]}}},{"id":"60b212d561df920001b3ca58","name":"glizzyR","flags":0,"timestamp":1688549905129,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60b212d561df920001b3ca58","name":"glizzyR","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60b20b0461df9200018e6eea","username":"m8use","display_name":"M8use","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/8232e029-591a-4de3-afd7-d960b4c7a626-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60b212d561df920001b3ca58","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":56,"size":33128,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":56,"size":46492,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":56,"size":68607,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":56,"size":95804,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":56,"size":106919,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":56,"size":158314,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":56,"size":140811,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":56,"size":170572,"format":"WEBP"}]}}},{"id":"60b1a41020b432903ad7129a","name":"glizzyL","flags":0,"timestamp":1688550015721,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60b1a41020b432903ad7129a","name":"glizzyL","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60b1588b50b7256d888b09b2","username":"chubbehmouse","display_name":"ChubbehMouse","avatar_url":"//cdn.7tv.app/pp/60b1588b50b7256d888b09b2/d8772dc7dbaa4cfdb9ae9f30fbaaaf35","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60b1a41020b432903ad7129a","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":150,"size":81105,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":150,"size":112492,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":150,"size":172441,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":150,"size":239486,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":150,"size":291308,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":150,"size":411804,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":150,"size":391759,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":150,"size":470058,"format":"WEBP"}]}}},{"id":"6462bd6c22acdc24c2c5de7e","name":"NeuronActivation","flags":0,"timestamp":1688558350284,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"6462bd6c22acdc24c2c5de7e","name":"NeuronActivation","flags":0,"tags":["activation","monkey","brain","neuron"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"62308c6683ac0ba37203727b","username":"sotenbori","display_name":"Sotenbori","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/2a7126a6-4b71-4019-9c14-868934a889a5-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6462bd6c22acdc24c2c5de7e","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1330,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":2166,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2835,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":6730,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4373,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":13018,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":6854,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":21488,"format":"WEBP"}]}}},{"id":"647a0242cde3496c3984e286","name":"JozefBrzeczyszczykiewicz","flags":0,"timestamp":1688558848852,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"647a0242cde3496c3984e286","name":"defaultPolishMale","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60aeb501955615deef869415","username":"froglin_","display_name":"Froglin_","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/5a020ff1-3768-4f68-8d75-d56f2dee8403-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/647a0242cde3496c3984e286","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":28,"height":32,"frame_count":1,"size":1144,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":28,"height":32,"frame_count":1,"size":1810,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":56,"height":64,"frame_count":1,"size":2282,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":56,"height":64,"frame_count":1,"size":5608,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":84,"height":96,"frame_count":1,"size":3840,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":84,"height":96,"frame_count":1,"size":10910,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":112,"height":128,"frame_count":1,"size":5375,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":112,"height":128,"frame_count":1,"size":17632,"format":"WEBP"}]}}},{"id":"63f63d960588a70e9a8d6638","name":"Polska","flags":0,"timestamp":1688559262036,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"63f63d960588a70e9a8d6638","name":"Polska","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"61bce0e75804e220aa6ae030","username":"raen","display_name":"Raen","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/ad71ec52-b146-4181-9fc1-7ea8640f6002-profile_image-70x70.png","style":{"color":-1857617921},"roles":["6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63f63d960588a70e9a8d6638","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":884,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1572,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1395,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":4690,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":1961,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":8768,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":2401,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":13624,"format":"WEBP"}]}}},{"id":"647b6e2dd4b5d6083e91c949","name":"blehE","flags":0,"timestamp":1688560187550,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"647b6e2dd4b5d6083e91c949","name":":b","flags":0,"tags":["psp1g","kitty","silly","cat","bleh","blep"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"64319f6845034e12788d219b","username":"cubedude20","display_name":"CubeDude20","avatar_url":"//cdn.7tv.app/user/64319f6845034e12788d219b/av_6470c813361980b8f005af52/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/647b6e2dd4b5d6083e91c949","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":989,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1714,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1702,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":5470,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":2452,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":10788,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":3161,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":17528,"format":"WEBP"}]}}},{"id":"62fe031499e98d38a128d66a","name":"defaultCzechMale","flags":0,"timestamp":1688572144571,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"62fe031499e98d38a128d66a","name":"krecik","flags":0,"tags":["krecik"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"621c9a18df86bac8c42fb9d0","username":"jezykk_1","display_name":"Jezykk_1","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/614f4773-1b72-4b5c-a3a4-09a7135f54de-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/62fe031499e98d38a128d66a","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":36,"height":32,"frame_count":1,"size":1075,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":36,"height":32,"frame_count":1,"size":1350,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":72,"height":64,"frame_count":1,"size":1929,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":72,"height":64,"frame_count":1,"size":3996,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":108,"height":96,"frame_count":1,"size":2948,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":108,"height":96,"frame_count":1,"size":7462,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":144,"height":128,"frame_count":1,"size":4057,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":144,"height":128,"frame_count":1,"size":11746,"format":"WEBP"}]}}},{"id":"63ac576c473fefc1365e16bf","name":"pokiEmote","flags":0,"timestamp":1688573640121,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"63ac576c473fefc1365e16bf","name":"pokiEmote","flags":0,"tags":["trikool","dance","pokimane","tiktok","poki"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae434b5d3fdae58382926a","username":"ayyybubu","display_name":"ayyybubu","avatar_url":"//cdn.7tv.app/user/60ae434b5d3fdae58382926a/av_636fa88735a18fd8b17e7399/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","62d86a8419fdcf401421c5ae","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63ac576c473fefc1365e16bf","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":274,"size":122303,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":274,"size":198886,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":274,"size":256459,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":274,"size":384450,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":274,"size":408818,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":274,"size":543776,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":274,"size":556433,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":274,"size":702996,"format":"WEBP"}]}}},{"id":"63ae2479a1fed74764cbccc4","name":"pokiCharm","flags":0,"timestamp":1688573656324,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"63ae2479a1fed74764cbccc4","name":"pokiCharm","flags":0,"tags":["queen","sassy","yass","flick","poki"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae434b5d3fdae58382926a","username":"ayyybubu","display_name":"ayyybubu","avatar_url":"//cdn.7tv.app/user/60ae434b5d3fdae58382926a/av_636fa88735a18fd8b17e7399/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","62d86a8419fdcf401421c5ae","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63ae2479a1fed74764cbccc4","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":86,"size":37447,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":86,"size":62204,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":86,"size":89719,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":86,"size":123844,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":86,"size":152134,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":86,"size":187968,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":86,"size":220578,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":86,"size":254196,"format":"WEBP"}]}}},{"id":"63b09aac5df82557901fdff7","name":"PayUp","flags":0,"timestamp":1688573666612,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"63b09aac5df82557901fdff7","name":"PayUp","flags":0,"tags":["gamba","poki","money","myna","scam","lime"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae434b5d3fdae58382926a","username":"ayyybubu","display_name":"ayyybubu","avatar_url":"//cdn.7tv.app/user/60ae434b5d3fdae58382926a/av_636fa88735a18fd8b17e7399/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","62d86a8419fdcf401421c5ae","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63b09aac5df82557901fdff7","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":131,"size":43504,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":131,"size":82404,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":131,"size":106587,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":131,"size":163092,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":131,"size":173114,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":131,"size":265492,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":131,"size":370919,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":131,"size":392946,"format":"WEBP"}]}}},{"id":"64a4d159a683f9aba55e66c8","name":"PartyPls","flags":0,"timestamp":1688587795134,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"64a4d159a683f9aba55e66c8","name":"PartyPls","flags":0,"tags":["elis","jam","dance","gathering","peepopls","monkeypls"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"61d99a8f82d5237d6795d934","username":"lithamsterlaze","display_name":"lithamsterlaze","avatar_url":"//cdn.7tv.app/user/61d99a8f82d5237d6795d934/av_656da6af92115eb399e4e677/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64a4d159a683f9aba55e66c8","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":96,"height":29,"frame_count":28,"size":24829,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":96,"height":29,"frame_count":28,"size":47996,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":192,"height":58,"frame_count":28,"size":42965,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":192,"height":58,"frame_count":28,"size":83668,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":288,"height":87,"frame_count":28,"size":53689,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":288,"height":87,"frame_count":28,"size":113330,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":384,"height":116,"frame_count":28,"size":60747,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":384,"height":116,"frame_count":28,"size":138144,"format":"WEBP"}]}}},{"id":"622b9dcc043b2a353ec8c328","name":"Oui","flags":1,"timestamp":1688770295912,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"622b9dcc043b2a353ec8c328","name":"Oui","flags":256,"tags":["brench","french","peepo","lime","texime"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"61f080f9f933d586cdda8228","username":"narjuh","display_name":"narjuh","avatar_url":"//static-cdn.jtvnw.net/user-default-pictures-uv/41780b5a-def8-11e9-94d9-784f43822e80-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/622b9dcc043b2a353ec8c328","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1255,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":980,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2058,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2092,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":3614,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3071,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":3976,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":5178,"format":"WEBP"}]}}},{"id":"64a880287107e45ee482084d","name":"angrERiot","flags":0,"timestamp":1688813732589,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"64a880287107e45ee482084d","name":"angrERiot","flags":0,"tags":["lule","play","peeporiot","forsen"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"62dfad28e2f69efc6a2c84b7","username":"esperdg","display_name":"EsperDG","avatar_url":"//cdn.7tv.app/user/62dfad28e2f69efc6a2c84b7/av_6515a414e66ad3b2e8846aab/3x.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64a880287107e45ee482084d","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":96,"height":32,"frame_count":82,"size":57892,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":96,"height":32,"frame_count":82,"size":192636,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":192,"height":64,"frame_count":82,"size":187553,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":192,"height":64,"frame_count":82,"size":540968,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":288,"height":96,"frame_count":82,"size":324006,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":288,"height":96,"frame_count":82,"size":937130,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":384,"height":128,"frame_count":82,"size":561642,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":384,"height":128,"frame_count":82,"size":1469430,"format":"WEBP"}]}}},{"id":"64a853196d4c4af9e2e454f1","name":"nim","flags":0,"timestamp":1688820792593,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"64a853196d4c4af9e2e454f1","name":"JeSuisNim","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"613bdc8feef63902454a4f2d","username":"maxdaxx","display_name":"maxdaxx","avatar_url":"//cdn.7tv.app/user/613bdc8feef63902454a4f2d/av_63b31dc35aa77eca61ec84b8/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64a853196d4c4af9e2e454f1","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1360,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":2082,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2648,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":6296,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4069,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":11854,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5902,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":18522,"format":"WEBP"}]}}},{"id":"61ea9578009502d85be4c015","name":"forsenPits","flags":0,"timestamp":1688821067206,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"61ea9578009502d85be4c015","name":"forsenPits","flags":0,"tags":["armpits"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60af9448199b90afe4b2d467","username":"motakam","display_name":"motaKam","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/3d7afd29-abb3-4774-b6fe-6157806ec9c0-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61ea9578009502d85be4c015","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":35,"height":32,"frame_count":1,"size":1337,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":35,"height":32,"frame_count":1,"size":1150,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":70,"height":64,"frame_count":1,"size":2429,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":70,"height":64,"frame_count":1,"size":2842,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":105,"height":96,"frame_count":1,"size":3657,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":105,"height":96,"frame_count":1,"size":4640,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":140,"height":128,"frame_count":1,"size":4641,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":140,"height":128,"frame_count":1,"size":7106,"format":"WEBP"}]}}},{"id":"61d5e2993d52bb5c33c4e319","name":"Labbe","flags":0,"timestamp":1688825622478,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"61d5e2993d52bb5c33c4e319","name":"Labbe","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60af8d3952a13d1adb11c8f7","username":"smagren","display_name":"smagren","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/09e5cbee-835e-4b11-a0f7-ffdc9f6d9dcb-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61d5e2993d52bb5c33c4e319","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1127,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":820,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2058,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2082,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3047,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":3510,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4595,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":5378,"format":"WEBP"}]}}},{"id":"64a96f9ab66fe5cd6f64a453","name":"nymnYell","flags":0,"timestamp":1688825864144,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"64a96f9ab66fe5cd6f64a453","name":"nymnYell","flags":0,"tags":["forsen","nymn","peepoyell","twitchcon"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae3cb1b2ecb0150521fa1f","username":"waterboiledpizza","display_name":"WaterBoiledPizza","avatar_url":"//cdn.7tv.app/user/60ae3cb1b2ecb0150521fa1f/av_652806843e9323c51e05082e/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64a96f9ab66fe5cd6f64a453","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":58,"height":32,"frame_count":1,"size":1691,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":58,"height":32,"frame_count":1,"size":2192,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":116,"height":64,"frame_count":1,"size":3768,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":116,"height":64,"frame_count":1,"size":6040,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":174,"height":96,"frame_count":1,"size":6123,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":174,"height":96,"frame_count":1,"size":11242,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":232,"height":128,"frame_count":1,"size":8272,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":232,"height":128,"frame_count":1,"size":17738,"format":"WEBP"}]}}},{"id":"64a975352d10dc644f50fac0","name":"TooMuchWork","flags":0,"timestamp":1688827205562,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"64a975352d10dc644f50fac0","name":"TooMuchWork","flags":0,"tags":["nymn"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae3e3eb2ecb01505346ae9","username":"fawcan","display_name":"Fawcan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38051bdb-83c6-4716-95e0-731462e02b45-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64a975352d10dc644f50fac0","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":96,"height":23,"frame_count":1,"size":1417,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":96,"height":23,"frame_count":1,"size":1872,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":192,"height":46,"frame_count":1,"size":2869,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":192,"height":46,"frame_count":1,"size":5232,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":288,"height":69,"frame_count":1,"size":4234,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":288,"height":69,"frame_count":1,"size":9276,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":384,"height":92,"frame_count":1,"size":6149,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":384,"height":92,"frame_count":1,"size":13754,"format":"WEBP"}]}}},{"id":"64a98e48c7d082e76f721fc5","name":"POV","flags":0,"timestamp":1688834157787,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"64a98e48c7d082e76f721fc5","name":"NymnThrottlingYou","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"613bdc8feef63902454a4f2d","username":"maxdaxx","display_name":"maxdaxx","avatar_url":"//cdn.7tv.app/user/613bdc8feef63902454a4f2d/av_63b31dc35aa77eca61ec84b8/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64a98e48c7d082e76f721fc5","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":54,"height":32,"frame_count":35,"size":23024,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":54,"height":32,"frame_count":35,"size":33324,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":108,"height":64,"frame_count":35,"size":51624,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":108,"height":64,"frame_count":35,"size":68524,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":162,"height":96,"frame_count":35,"size":82573,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":162,"height":96,"frame_count":35,"size":103068,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":216,"height":128,"frame_count":35,"size":114719,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":216,"height":128,"frame_count":35,"size":136548,"format":"WEBP"}]}}},{"id":"6481e830ef5132cc7ab59c2c","name":"$$fish","flags":0,"timestamp":1688886573225,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"6481e830ef5132cc7ab59c2c","name":"WeirdFishing","flags":0,"tags":["feelsweirdman"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60879d10fcf1f9923f6e1573","username":"somso2e","display_name":"Somso2e","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/7291e0ba-abe4-4928-9951-6becee40fb61-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6481e830ef5132cc7ab59c2c","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":55,"height":32,"frame_count":1,"size":1949,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":55,"height":32,"frame_count":1,"size":2494,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":110,"height":64,"frame_count":1,"size":4267,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":110,"height":64,"frame_count":1,"size":7026,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":165,"height":96,"frame_count":1,"size":7092,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":165,"height":96,"frame_count":1,"size":13412,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":220,"height":128,"frame_count":1,"size":9534,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":220,"height":128,"frame_count":1,"size":20684,"format":"WEBP"}]}}},{"id":"624d6dd8e02fe198206673f5","name":"NowWot","flags":0,"timestamp":1688914530730,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"624d6dd8e02fe198206673f5","name":"NowWot","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae6c1e86fc40d488e398cc","username":"kaetnn","display_name":"KAETNN","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/8489c26f-9d36-460d-a9ee-ca5bbf5d6518-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/624d6dd8e02fe198206673f5","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":48,"height":32,"frame_count":1,"size":1332,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":48,"height":32,"frame_count":1,"size":1160,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":96,"height":64,"frame_count":1,"size":3130,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":96,"height":64,"frame_count":1,"size":2746,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":144,"height":96,"frame_count":1,"size":5498,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":144,"height":96,"frame_count":1,"size":4401,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":192,"height":128,"frame_count":1,"size":6565,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":192,"height":128,"frame_count":1,"size":8872,"format":"WEBP"}]}}},{"id":"60e5a68c6d2fbedb0118109b","name":"lulWut","flags":0,"timestamp":1688918175633,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60e5a68c6d2fbedb0118109b","name":"lulWut","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60e5a4d8840f3a570116016a","username":"ieatcoomforbreakfast","display_name":"ieatcoomforbreakfast","avatar_url":"//static-cdn.jtvnw.net/user-default-pictures-uv/ce57700a-def9-11e9-842d-784f43822e80-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60e5a68c6d2fbedb0118109b","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1186,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1321,"format":"AVIF"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2801,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":3070,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4421,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":5364,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":6681,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":8412,"format":"WEBP"}]}}},{"id":"60c3d56b6d2fcea82fc7658d","name":"modCheck","flags":0,"timestamp":1689017743343,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60c3d56b6d2fcea82fc7658d","name":"modCheck","flags":0,"tags":["cat","cute","check"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60b3d2a04496c74949ac8bc5","username":"alphex2","display_name":"alphex2","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/5ed841d2-c617-4b34-9c12-a70ce497866c-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60c3d56b6d2fcea82fc7658d","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":68,"size":17646,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":68,"size":42972,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":68,"size":67683,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":68,"size":113310,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":68,"size":128372,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":68,"size":207250,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":68,"size":190542,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":68,"size":173538,"format":"WEBP"}]}}},{"id":"6466affa58d599a0419f313c","name":"WaitingForNymNToGoLive","flags":0,"timestamp":1689272995286,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"6466affa58d599a0419f313c","name":"WaitingForStream","flags":0,"tags":["stream","futurama","waiting"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"632e25835d511ac23bcc8b34","username":"br0wlol","display_name":"Br0wLoL","avatar_url":"//cdn.7tv.app/user/632e25835d511ac23bcc8b34/av_64e262f9e4d325845e86c521/3x_static.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6466affa58d599a0419f313c","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":64,"size":20456,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":64,"size":29976,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":64,"size":51971,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":64,"size":71110,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":64,"size":96254,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":64,"size":117262,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":64,"size":146907,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":64,"size":161274,"format":"WEBP"}]}}},{"id":"63fb2371e5d9925da81238e8","name":"catBruh","flags":0,"timestamp":1689282869873,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"63fb2371e5d9925da81238e8","name":"bruh","flags":0,"tags":["bruuuuuuuuuuuuuuuuuuuuuuuuh","catsofcringe","bruh","catbruh","cat","bruuuh"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"6311ef9cbdf4c4798bed60f0","username":"mamanesipolotence","display_name":"MamaNesiPOLOTENCE","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/7cb5165c-9ebb-4b51-9ef9-33e10027c6a9-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63fb2371e5d9925da81238e8","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":40,"height":32,"frame_count":91,"size":21328,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":40,"height":32,"frame_count":91,"size":43854,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":80,"height":64,"frame_count":91,"size":47722,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":80,"height":64,"frame_count":91,"size":94778,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":120,"height":96,"frame_count":91,"size":75857,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":120,"height":96,"frame_count":91,"size":148042,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":160,"height":128,"frame_count":91,"size":152759,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":160,"height":128,"frame_count":91,"size":220212,"format":"WEBP"}]}}},{"id":"64aff4ae2b9b9a7b4ba0482e","name":"ribbon0","flags":1,"timestamp":1689330958874,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"64aff4ae2b9b9a7b4ba0482e","name":"peeporibbon","flags":256,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"639b9f25e940ad10c008b5f5","username":"hibike7","display_name":"HiBike7","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/42199548-7b96-4d88-b9b4-5f5ca4e8d4ee-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64aff4ae2b9b9a7b4ba0482e","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1336,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":954,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2349,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2836,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3582,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":5904,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4762,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":7582,"format":"WEBP"}]}}},{"id":"64b1a457c4bd269b16f46fb9","name":"SoyPoint","flags":0,"timestamp":1689365859883,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"64b1a457c4bd269b16f46fb9","name":"SoyPoint","flags":0,"lifecycle":3,"state":["NO_PERSONAL","LISTED"],"listed":true,"animated":false,"owner":{"id":"613bdc8feef63902454a4f2d","username":"maxdaxx","display_name":"maxdaxx","avatar_url":"//cdn.7tv.app/user/613bdc8feef63902454a4f2d/av_63b31dc35aa77eca61ec84b8/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64b1a457c4bd269b16f46fb9","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":31,"height":32,"frame_count":1,"size":1333,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":31,"height":32,"frame_count":1,"size":1764,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":62,"height":64,"frame_count":1,"size":2627,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":62,"height":64,"frame_count":1,"size":5284,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":93,"height":96,"frame_count":1,"size":4132,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":93,"height":96,"frame_count":1,"size":10354,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":124,"height":128,"frame_count":1,"size":5909,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":124,"height":128,"frame_count":1,"size":16664,"format":"WEBP"}]}}},{"id":"6379879d541f8c821fb1fe17","name":"ppBounce","flags":0,"timestamp":1689430419824,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"6379879d541f8c821fb1fe17","name":"ppBounce","flags":0,"tags":["pphop","pphopper","ppoverheat","pepel","rescaled","ppl"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60b04a7fad7fb4b50bd3a982","username":"brian6932","display_name":"brian6932","avatar_url":"//cdn.7tv.app/user/60b04a7fad7fb4b50bd3a982/av_64f8035f8bef730969094d7a/3x_static.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6379879d541f8c821fb1fe17","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":11,"height":28,"frame_count":21,"size":3756,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":11,"height":28,"frame_count":21,"size":2546,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":22,"height":56,"frame_count":21,"size":4194,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":22,"height":56,"frame_count":21,"size":2588,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":33,"height":84,"frame_count":21,"size":4475,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":33,"height":84,"frame_count":21,"size":2938,"format":"WEBP"},{"name":"4x.webp","static_name":"4x_static.webp","width":44,"height":112,"frame_count":21,"size":2802,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":44,"height":112,"frame_count":21,"size":4048,"format":"AVIF"}]}}},{"id":"64b2d3d5b230f419266f857b","name":"peepoShaker","flags":0,"timestamp":1689501659208,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"64b2d3d5b230f419266f857b","name":"peepoShaker","flags":0,"tags":["partypls","thugshaker","peepopls"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"62dfad28e2f69efc6a2c84b7","username":"esperdg","display_name":"EsperDG","avatar_url":"//cdn.7tv.app/user/62dfad28e2f69efc6a2c84b7/av_6515a414e66ad3b2e8846aab/3x.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64b2d3d5b230f419266f857b","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":22,"size":9091,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":22,"size":4352,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":22,"size":8636,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":22,"size":4480,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":22,"size":12896,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":22,"size":5822,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":22,"size":10885,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":22,"size":5360,"format":"WEBP"}]}}},{"id":"64b43651b230f419266fd3de","name":"nymnWatchingBuildersRenovatingHisHouse","flags":0,"timestamp":1689532155124,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"64b43651b230f419266fd3de","name":"nymnWatchingBuildersRenovatingHisHouse","flags":0,"tags":["nymn","yabbe","house"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae3e3eb2ecb01505346ae9","username":"fawcan","display_name":"Fawcan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38051bdb-83c6-4716-95e0-731462e02b45-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64b43651b230f419266fd3de","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":48,"height":32,"frame_count":101,"size":20139,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":48,"height":32,"frame_count":101,"size":63244,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":96,"height":64,"frame_count":101,"size":78232,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":96,"height":64,"frame_count":101,"size":169102,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":144,"height":96,"frame_count":101,"size":177186,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":144,"height":96,"frame_count":101,"size":286280,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":192,"height":128,"frame_count":101,"size":455843,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":192,"height":128,"frame_count":101,"size":454832,"format":"WEBP"}]}}},{"id":"63d5c4d111bbef1b64b10645","name":"crunch","flags":0,"timestamp":1689610521657,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"63d5c4d111bbef1b64b10645","name":"yum","flags":0,"tags":["bussin","verypog","monkeycatluna","cateat","plink"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ff0e7a25bb6dd0b03e40f9","username":"saffybop","display_name":"saffybop","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/fd91a409-b82f-474f-a83f-45ab6e4bc3f1-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63d5c4d111bbef1b64b10645","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":114,"size":41444,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":114,"size":69084,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":114,"size":106072,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":114,"size":129374,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":114,"size":179604,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":114,"size":200518,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":114,"size":276161,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":114,"size":238812,"format":"WEBP"}]}}},{"id":"63792919bd65aba244978aac","name":"PoroFlushed","flags":0,"timestamp":1689770538832,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"63792919bd65aba244978aac","name":"PoroFlushed","flags":0,"tags":["flushed","porohappy","plm","poro","poropls","porotwerk"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60d27440c110a67b0f772489","username":"thetoomm","display_name":"THETOOMM","avatar_url":"//cdn.7tv.app/user/60d27440c110a67b0f772489/av_652a6c93b79e22d8df9023a4/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63792919bd65aba244978aac","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1444,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":2102,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":3089,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":6366,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4909,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":12310,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":7271,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":19364,"format":"WEBP"}]}}},{"id":"64b83959accfe6461e7c6ac4","name":"BAND","flags":0,"timestamp":1689870401161,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"64b83959accfe6461e7c6ac4","name":"BANND","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"62e1946ef20806d3c47d1702","username":"decentbv","display_name":"DecentBV","avatar_url":"//cdn.7tv.app/user/62e1946ef20806d3c47d1702/av_658f15b252e38f90f33448e2/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64b83959accfe6461e7c6ac4","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":202,"size":52872,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":202,"size":134694,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":202,"size":121208,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":202,"size":231560,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":202,"size":213518,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":202,"size":391926,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":202,"size":308605,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":202,"size":436614,"format":"WEBP"}]}}},{"id":"64a60032a8b7560b00a3ea06","name":"docalmostnotL","flags":0,"timestamp":1689963720593,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"64a60032a8b7560b00a3ea06","name":"docalmostnotL","flags":0,"tags":["drdisrespect","smash","slam","docl","docnotl"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"62dfad28e2f69efc6a2c84b7","username":"esperdg","display_name":"EsperDG","avatar_url":"//cdn.7tv.app/user/62dfad28e2f69efc6a2c84b7/av_6515a414e66ad3b2e8846aab/3x.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64a60032a8b7560b00a3ea06","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":70,"size":39206,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":70,"size":51608,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":70,"size":94148,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":70,"size":112980,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":70,"size":149369,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":70,"size":191174,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":70,"size":220978,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":70,"size":262672,"format":"WEBP"}]}}},{"id":"625d08d278e5be390b9a0427","name":"Gaslight","flags":0,"timestamp":1690111362786,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"625d08d278e5be390b9a0427","name":"peepoGaslight","flags":0,"tags":["gaslight","peepo","pepem","xqcm","evil","manipulate"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"6241e637db3ed5fb67b4692e","username":"makkusu","display_name":"MAKKUSU","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/f3eb5473-7e41-4b3a-b4f0-4086279f9a27-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/625d08d278e5be390b9a0427","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1318,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1020,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2631,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2414,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3976,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4124,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5367,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":5976,"format":"WEBP"}]}}},{"id":"64bd0eba8424fe613723586f","name":"beavsPotFriend","flags":0,"timestamp":1690111758829,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"64bd0eba8424fe613723586f","name":"beavsPotFriend","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"613bdc8feef63902454a4f2d","username":"maxdaxx","display_name":"maxdaxx","avatar_url":"//cdn.7tv.app/user/613bdc8feef63902454a4f2d/av_63b31dc35aa77eca61ec84b8/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64bd0eba8424fe613723586f","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1633,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1560,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":3595,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":4526,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":5565,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":8818,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":8083,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":13490,"format":"WEBP"}]}}},{"id":"64bd0f2c9a49e5f24ed79c13","name":"beavsAlright","flags":0,"timestamp":1690111837200,"actor_id":"60ae8fc0ea50f43c9e3ae255","data":{"id":"64bd0f2c9a49e5f24ed79c13","name":"beavsAlright","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae8fc0ea50f43c9e3ae255","username":"agenttud","display_name":"agenttud","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/273db808-d42f-4dab-9b39-9780ef2777b0-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64bd0f2c9a49e5f24ed79c13","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1000,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1932,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1552,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":5568,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":2193,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":10362,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":2788,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":16844,"format":"WEBP"}]}}},{"id":"60d01df40af37284c9cf839b","name":"nymnReady","flags":0,"timestamp":1690111910224,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"60d01df40af37284c9cf839b","name":"nymnReady","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60b561cc04283ab952bfd4e0","username":"on_a_stack","display_name":"On_a_stack","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/1ed36b65-71c8-4eb2-a6a7-83ad2bb7566a-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60d01df40af37284c9cf839b","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":51,"size":18553,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":51,"size":40672,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":51,"size":37546,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":51,"size":85190,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":51,"size":63281,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":51,"size":135746,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":51,"size":94530,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":51,"size":144312,"format":"WEBP"}]}}},{"id":"614919291eb7078240528998","name":"PeepoPoolboy","flags":0,"timestamp":1690112298618,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"614919291eb7078240528998","name":"PeepoPoolboy","flags":0,"tags":["poolboy","peepo","peepopoolboy"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"61356041d21b5ea97e5956ad","username":"crylax_","display_name":"Crylax_","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/aa4e1684-b5c3-4cbd-bd1d-5efdfbc1f038-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/614919291eb7078240528998","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1700,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1346,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":3465,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":3316,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":5235,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":5776,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":7411,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":8714,"format":"WEBP"}]}}},{"id":"60afe9efb254a5e16b8705df","name":"BillyArrive","flags":0,"timestamp":1690112339135,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"60afe9efb254a5e16b8705df","name":"BillyArrive","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60afebabfd9839f62d883284","username":"h4te_l1fe","display_name":"h4te_l1fe","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/bcfbed83-5005-4ff9-8d95-520d9706280d-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60afe9efb254a5e16b8705df","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":83,"size":33381,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":83,"size":64020,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":83,"size":92124,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":83,"size":162672,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":83,"size":158339,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":83,"size":269218,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":83,"size":246540,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":83,"size":231444,"format":"WEBP"}]}}},{"id":"64bd1e58fb581ca4e4d1c676","name":"NymnTellingAnotherJoke","flags":0,"timestamp":1690115783776,"actor_id":"60ae8fc0ea50f43c9e3ae255","data":{"id":"64bd1e58fb581ca4e4d1c676","name":"NymnTellingAnotherJoke","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae8fc0ea50f43c9e3ae255","username":"agenttud","display_name":"agenttud","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/273db808-d42f-4dab-9b39-9780ef2777b0-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64bd1e58fb581ca4e4d1c676","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":50,"height":32,"frame_count":159,"size":36567,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":50,"height":32,"frame_count":159,"size":114222,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":100,"height":64,"frame_count":159,"size":92379,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":100,"height":64,"frame_count":159,"size":247516,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":150,"height":96,"frame_count":159,"size":160855,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":150,"height":96,"frame_count":159,"size":362498,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":200,"height":128,"frame_count":159,"size":244544,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":200,"height":128,"frame_count":159,"size":484326,"format":"WEBP"}]}}},{"id":"61710fdeffc7244d797ca707","name":"GoslingDrive","flags":0,"timestamp":1690150347927,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"61710fdeffc7244d797ca707","name":"GoslingDrive","flags":0,"tags":["ryan","gosling","drive"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"61710d645ff09767de29c651","username":"hawkiman","display_name":"Hawkiman","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/14aa8b93-bca0-423d-85c3-1d8a86089866-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61710fdeffc7244d797ca707","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":73,"height":32,"frame_count":52,"size":22272,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":73,"height":32,"frame_count":52,"size":67962,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":146,"height":64,"frame_count":52,"size":62506,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":146,"height":64,"frame_count":52,"size":167064,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":219,"height":96,"frame_count":52,"size":110285,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":219,"height":96,"frame_count":52,"size":276412,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":292,"height":128,"frame_count":52,"size":186864,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":292,"height":128,"frame_count":52,"size":351922,"format":"WEBP"}]}}},{"id":"62121e6e35d91b821ec0eb26","name":"alizeePls","flags":0,"timestamp":1690151956756,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"62121e6e35d91b821ec0eb26","name":"alizeePls","flags":0,"tags":["alizee","pls","dance"],"lifecycle":3,"state":["NO_PERSONAL","LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae3e98b2ecb0150535c6b7","username":"gempir","display_name":"gempir","avatar_url":"//cdn.7tv.app/pp/60ae3e98b2ecb0150535c6b7/4aa1786cec024098be20d7b0683bae72","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/62121e6e35d91b821ec0eb26","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":137,"size":67419,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":137,"size":101364,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":137,"size":138050,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":137,"size":198658,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":137,"size":226155,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":137,"size":321194,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":137,"size":304185,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":137,"size":310636,"format":"WEBP"}]}}},{"id":"64c01abf0365af129c48e90a","name":"handsWup","flags":0,"timestamp":1690311386377,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"64c01abf0365af129c48e90a","name":"handsWup","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae3e3eb2ecb01505346ae9","username":"fawcan","display_name":"Fawcan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38051bdb-83c6-4716-95e0-731462e02b45-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64c01abf0365af129c48e90a","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":38,"height":32,"frame_count":1,"size":1557,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":38,"height":32,"frame_count":1,"size":1954,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":76,"height":64,"frame_count":1,"size":3290,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":76,"height":64,"frame_count":1,"size":6092,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":114,"height":96,"frame_count":1,"size":5254,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":114,"height":96,"frame_count":1,"size":11754,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":152,"height":128,"frame_count":1,"size":7857,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":152,"height":128,"frame_count":1,"size":19248,"format":"WEBP"}]}}},{"id":"64c034dfe96a5dd41515f3c7","name":"BBQING","flags":0,"timestamp":1690368246237,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"64c034dfe96a5dd41515f3c7","name":"BBQING","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"613bdc8feef63902454a4f2d","username":"maxdaxx","display_name":"maxdaxx","avatar_url":"//cdn.7tv.app/user/613bdc8feef63902454a4f2d/av_63b31dc35aa77eca61ec84b8/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64c034dfe96a5dd41515f3c7","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":11,"size":5943,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":11,"size":7030,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":11,"size":11307,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":11,"size":15002,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":11,"size":17780,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":11,"size":23444,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":11,"size":23540,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":11,"size":31170,"format":"WEBP"}]}}},{"id":"639b68b03369d865cf442c30","name":"mrbeast","flags":0,"timestamp":1690543312575,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"639b68b03369d865cf442c30","name":"mrbeast","flags":0,"tags":["mrbreaaaaast","breast"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"630674bdbe8c19d70f9d6897","username":"ace____________________","display_name":"ace____________________","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/23c902b6-9c0f-4c47-861a-573c918d6fd9-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/639b68b03369d865cf442c30","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":57,"height":32,"frame_count":54,"size":26754,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":57,"height":32,"frame_count":54,"size":57948,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":114,"height":64,"frame_count":54,"size":69147,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":114,"height":64,"frame_count":54,"size":128274,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":171,"height":96,"frame_count":54,"size":116030,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":171,"height":96,"frame_count":54,"size":207440,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":228,"height":128,"frame_count":54,"size":167002,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":228,"height":128,"frame_count":54,"size":287578,"format":"WEBP"}]}}},{"id":"60e6091f6d2fbedb01ee2cc2","name":"gunpowder","flags":0,"timestamp":1690552648766,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60e6091f6d2fbedb01ee2cc2","name":"gunpowder","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ca44249fd6791bfcea1a39","username":"deaththedoorant","display_name":"DeathTheDoorAnt","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/319d1273-c341-432d-80e7-37cbd65f2b29-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60e6091f6d2fbedb01ee2cc2","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":869,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":566,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":974,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":1044,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":1266,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":1410,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":1591,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":1484,"format":"WEBP"}]}}},{"id":"63c56fc3dedcfdd2e2b5d85a","name":"lookBoth","flags":0,"timestamp":1690569526731,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"63c56fc3dedcfdd2e2b5d85a","name":"lookBoth","flags":0,"tags":["lookup","lookdown","apu"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"6042058896832ffa785800fe","username":"zhark","display_name":"Zhark","avatar_url":"//cdn.7tv.app/pp/6042058896832ffa785800fe/37ee95ffaa9846b286cb5554ff0716c5","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63c56fc3dedcfdd2e2b5d85a","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":19,"size":9780,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":19,"size":14538,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":27,"size":19653,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":19,"size":31774,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":29,"size":28395,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":19,"size":48828,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":39,"size":37774,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":19,"size":65222,"format":"WEBP"}]}}},{"id":"60b05d483cadd71dffc67135","name":"WifeCheck","flags":0,"timestamp":1690663328812,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"60b05d483cadd71dffc67135","name":"WifeCheck","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae653c9627f9aff4f5ccd1","username":"xoo_6119","display_name":"xoo_6119","avatar_url":"//cdn.7tv.app/user/60ae653c9627f9aff4f5ccd1/av_63ca0eccdedb49b24383ae5c/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60b05d483cadd71dffc67135","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":30,"height":32,"frame_count":156,"size":63891,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":30,"height":32,"frame_count":156,"size":136204,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":60,"height":64,"frame_count":156,"size":179422,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":60,"height":64,"frame_count":156,"size":314350,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":90,"height":96,"frame_count":156,"size":308820,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":90,"height":96,"frame_count":156,"size":526006,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":120,"height":128,"frame_count":156,"size":494417,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":120,"height":128,"frame_count":156,"size":601682,"format":"WEBP"}]}}},{"id":"6191a85dd34608492cc34fbf","name":"Wavegers","flags":0,"timestamp":1690748508029,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"6191a85dd34608492cc34fbf","name":"Wavegers","flags":0,"tags":["gers","gladgers","wave"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae57c45d3fdae58382fd51","username":"captaincrohny","display_name":"CaptainCrohny","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/c1ee9c3a-9151-445c-891e-371c7fa67b2a-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6191a85dd34608492cc34fbf","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":6,"size":3888,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":6,"size":5326,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":6,"size":6763,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":6,"size":11936,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":6,"size":10603,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":6,"size":19566,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":6,"size":16074,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":6,"size":23580,"format":"WEBP"}]}}},{"id":"61253a326cb0b55c059f7f88","name":"noxSorry","flags":0,"timestamp":1690811316240,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"61253a326cb0b55c059f7f88","name":"noxSorry","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60bd5159a8c00a202bf7425f","username":"a_t_m_0_s","display_name":"A_T_M_0_S","avatar_url":"//cdn.7tv.app/user/60bd5159a8c00a202bf7425f/av_6482d40390f619d9af6922f1/3x.webp","style":{"color":849892095},"roles":["60724f65e93d828bf8858789","6076a86b09a4c63a38ebe801","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61253a326cb0b55c059f7f88","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":17,"size":6282,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":17,"size":15056,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":17,"size":14314,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":17,"size":34404,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":17,"size":25188,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":17,"size":56448,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":17,"size":39573,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":17,"size":68218,"format":"WEBP"}]}}},{"id":"64c75391e5effffa3c774df9","name":"poglinspotted","flags":0,"timestamp":1690845661080,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"64c75391e5effffa3c774df9","name":"thatAss","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"63c835cefc866ebbc80af66f","username":"sjonkonnerie","display_name":"sJoNkOnNeRiE","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38547da9-7866-4d0d-9816-5f9c00e68cc5-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64c75391e5effffa3c774df9","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":46,"height":32,"frame_count":1,"size":1707,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":46,"height":32,"frame_count":1,"size":2164,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":92,"height":64,"frame_count":1,"size":3372,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":92,"height":64,"frame_count":1,"size":6786,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":138,"height":96,"frame_count":1,"size":5401,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":138,"height":96,"frame_count":1,"size":13320,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":184,"height":128,"frame_count":1,"size":7531,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":184,"height":128,"frame_count":1,"size":20970,"format":"WEBP"}]}}},{"id":"6402cc9dd283ec401319df1d","name":"mrah","flags":0,"timestamp":1690882199977,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"6402cc9dd283ec401319df1d","name":"mrah","flags":0,"tags":["plonk","ewpert","robert","meow","plink","mrah"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"62ebda39f2286f265273d001","username":"dn9n","display_name":"dn9n","avatar_url":"//cdn.7tv.app/user/62ebda39f2286f265273d001/av_653a15e60332d91f335d555a/3x.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6402cc9dd283ec401319df1d","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":34,"height":32,"frame_count":91,"size":25787,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":34,"height":32,"frame_count":91,"size":42342,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":68,"height":64,"frame_count":91,"size":62024,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":68,"height":64,"frame_count":91,"size":100552,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":102,"height":96,"frame_count":91,"size":99774,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":102,"height":96,"frame_count":91,"size":157750,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":136,"height":128,"frame_count":91,"size":143014,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":136,"height":128,"frame_count":91,"size":212436,"format":"WEBP"}]}}},{"id":"64b55da3c431f7a72656d0e7","name":"MYNM","flags":0,"timestamp":1690889982534,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"64b55da3c431f7a72656d0e7","name":"MYNM","flags":0,"tags":["cursed","stare","nymn","mynm","158cm","whymypeepeehard"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"61648644ea8ead16589dc5b8","username":"floxd","display_name":"FloxD","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/10fce1d7-2d5d-47b8-b0c8-b2d12c1116cb-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64b55da3c431f7a72656d0e7","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":970,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1804,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1434,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":4808,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":1910,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":9446,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":2265,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":14242,"format":"WEBP"}]}}},{"id":"61ee8a791a1b2a6e73254219","name":"!when","flags":0,"timestamp":1691079746570,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"61ee8a791a1b2a6e73254219","name":"Waiting","flags":0,"tags":["bean"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"614aebb05765a48b24d954e0","username":"mafiadanger","display_name":"MafiaDanger","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61ee8a791a1b2a6e73254219","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":72,"size":16999,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":72,"size":51834,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":72,"size":57426,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":72,"size":148350,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":72,"size":115176,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":72,"size":263022,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":72,"size":219702,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":72,"size":240882,"format":"WEBP"}]}}},{"id":"640608a13c3750a0c8281d3f","name":"!sr","flags":0,"timestamp":1691079780830,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"640608a13c3750a0c8281d3f","name":"!sr","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60bb7e5f918e96162caccdfd","username":"sennin_mady","display_name":"Sennin_Mady","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/cc2ba427-f8e2-40bd-985a-ad95d487080b-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/640608a13c3750a0c8281d3f","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1452,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1992,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2807,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":6072,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4211,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":11428,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":6044,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":18052,"format":"WEBP"}]}}},{"id":"64cbd85af014912c29bc1660","name":"docCum","flags":0,"timestamp":1691093148162,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"64cbd85af014912c29bc1660","name":"docCooming","flags":0,"tags":["coomer","2time","doccoomer","ambatukam","doc"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"61f0445a170449495610d147","username":"winniethepeepo","display_name":"WinnieThepeepo","avatar_url":"//cdn.7tv.app/pp/61f0445a170449495610d147/850f578eb4d147a99064f2eca63c22d1","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64cbd85af014912c29bc1660","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":43,"height":32,"frame_count":70,"size":33747,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":43,"height":32,"frame_count":70,"size":51936,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":86,"height":64,"frame_count":70,"size":71631,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":86,"height":64,"frame_count":70,"size":98902,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":129,"height":96,"frame_count":70,"size":114089,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":129,"height":96,"frame_count":70,"size":154200,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":172,"height":128,"frame_count":70,"size":154407,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":172,"height":128,"frame_count":70,"size":201104,"format":"WEBP"}]}}},{"id":"61db9dc227a4f6d6544ea84d","name":"HENRY","flags":0,"timestamp":1691094671758,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"61db9dc227a4f6d6544ea84d","name":"HENRY","flags":0,"tags":["henry","stare"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"6185f323f1ae15abc7ec0e51","username":"tw00fspades","display_name":"tw00fspades","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/fcfe3ed3-9940-4d44-af14-935e1d9cd55c-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61db9dc227a4f6d6544ea84d","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":30,"height":32,"frame_count":1,"size":1290,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":30,"height":32,"frame_count":1,"size":978,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":60,"height":64,"frame_count":1,"size":2264,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":60,"height":64,"frame_count":1,"size":2152,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":90,"height":96,"frame_count":1,"size":3252,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":90,"height":96,"frame_count":1,"size":3558,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":120,"height":128,"frame_count":1,"size":4154,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":120,"height":128,"frame_count":1,"size":4970,"format":"WEBP"}]}}},{"id":"64ccf5dbfdd01a0862695fda","name":"TwoDumbasses","flags":0,"timestamp":1691154218795,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"64ccf5dbfdd01a0862695fda","name":"TwoDumbasses","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae409daee2aa55383ebb4b","username":"tolatos","display_name":"tolatos","avatar_url":"//cdn.7tv.app/user/60ae409daee2aa55383ebb4b/av_657b75e8b0d945ef35823739/3x.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64ccf5dbfdd01a0862695fda","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":61,"height":32,"frame_count":1,"size":1624,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":61,"height":32,"frame_count":1,"size":3514,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":122,"height":64,"frame_count":1,"size":3278,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":122,"height":64,"frame_count":1,"size":11156,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":183,"height":96,"frame_count":1,"size":4986,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":183,"height":96,"frame_count":1,"size":21178,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":244,"height":128,"frame_count":1,"size":6976,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":244,"height":128,"frame_count":1,"size":35054,"format":"WEBP"}]}}},{"id":"6442e5be2be860de107c1f37","name":"IVEGONEPASTHEPOINTOFINSANITY","flags":0,"timestamp":1691169545240,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"6442e5be2be860de107c1f37","name":"IVEGONEPASTHEPOINTOFINSANITY","flags":0,"tags":["goinginsane","malding","tilt","copiumdepleted","outofcope","losingit"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"62ce20e8fa3d15d7e44e6dfd","username":"2syndras1cup","display_name":"2Syndras1Cup","avatar_url":"//cdn.7tv.app/user/62ce20e8fa3d15d7e44e6dfd/av_6476c7d2804ca09ebf233501/3x_static.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6442e5be2be860de107c1f37","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":64,"size":50644,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":64,"size":44856,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":64,"size":133126,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":64,"size":92136,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":64,"size":231753,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":64,"size":152296,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":64,"size":389093,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":64,"size":215614,"format":"WEBP"}]}}},{"id":"6415b7e1220f8400b8784fd6","name":"OFC","flags":0,"timestamp":1691170549330,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"6415b7e1220f8400b8784fd6","name":"OFC","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"63dd2d98a039b6522cdd59a5","username":"rawbh","display_name":"RawbH","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/rawbh-profile_image-20ffd0839bd5b426-70x70.jpeg","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6415b7e1220f8400b8784fd6","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1338,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1440,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2497,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":4030,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4085,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":7796,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5312,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":12582,"format":"WEBP"}]}}},{"id":"64cd3f7517cab280bab4cdf3","name":"guh","flags":0,"timestamp":1691172764946,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"64cd3f7517cab280bab4cdf3","name":"guh","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae434b5d3fdae58382926a","username":"ayyybubu","display_name":"ayyybubu","avatar_url":"//cdn.7tv.app/user/60ae434b5d3fdae58382926a/av_636fa88735a18fd8b17e7399/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","62d86a8419fdcf401421c5ae","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64cd3f7517cab280bab4cdf3","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":128,"size":22476,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":118,"size":44614,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":128,"size":41107,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":126,"size":101554,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":128,"size":66506,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":128,"size":171002,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":128,"size":92323,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":128,"size":246714,"format":"WEBP"}]}}},{"id":"60aeabbef39a7552b6b2cf19","name":"POOTERS","flags":0,"timestamp":1691178160153,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60aeabbef39a7552b6b2cf19","name":"POOTERS","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60f88f6331ba6ae6229fb346","username":"damunnyhoney","display_name":"damunnyhoney","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/94058f51-2bd7-4a34-83bb-dae1cf5bd244-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60aeabbef39a7552b6b2cf19","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":25,"size":9663,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":25,"size":21066,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":25,"size":17138,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":25,"size":43430,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":25,"size":29091,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":25,"size":70154,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":25,"size":40316,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":25,"size":76472,"format":"WEBP"}]}}},{"id":"634ec57e51c7bffc93bfeab1","name":"cumin","flags":0,"timestamp":1691180343205,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"634ec57e51c7bffc93bfeab1","name":"cumin","flags":0,"tags":["henryskitchen","spice","cumin","henry"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"62807168bfe678d307584c89","username":"dombeef","display_name":"dombeef","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/07ef4f79-c873-4aca-95f0-0775ff6107b0-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/634ec57e51c7bffc93bfeab1","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1486,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1982,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2877,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":4858,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4489,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":8882,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":6251,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":13378,"format":"WEBP"}]}}},{"id":"64ce70c1e1edc2e9f4fc1a9c","name":"rentfree","flags":0,"timestamp":1691250909015,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"64ce70c1e1edc2e9f4fc1a9c","name":"rentfree","flags":0,"tags":["baldursgate","bg3","baldursgate3"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae3e3eb2ecb01505346ae9","username":"fawcan","display_name":"Fawcan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38051bdb-83c6-4716-95e0-731462e02b45-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64ce70c1e1edc2e9f4fc1a9c","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":81,"size":25491,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":81,"size":28096,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":81,"size":57112,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":81,"size":63716,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":81,"size":92863,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":81,"size":104300,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":81,"size":134328,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":81,"size":151992,"format":"WEBP"}]}}},{"id":"64d0dd6e10b8987ddc7c4a31","name":"nuh","flags":0,"timestamp":1691409874723,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"64d0dd6e10b8987ddc7c4a31","name":"nuh","flags":0,"tags":["nymn","forsen","buh"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"6131d9de492022af58394453","username":"jerrythedoctor","display_name":"JerryTheDoctor","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/a4a6f511-4bc7-466b-a73d-f9dc242bdef9-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64d0dd6e10b8987ddc7c4a31","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":48,"height":32,"frame_count":76,"size":17624,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":48,"height":32,"frame_count":76,"size":53368,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":96,"height":64,"frame_count":76,"size":37743,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":96,"height":64,"frame_count":76,"size":130070,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":144,"height":96,"frame_count":76,"size":63945,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":144,"height":96,"frame_count":76,"size":209210,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":192,"height":128,"frame_count":76,"size":118471,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":192,"height":128,"frame_count":76,"size":297108,"format":"WEBP"}]}}},{"id":"64d1852517be4dac583385cb","name":"nimeDance","flags":0,"timestamp":1691452820219,"actor_id":"60ae518c0e35477634c151f1","data":{"id":"64d1852517be4dac583385cb","name":"nimeDance","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae518c0e35477634c151f1","username":"fabulouspotato69","display_name":"FabulousPotato69","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64d1852517be4dac583385cb","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":53,"height":32,"frame_count":30,"size":19098,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":53,"height":32,"frame_count":30,"size":26610,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":106,"height":64,"frame_count":30,"size":36570,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":106,"height":64,"frame_count":30,"size":56084,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":159,"height":96,"frame_count":30,"size":61867,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":159,"height":96,"frame_count":30,"size":88766,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":212,"height":128,"frame_count":30,"size":98205,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":212,"height":128,"frame_count":30,"size":121126,"format":"WEBP"}]}}},{"id":"64d18c2aa47f9006a3bfc46d","name":"DonkAndDonker","flags":0,"timestamp":1691489530478,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"64d18c2aa47f9006a3bfc46d","name":"DonkAndDonker","flags":0,"tags":["rogue","donk","feelsdonkman","darkanddarker"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ff054ffbd646ea3b221dc9","username":"tunari__","display_name":"Tunari__","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/bc530a7a-e04d-4765-a662-bb3efde482e2-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64d18c2aa47f9006a3bfc46d","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":43,"height":32,"frame_count":18,"size":5905,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":43,"height":32,"frame_count":18,"size":8010,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":86,"height":64,"frame_count":18,"size":10946,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":86,"height":64,"frame_count":18,"size":16676,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":129,"height":96,"frame_count":18,"size":16210,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":129,"height":96,"frame_count":18,"size":25522,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":172,"height":128,"frame_count":18,"size":20504,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":172,"height":128,"frame_count":18,"size":42350,"format":"WEBP"}]}}},{"id":"64d28b7cd07ad5c85a4c0b88","name":"yabbePls","flags":0,"timestamp":1691526426017,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"64d28b7cd07ad5c85a4c0b88","name":"yabbePls","flags":0,"lifecycle":3,"state":["NO_PERSONAL","LISTED"],"listed":true,"animated":true,"owner":{"id":"6159fb1a93686fbfe7fc1c38","username":"yabbe","display_name":"Yabbe","avatar_url":"//cdn.7tv.app/user/6159fb1a93686fbfe7fc1c38/av_634b3ae0d6ba45f7f103a8ca/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64d28b7cd07ad5c85a4c0b88","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":204,"size":112205,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":204,"size":157606,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":204,"size":246782,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":204,"size":297072,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":204,"size":396133,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":204,"size":429894,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":204,"size":560198,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":204,"size":553740,"format":"WEBP"}]}}},{"id":"611663359bf574f1fded76e4","name":"BOTHA","flags":0,"timestamp":1691673830586,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"611663359bf574f1fded76e4","name":"BOTHA","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"6116631547935f36575c79c2","username":"gloriousbeard","display_name":"GloriousBeard","avatar_url":"//cdn.7tv.app/pp/6116631547935f36575c79c2/f30871a43ca247ecbabb7623cef8d302","style":{"color":849892095},"roles":["60724f65e93d828bf8858789","6076a86b09a4c63a38ebe801","62d86a8419fdcf401421c5ae","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/611663359bf574f1fded76e4","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":63,"size":18666,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":63,"size":60664,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":63,"size":43239,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":63,"size":136214,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":63,"size":79634,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":63,"size":231960,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":63,"size":138888,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":63,"size":278148,"format":"WEBP"}]}}},{"id":"61b633d98ffada6c4bafab74","name":"YURRY","flags":0,"timestamp":1691680118413,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"61b633d98ffada6c4bafab74","name":"YURRY","flags":0,"tags":["yabbe","furry"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60aeab21229664e8663345dd","username":"barricade0_","display_name":"BARRICADE0_","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/948ce321-c188-4c7a-90c0-16169e190ac2-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61b633d98ffada6c4bafab74","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":44,"height":32,"frame_count":1,"size":1935,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":44,"height":32,"frame_count":1,"size":1358,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":88,"height":64,"frame_count":1,"size":4496,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":88,"height":64,"frame_count":1,"size":3910,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":132,"height":96,"frame_count":1,"size":7404,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":132,"height":96,"frame_count":1,"size":7174,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":176,"height":128,"frame_count":1,"size":10968,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":176,"height":128,"frame_count":1,"size":10988,"format":"WEBP"}]}}},{"id":"61e62b39095be332e347deaa","name":"TouchGrass","flags":1,"timestamp":1691680146936,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"61e62b39095be332e347deaa","name":"TouchGrass","flags":256,"tags":["handrub","outside","petpet","feelsgrassman","zerowidth"],"lifecycle":3,"state":["PERSONAL","LISTED"],"listed":true,"animated":true,"owner":{"id":"60f39e8bc07d1ac193652def","username":"shmovy","display_name":"Shmovy","avatar_url":"//cdn.7tv.app/user/60f39e8bc07d1ac193652def/av_63a4e84f5c2aba9b3b60bf46/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61e62b39095be332e347deaa","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":54,"size":14115,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":54,"size":29358,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":54,"size":33812,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":54,"size":72544,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":54,"size":55463,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":54,"size":124094,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":54,"size":96739,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":54,"size":165562,"format":"WEBP"}]}}},{"id":"63aceacc7fe47e928e3e6f44","name":"mods","flags":0,"timestamp":1691858683457,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"63aceacc7fe47e928e3e6f44","name":"mods","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"63a761869968b7c4e6ca6f96","username":"bi__","display_name":"bi__","avatar_url":"//cdn.7tv.app/user/63a761869968b7c4e6ca6f96/av_642b2ce2c82a06d25a4e4766/3x_static.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63aceacc7fe47e928e3e6f44","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":67,"height":32,"frame_count":153,"size":40241,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":67,"height":32,"frame_count":151,"size":96302,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":134,"height":64,"frame_count":153,"size":92981,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":134,"height":64,"frame_count":153,"size":252086,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":201,"height":96,"frame_count":153,"size":172872,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":201,"height":96,"frame_count":153,"size":422284,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":268,"height":128,"frame_count":153,"size":250957,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":268,"height":128,"frame_count":153,"size":598316,"format":"WEBP"}]}}},{"id":"64d7ef64b0552f709e06ded5","name":"yap","flags":0,"timestamp":1691873425709,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"64d7ef64b0552f709e06ded5","name":"yap","flags":0,"tags":["kripp","anythingelse","yapping","yappp"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"613bdc8feef63902454a4f2d","username":"maxdaxx","display_name":"maxdaxx","avatar_url":"//cdn.7tv.app/user/613bdc8feef63902454a4f2d/av_63b31dc35aa77eca61ec84b8/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64d7ef64b0552f709e06ded5","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":39,"height":32,"frame_count":253,"size":46364,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":39,"height":32,"frame_count":253,"size":198444,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":78,"height":64,"frame_count":253,"size":107390,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":78,"height":64,"frame_count":253,"size":426690,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":117,"height":96,"frame_count":253,"size":194281,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":117,"height":96,"frame_count":253,"size":679094,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":156,"height":128,"frame_count":253,"size":354566,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":156,"height":128,"frame_count":253,"size":924446,"format":"WEBP"}]}}},{"id":"647bba24628540685215540b","name":"$fish","flags":0,"timestamp":1691909468429,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"647bba24628540685215540b","name":"$fish","flags":0,"tags":["bot","fishing","supibot","fish","osrs","runescape"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae434b5d3fdae58382926a","username":"ayyybubu","display_name":"ayyybubu","avatar_url":"//cdn.7tv.app/user/60ae434b5d3fdae58382926a/av_636fa88735a18fd8b17e7399/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","62d86a8419fdcf401421c5ae","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/647bba24628540685215540b","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":42,"height":32,"frame_count":120,"size":18693,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":42,"height":32,"frame_count":120,"size":34674,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":84,"height":64,"frame_count":120,"size":35716,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":84,"height":64,"frame_count":120,"size":64698,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":126,"height":96,"frame_count":120,"size":55094,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":126,"height":96,"frame_count":120,"size":93280,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":168,"height":128,"frame_count":120,"size":78531,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":168,"height":128,"frame_count":120,"size":124658,"format":"WEBP"}]}}},{"id":"64d9337e28901aea7c367934","name":"Kissamoney","flags":0,"timestamp":1691957670701,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"64d9337e28901aea7c367934","name":"Kissamoney","flags":0,"tags":["nymn","kissahomie","money","nime","krabs"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"6131dab2af9287c4eb609268","username":"vicneeel","display_name":"vicneeel","avatar_url":"//cdn.7tv.app/user/6131dab2af9287c4eb609268/av_6520576332b1db5b90ef6b24/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64d9337e28901aea7c367934","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":66,"height":32,"frame_count":15,"size":16347,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":66,"height":32,"frame_count":15,"size":22238,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":132,"height":64,"frame_count":15,"size":37095,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":132,"height":64,"frame_count":15,"size":51720,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":198,"height":96,"frame_count":15,"size":60700,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":198,"height":96,"frame_count":15,"size":85674,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":264,"height":128,"frame_count":15,"size":102133,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":264,"height":128,"frame_count":15,"size":115826,"format":"WEBP"}]}}},{"id":"605391a99d9e96000d244fd0","name":"Okayge+1","flags":0,"timestamp":1692036877815,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"605391a99d9e96000d244fd0","name":"Okayge","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60538de8b4d31e459fe6f49f","username":"moonmoon_has_tiny_teeth","display_name":"moonmoon_has_tiny_teeth","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/49d3f80c-d15a-467c-a644-ed28f8c69806-profile_image-70x70.jpg","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/605391a99d9e96000d244fd0","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1342,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":998,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2530,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2516,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3717,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4314,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5055,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":6404,"format":"WEBP"}]}}},{"id":"64db5ab6549eec84e19a6932","name":"Pillowfort","flags":0,"timestamp":1692097238188,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"64db5ab6549eec84e19a6932","name":"Pillowfort","flags":0,"tags":["pillow","nogirls","fort","moonstrobes","pepe","moonlightstrobes"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae3e3eb2ecb01505346ae9","username":"fawcan","display_name":"Fawcan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38051bdb-83c6-4716-95e0-731462e02b45-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64db5ab6549eec84e19a6932","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":68,"height":32,"frame_count":1,"size":2506,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":68,"height":32,"frame_count":1,"size":2876,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":136,"height":64,"frame_count":1,"size":5691,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":136,"height":64,"frame_count":1,"size":8046,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":204,"height":96,"frame_count":1,"size":9261,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":204,"height":96,"frame_count":1,"size":14336,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":272,"height":128,"frame_count":1,"size":12819,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":272,"height":128,"frame_count":1,"size":22670,"format":"WEBP"}]}}},{"id":"64da270fabda45849ab57a5e","name":"shortcut","flags":0,"timestamp":1692101682105,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"64da270fabda45849ab57a5e","name":"shortcut","flags":0,"tags":["enter","nymn","apollo","cat"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"63fb9249a27fda24e806d1cc","username":"abithappy","display_name":"abithappy","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/f7a46513-21a8-46ff-8ef2-d388dc069e8c-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64da270fabda45849ab57a5e","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":46,"height":32,"frame_count":185,"size":29624,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":46,"height":32,"frame_count":169,"size":54096,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":92,"height":64,"frame_count":185,"size":65476,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":92,"height":64,"frame_count":185,"size":204158,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":138,"height":96,"frame_count":185,"size":118014,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":138,"height":96,"frame_count":185,"size":490700,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":184,"height":128,"frame_count":185,"size":162810,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":184,"height":128,"frame_count":185,"size":802644,"format":"WEBP"}]}}},{"id":"64dbe4259dce677b74f89e52","name":"tickpolloArrive","flags":0,"timestamp":1692132530112,"actor_id":"60ae8fc0ea50f43c9e3ae255","data":{"id":"64dbe4259dce677b74f89e52","name":"tickpolloArrive","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae8fc0ea50f43c9e3ae255","username":"agenttud","display_name":"agenttud","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/273db808-d42f-4dab-9b39-9780ef2777b0-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64dbe4259dce677b74f89e52","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":43,"height":32,"frame_count":104,"size":37424,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":43,"height":32,"frame_count":104,"size":52366,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":86,"height":64,"frame_count":104,"size":79784,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":86,"height":64,"frame_count":104,"size":104152,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":129,"height":96,"frame_count":104,"size":129036,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":129,"height":96,"frame_count":104,"size":159172,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":172,"height":128,"frame_count":104,"size":173453,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":172,"height":128,"frame_count":104,"size":225866,"format":"WEBP"}]}}},{"id":"61dc53b457c70f633ebd86fb","name":"Tex","flags":1,"timestamp":1692139647734,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"61dc53b457c70f633ebd86fb","name":"Tex","flags":256,"tags":["texime","erobb221"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"61c7196612987d64d6ae7fc6","username":"quaxi13","display_name":"Quaxi13","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/39135e01-6d49-40ad-afe5-2973e6176848-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61dc53b457c70f633ebd86fb","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1022,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1403,"format":"AVIF"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2483,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2530,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3871,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4522,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5248,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":6792,"format":"WEBP"}]}}},{"id":"60aed84b423a803ccafdd4b4","name":"forsenCoomer","flags":0,"timestamp":1692273754155,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"60aed84b423a803ccafdd4b4","name":"forsenCoomer","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae66e086fc40d4887c81cb","username":"etx4n","display_name":"Etx4n","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/62a8869a-ae56-4013-9cbf-31fcd74ede09-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60aed84b423a803ccafdd4b4","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":97,"size":26772,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":97,"size":72112,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":97,"size":55639,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":97,"size":143332,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":97,"size":95019,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":97,"size":224454,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":97,"size":138168,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":97,"size":237902,"format":"WEBP"}]}}},{"id":"60afafe760e24df01a1172b6","name":"NymNCube","flags":0,"timestamp":1692273991274,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"60afafe760e24df01a1172b6","name":"NymNCube","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae81ff0bf2ee96aea05247","username":"snortexx","display_name":"snortexx","avatar_url":"//cdn.7tv.app/pp/60ae81ff0bf2ee96aea05247/183b9b6ab7624a53966fb782ec0963e0","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60afafe760e24df01a1172b6","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":98,"size":56320,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":98,"size":88150,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":98,"size":135827,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":98,"size":195670,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":98,"size":221126,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":98,"size":326728,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":98,"size":344824,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":98,"size":378176,"format":"WEBP"}]}}},{"id":"610d2fafd53540d5aad11594","name":"Alien360","flags":0,"timestamp":1692281499972,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"610d2fafd53540d5aad11594","name":"AlienPls","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60a24668146e092ea1e0f29b","username":"tichus_1273","display_name":"Tichus_1273","avatar_url":"//cdn.7tv.app/user/60a24668146e092ea1e0f29b/av_656cda4663b9857320914e09/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/610d2fafd53540d5aad11594","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":160,"size":91977,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":160,"size":148172,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":160,"size":225717,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":160,"size":356298,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":160,"size":401172,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":160,"size":637662,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":160,"size":613682,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":160,"size":854142,"format":"WEBP"}]}}},{"id":"6140b9a97b14fdf700b8e101","name":"Talk0","flags":1,"timestamp":1692340650955,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"6140b9a97b14fdf700b8e101","name":"Talk","flags":256,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60c1708f6cf40799f98c392a","username":"aroopc","display_name":"ArooPC","avatar_url":"//cdn.7tv.app/pp/60c1708f6cf40799f98c392a/d78c02f77fcf4b8e9d8b4452bea5b2ad","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6140b9a97b14fdf700b8e101","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":16,"size":3711,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":16,"size":2832,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":16,"size":5162,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":16,"size":4430,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":16,"size":7044,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":16,"size":7054,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":16,"size":7772,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":16,"size":7744,"format":"WEBP"}]}}},{"id":"610eb19d3f3e99ddb462710f","name":"peepoConfused","flags":0,"timestamp":1692615543164,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"610eb19d3f3e99ddb462710f","name":"peepoConfused","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60f244dfc07d1ac193c6314e","username":"kushala_0001","display_name":"Kushala_0001","avatar_url":"//cdn.7tv.app/user/60f244dfc07d1ac193c6314e/av_640be52567f0badff748097e/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/610eb19d3f3e99ddb462710f","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":16,"size":7521,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":16,"size":13396,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":16,"size":16703,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":16,"size":29854,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":16,"size":28081,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":16,"size":51390,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":16,"size":47034,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":16,"size":62462,"format":"WEBP"}]}}},{"id":"60b955e2f09ea88072efbacd","name":"PagRubiksCube","flags":0,"timestamp":1692629002795,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"60b955e2f09ea88072efbacd","name":"PagRubiksCube","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60b3fdafdb203df7cd1efca7","username":"blu3smok3","display_name":"Blu3Smok3","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/caa46d28-d9dc-4bb8-a349-f75c428262e3-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60b955e2f09ea88072efbacd","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":100,"size":42767,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":100,"size":91176,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":100,"size":90811,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":100,"size":196030,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":100,"size":151620,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":100,"size":310840,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":100,"size":219269,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":100,"size":329858,"format":"WEBP"}]}}},{"id":"61bf539792cc54658156334d","name":"hacking0","flags":1,"timestamp":1692705897576,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"61bf539792cc54658156334d","name":"HACKERING","flags":256,"tags":["typing","hacking","0width","zythemotes"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"61a3dfabffa9aba101bb9716","username":"zyth_dr","display_name":"Zyth_Dr","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/zyth_dr-profile_image-b82b9584dae623a2-70x70.jpeg","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61bf539792cc54658156334d","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":11,"size":4156,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":11,"size":6128,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":11,"size":7593,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":11,"size":14786,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":11,"size":12166,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":11,"size":26926,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":11,"size":19646,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":11,"size":35206,"format":"WEBP"}]}}},{"id":"60ae9863ac03cad607437fae","name":"pokiBurrito","flags":0,"timestamp":1692711159433,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"60ae9863ac03cad607437fae","name":"pokiBurrito","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60a536d1ac08622846bced71","username":"marcfryd_0","display_name":"marcfryd_0","avatar_url":"//cdn.7tv.app/user/60a536d1ac08622846bced71/av_63537c8f28e6aaaea2bb599e/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","631ef5ea03e9beb96f849a7e","6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60ae9863ac03cad607437fae","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":25,"size":13208,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":25,"size":22482,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":25,"size":49140,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":25,"size":29372,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":25,"size":81108,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":25,"size":46956,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":25,"size":68997,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":25,"size":83500,"format":"WEBP"}]}}},{"id":"64a94b0d2d10dc644f50f279","name":"POGASS","flags":0,"timestamp":1692716907851,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"64a94b0d2d10dc644f50f279","name":"POGASS","flags":0,"tags":["lirik","lirikfr","pog","sussy"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"61b3c83015b3ff4a5bba5c1a","username":"weelqa","display_name":"Weelqa","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/d3d77294-1a98-46ff-99fe-508c33c58e84-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64a94b0d2d10dc644f50f279","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":42,"height":32,"frame_count":11,"size":9201,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":42,"height":32,"frame_count":11,"size":7678,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":84,"height":64,"frame_count":11,"size":16869,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":84,"height":64,"frame_count":11,"size":15832,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":126,"height":96,"frame_count":11,"size":26295,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":126,"height":96,"frame_count":11,"size":24758,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":168,"height":128,"frame_count":11,"size":29011,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":168,"height":128,"frame_count":11,"size":31072,"format":"WEBP"}]}}},{"id":"60aead003c27a8b79c49566e","name":"arnoldProceed","flags":0,"timestamp":1692720563752,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"60aead003c27a8b79c49566e","name":"arnoldProceed","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae99954b1ea4526d8ac75b","username":"evilmessy","display_name":"evilmessy","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/aaec06da-90ff-46e4-9dfd-ec57221cd405-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60aead003c27a8b79c49566e","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1390,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1082,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2627,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2780,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3962,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4566,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5257,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":5598,"format":"WEBP"}]}}},{"id":"64e61526e59df5a070a02688","name":"nimenniversary","flags":0,"timestamp":1692800320143,"actor_id":"60ae8fc0ea50f43c9e3ae255","data":{"id":"64e61526e59df5a070a02688","name":"nimeAnniversary","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae8fc0ea50f43c9e3ae255","username":"agenttud","display_name":"agenttud","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/273db808-d42f-4dab-9b39-9780ef2777b0-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64e61526e59df5a070a02688","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1444,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1820,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2810,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":5294,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4202,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":9958,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5410,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":15024,"format":"WEBP"}]}}},{"id":"62ae2faaa39450ce025aca62","name":"omgE","flags":0,"timestamp":1692876433662,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"62ae2faaa39450ce025aca62","name":"omE","flags":0,"tags":["ome","poki","omegalul","lulw","kekw","pokimane"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60b1cefbfdd2d7d7bdd566d6","username":"qdtn","display_name":"QdtN","avatar_url":"//cdn.7tv.app/pp/60b1cefbfdd2d7d7bdd566d6/c954f051a4404463b4bc23377f1d82df","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/62ae2faaa39450ce025aca62","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":844,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1090,"format":"AVIF"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2166,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2286,"format":"WEBP"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":3776,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3261,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4817,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":5880,"format":"WEBP"}]}}},{"id":"64e777ee118d24372670b42c","name":"ApolloPW","flags":0,"timestamp":1692891299007,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"64e777ee118d24372670b42c","name":"ApolloPW","flags":0,"tags":["apollo","nymn","cat","dead"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae409daee2aa55383ebb4b","username":"tolatos","display_name":"tolatos","avatar_url":"//cdn.7tv.app/user/60ae409daee2aa55383ebb4b/av_657b75e8b0d945ef35823739/3x.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64e777ee118d24372670b42c","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":179,"size":24526,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":179,"size":77996,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":179,"size":73016,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":179,"size":165448,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":179,"size":162245,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":179,"size":254894,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":179,"size":328007,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":179,"size":343042,"format":"WEBP"}]}}},{"id":"60aeab8df6a2c3b332d21139","name":"FloppaL","flags":0,"timestamp":1692895445418,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"60aeab8df6a2c3b332d21139","name":"FloppaL","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae94964b1ea4526d2f7206","username":"nozzlenols","display_name":"NozzleNols","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/nozzlenols-profile_image-dc00b2850378ebb2-70x70.jpeg","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60aeab8df6a2c3b332d21139","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":8,"size":7488,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":8,"size":8174,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":8,"size":14463,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":8,"size":17758,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":8,"size":21877,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":8,"size":28656,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":8,"size":29462,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":8,"size":30390,"format":"WEBP"}]}}},{"id":"6461d58f5070b2cda24f1b3a","name":"JOEVER","flags":0,"timestamp":1692897053188,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"6461d58f5070b2cda24f1b3a","name":"JOEVER","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"6141c6937b14fdf700b8fa63","username":"ninjyte","display_name":"ninjyte","avatar_url":"//static-cdn.jtvnw.net/user-default-pictures-uv/ead5c8b2-a4c9-4724-b1dd-9f00b46cbd3d-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6461d58f5070b2cda24f1b3a","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1267,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1856,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2350,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":5442,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3507,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":10142,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4820,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":16144,"format":"WEBP"}]}}},{"id":"61bbb1b8fba91c72ead7081b","name":"gachiHop","flags":0,"timestamp":1692899358042,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"61bbb1b8fba91c72ead7081b","name":"gachiHop","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60edbe6890b3667a8a3cfb66","username":"flushedjulian","display_name":"flushedjulian","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/ede97175-eb3e-4656-967d-1fc0da391dac-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61bbb1b8fba91c72ead7081b","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":49,"size":16841,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":49,"size":35284,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":49,"size":31970,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":49,"size":81224,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":49,"size":56233,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":49,"size":132036,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":49,"size":95672,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":49,"size":158318,"format":"WEBP"}]}}},{"id":"64e7a6787937d5233b11c700","name":"docNOWAY","flags":0,"timestamp":1692903391074,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"64e7a6787937d5233b11c700","name":"docNOWAY","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"613bdc8feef63902454a4f2d","username":"maxdaxx","display_name":"maxdaxx","avatar_url":"//cdn.7tv.app/user/613bdc8feef63902454a4f2d/av_63b31dc35aa77eca61ec84b8/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64e7a6787937d5233b11c700","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":49,"height":32,"frame_count":62,"size":28374,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":49,"height":32,"frame_count":62,"size":55772,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":98,"height":64,"frame_count":62,"size":64294,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":98,"height":64,"frame_count":62,"size":106508,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":147,"height":96,"frame_count":62,"size":110218,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":147,"height":96,"frame_count":62,"size":168770,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":196,"height":128,"frame_count":62,"size":181524,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":196,"height":128,"frame_count":62,"size":213134,"format":"WEBP"}]}}},{"id":"64e7b738bad5cade546341da","name":"FURRY","flags":0,"timestamp":1692907535873,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"64e7b738bad5cade546341da","name":"FURRY","flags":0,"lifecycle":3,"state":["NO_PERSONAL","LISTED"],"listed":true,"animated":true,"owner":{"id":"613bdc8feef63902454a4f2d","username":"maxdaxx","display_name":"maxdaxx","avatar_url":"//cdn.7tv.app/user/613bdc8feef63902454a4f2d/av_63b31dc35aa77eca61ec84b8/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64e7b738bad5cade546341da","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":370,"size":106968,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":370,"size":195256,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":370,"size":279029,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":370,"size":477132,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":370,"size":538120,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":370,"size":789370,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":370,"size":869332,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":370,"size":1125388,"format":"WEBP"}]}}},{"id":"64e805fca6bfa513f8a064f6","name":"CaughtTrolling","flags":0,"timestamp":1692957817513,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"64e805fca6bfa513f8a064f6","name":"GotCaughtTrolling","flags":0,"tags":["trump","mugshot","trumpmugshot","myhonestreaction","godblessamerica"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"6144997f962a60904864d7f3","username":"ligmatoes88","display_name":"ligmatoes88","avatar_url":"//cdn.7tv.app/user/6144997f962a60904864d7f3/av_649ce9d9319b6f7746193f1e/3x_static.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64e805fca6bfa513f8a064f6","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":80,"height":32,"frame_count":1,"size":1633,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":80,"height":32,"frame_count":1,"size":3290,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":160,"height":64,"frame_count":1,"size":3110,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":160,"height":64,"frame_count":1,"size":9652,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":240,"height":96,"frame_count":1,"size":4852,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":240,"height":96,"frame_count":1,"size":17904,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":320,"height":128,"frame_count":1,"size":6405,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":320,"height":128,"frame_count":1,"size":27126,"format":"WEBP"}]}}},{"id":"64e90c9a235d13cff971e1dc","name":"nymnUK","flags":0,"timestamp":1692998613502,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"64e90c9a235d13cff971e1dc","name":"nymnUK","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"613bdc8feef63902454a4f2d","username":"maxdaxx","display_name":"maxdaxx","avatar_url":"//cdn.7tv.app/user/613bdc8feef63902454a4f2d/av_63b31dc35aa77eca61ec84b8/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64e90c9a235d13cff971e1dc","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1209,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":2110,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2520,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":7046,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4361,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":13532,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":6316,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":22324,"format":"WEBP"}]}}},{"id":"6431b97987c43af56c004b67","name":"Baldge","flags":0,"timestamp":1693053822132,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"6431b97987c43af56c004b67","name":"Baldge","flags":0,"tags":["feelssadman","feelsbaldman","malding","balding","sadge","feelsbadman"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"62a7977aaa031674e785ccc3","username":"hourshift","display_name":"hourshift","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6431b97987c43af56c004b67","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":42,"height":32,"frame_count":14,"size":5255,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":42,"height":32,"frame_count":14,"size":3690,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":84,"height":64,"frame_count":14,"size":9054,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":84,"height":64,"frame_count":14,"size":6328,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":126,"height":96,"frame_count":14,"size":13286,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":126,"height":96,"frame_count":14,"size":9182,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":168,"height":128,"frame_count":14,"size":17961,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":168,"height":128,"frame_count":14,"size":11480,"format":"WEBP"}]}}},{"id":"64ab423f9675d1e447869c7f","name":"NOSHOT","flags":0,"timestamp":1693053837663,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"64ab423f9675d1e447869c7f","name":"NOSHOT","flags":0,"tags":["nahhh","deadass","aintnoway","skull","skeleton","imdead"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"630e92963cfad7b708c47b1c","username":"reapax","display_name":"Reapax","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/ae51c1bf-b562-4052-8127-2f205c06c828-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64ab423f9675d1e447869c7f","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":81,"size":22576,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":81,"size":39544,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":81,"size":67921,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":81,"size":108962,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":81,"size":128456,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":81,"size":190250,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":81,"size":196765,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":81,"size":271834,"format":"WEBP"}]}}},{"id":"61b0a270e9684edbbc399e6f","name":"KUKLEG","flags":0,"timestamp":1693054171366,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"61b0a270e9684edbbc399e6f","name":"KUKLEG","flags":0,"tags":["kukle","lule","lile","lolw","lulw","okayeg"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"6042162896832ffa786ffe60","username":"kryha5555","display_name":"kryha5555","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/29953655-12f7-43a1-b93a-4d4b943a97ee-profile_image-70x70.jpeg","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61b0a270e9684edbbc399e6f","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1493,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1156,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2892,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":3032,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4798,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4785,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":6897,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":7334,"format":"WEBP"}]}}},{"id":"636edd7db665cca5227a034d","name":"SaguiPls","flags":0,"timestamp":1693054205933,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"636edd7db665cca5227a034d","name":"SaguiPls","flags":0,"tags":["spongepls","geckpls","alienpls","ratjam","catjam"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"633fbc6cb3723828343b7776","username":"apartyy","display_name":"ApartyY","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/fa392215-6b65-4a8b-8c33-cf2ca7b13a67-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/636edd7db665cca5227a034d","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":246,"size":78690,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":246,"size":111178,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":246,"size":154341,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":246,"size":189980,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":246,"size":247951,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":246,"size":264722,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":246,"size":309656,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":246,"size":337486,"format":"WEBP"}]}}},{"id":"61d0c849ce8bd4a59cb8934c","name":"sweat0","flags":1,"timestamp":1693082991667,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"61d0c849ce8bd4a59cb8934c","name":"SweatTime","flags":256,"tags":["pepes","pepe","peepos","peepo"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ca9694f411fca3bb35060f","username":"tajj","display_name":"Tajj","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/8b4b9f3da711dc19-profile_image-70x70.jpeg","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61d0c849ce8bd4a59cb8934c","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":10,"size":4028,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":10,"size":3304,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":10,"size":5394,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":10,"size":6028,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":10,"size":8199,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":10,"size":9118,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":10,"size":8928,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":10,"size":10280,"format":"WEBP"}]}}},{"id":"64ea6c67b4485800629d313a","name":"POWERWASHING","flags":0,"timestamp":1693084835117,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"64ea6c67b4485800629d313a","name":"POWERWASHING","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"613bdc8feef63902454a4f2d","username":"maxdaxx","display_name":"maxdaxx","avatar_url":"//cdn.7tv.app/user/613bdc8feef63902454a4f2d/av_63b31dc35aa77eca61ec84b8/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64ea6c67b4485800629d313a","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":59,"height":32,"frame_count":116,"size":56535,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":59,"height":32,"frame_count":116,"size":113420,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":118,"height":64,"frame_count":116,"size":140780,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":118,"height":64,"frame_count":116,"size":249774,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":177,"height":96,"frame_count":116,"size":264084,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":177,"height":96,"frame_count":116,"size":383424,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":236,"height":128,"frame_count":116,"size":395973,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":236,"height":128,"frame_count":116,"size":532334,"format":"WEBP"}]}}},{"id":"646139c75070b2cda24f0776","name":"SKILLISSUE","flags":0,"timestamp":1693139567462,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"646139c75070b2cda24f0776","name":"SKILLISSUE","flags":0,"tags":["skill","issue","mario"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"626247264f320c1b8cf58d7e","username":"izeeh","display_name":"iZeeh","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/646139c75070b2cda24f0776","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":63,"height":32,"frame_count":1,"size":2446,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":63,"height":32,"frame_count":1,"size":2734,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":126,"height":64,"frame_count":1,"size":4528,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":126,"height":64,"frame_count":1,"size":6360,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":189,"height":96,"frame_count":1,"size":6284,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":189,"height":96,"frame_count":1,"size":11074,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":252,"height":128,"frame_count":1,"size":7775,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":252,"height":128,"frame_count":1,"size":16494,"format":"WEBP"}]}}},{"id":"63c47d2317a417b27a307edf","name":"catDance","flags":0,"timestamp":1693146334821,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"63c47d2317a417b27a307edf","name":"catDance","flags":0,"tags":["cat","maxwell","meme","goodcomplexo","dance","black"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"61a24266e9684edbbc371516","username":"psicopompo","display_name":"Psicopompo","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/5623ada9-ea90-4902-92c0-72ccbae464cf-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63c47d2317a417b27a307edf","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":41,"height":32,"frame_count":22,"size":9946,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":41,"height":32,"frame_count":22,"size":17478,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":82,"height":64,"frame_count":22,"size":18988,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":82,"height":64,"frame_count":22,"size":33372,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":123,"height":96,"frame_count":22,"size":30382,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":123,"height":96,"frame_count":22,"size":48968,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":164,"height":128,"frame_count":22,"size":44868,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":164,"height":128,"frame_count":22,"size":64658,"format":"WEBP"}]}}},{"id":"60996ad0b2344260a28b943d","name":"monkaPickle","flags":0,"timestamp":1693235629408,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60996ad0b2344260a28b943d","name":"monkaPickle","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"6053853cb4d31e459fdaa2dc","username":"laden","display_name":"Laden","avatar_url":"//cdn.7tv.app/pp/6053853cb4d31e459fdaa2dc/a94c67d7736940feb543e42024b740ef","style":{"color":849892095},"roles":["60724f65e93d828bf8858789","6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60996ad0b2344260a28b943d","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1328,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":936,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2549,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2384,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3904,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4120,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5340,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":5778,"format":"WEBP"}]}}},{"id":"629f934a3cfb54ec859bb3ab","name":"PicklePoro","flags":0,"timestamp":1693235642100,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"629f934a3cfb54ec859bb3ab","name":"PicklePoro","flags":0,"tags":["porosad","rick","morty","monkapickle"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60879d10fcf1f9923f6e1573","username":"somso2e","display_name":"Somso2e","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/7291e0ba-abe4-4928-9951-6becee40fb61-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/629f934a3cfb54ec859bb3ab","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1129,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":994,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2055,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2314,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3220,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4062,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4615,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":6224,"format":"WEBP"}]}}},{"id":"64ed1ca2c79b0b0a6f30ccf6","name":"catWait","flags":0,"timestamp":1693261008646,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"64ed1ca2c79b0b0a6f30ccf6","name":"catWait","flags":0,"tags":["wait","cat","waiting"],"lifecycle":3,"state":["NO_PERSONAL","LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae434b5d3fdae58382926a","username":"ayyybubu","display_name":"ayyybubu","avatar_url":"//cdn.7tv.app/user/60ae434b5d3fdae58382926a/av_636fa88735a18fd8b17e7399/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","62d86a8419fdcf401421c5ae","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64ed1ca2c79b0b0a6f30ccf6","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":24,"size":6707,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":24,"size":8696,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":24,"size":10687,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":24,"size":13998,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":24,"size":16669,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":24,"size":21862,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":24,"size":21012,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":24,"size":24692,"format":"WEBP"}]}}},{"id":"64edf8c33b7b939f77575f16","name":"apolloLooking","flags":0,"timestamp":1693317359222,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"64edf8c33b7b939f77575f16","name":"apolloLooking","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60e87e9277b18d5dd343f852","username":"39matt","display_name":"39matt","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/4bef09aa-9fea-4dab-86c9-b0e75352374d-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64edf8c33b7b939f77575f16","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":31,"height":32,"frame_count":1,"size":988,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":31,"height":32,"frame_count":1,"size":1312,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":62,"height":64,"frame_count":1,"size":1461,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":62,"height":64,"frame_count":1,"size":3464,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":93,"height":96,"frame_count":1,"size":2072,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":93,"height":96,"frame_count":1,"size":6434,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":124,"height":128,"frame_count":1,"size":2644,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":124,"height":128,"frame_count":1,"size":8858,"format":"WEBP"}]}}},{"id":"64eb5c4ef71356391c1cc1bf","name":"WAYTOOBUH","flags":0,"timestamp":1693474115853,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"64eb5c4ef71356391c1cc1bf","name":"WAYTOOBUH","flags":0,"tags":["huh","waytoodank","cat","guh","uuh","meow"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"63b72ca0ed6dce0cb863d88a","username":"dx9er","display_name":"dx9er","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/28b3208f-9272-4802-aeba-ffe4664d3070-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64eb5c4ef71356391c1cc1bf","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":45,"height":32,"frame_count":46,"size":16206,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":45,"height":32,"frame_count":46,"size":21582,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":90,"height":64,"frame_count":46,"size":30119,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":90,"height":64,"frame_count":46,"size":47166,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":135,"height":96,"frame_count":46,"size":46040,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":135,"height":96,"frame_count":46,"size":74568,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":180,"height":128,"frame_count":46,"size":65213,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":180,"height":128,"frame_count":46,"size":114488,"format":"WEBP"}]}}},{"id":"63642744b52feba3d73f0781","name":"ChatBelowGetsCoolAir","flags":0,"timestamp":1693482992175,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"63642744b52feba3d73f0781","name":"ChatBelowGetsCoolAir","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"6363ff9871f78620f1e4775a","username":"jjackwv","display_name":"JJackWV","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63642744b52feba3d73f0781","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":93,"height":32,"frame_count":12,"size":12093,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":93,"height":32,"frame_count":12,"size":12820,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":186,"height":64,"frame_count":12,"size":24543,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":186,"height":64,"frame_count":12,"size":23858,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":279,"height":96,"frame_count":12,"size":36474,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":279,"height":96,"frame_count":12,"size":34020,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":372,"height":128,"frame_count":12,"size":46782,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":372,"height":128,"frame_count":12,"size":44970,"format":"WEBP"}]}}},{"id":"64246a2f7a0698a901dab82a","name":"Binoculars","flags":0,"timestamp":1693486809997,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"64246a2f7a0698a901dab82a","name":"SusgeBinoculars","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"6328a1545fb3f93767c33777","username":"destroyerjaan","display_name":"destroyerjaan","avatar_url":"//static-cdn.jtvnw.net/user-default-pictures-uv/de130ab0-def7-11e9-b668-784f43822e80-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64246a2f7a0698a901dab82a","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":59,"height":32,"frame_count":1,"size":2061,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":59,"height":32,"frame_count":1,"size":2970,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":118,"height":64,"frame_count":1,"size":8748,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":118,"height":64,"frame_count":1,"size":4112,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":177,"height":96,"frame_count":1,"size":16384,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":177,"height":96,"frame_count":1,"size":6178,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":236,"height":128,"frame_count":1,"size":8180,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":236,"height":128,"frame_count":1,"size":25458,"format":"WEBP"}]}}},{"id":"63c9080bec685e58d1727476","name":"Bitrate","flags":0,"timestamp":1693489431481,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"63c9080bec685e58d1727476","name":"bitrate","flags":0,"tags":["lagpixelsxqcforsen","plink","catdogcuteblinkstare"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"636946b5707319be8c666317","username":"oo00o0o00o000o0o0o0oo00oo","display_name":"oo00o0o00o000o0o0o0oo00oo","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/a9bcceb7-6017-4eed-a377-5e87fc24b2dc-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63c9080bec685e58d1727476","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":80,"height":32,"frame_count":60,"size":16583,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":80,"height":32,"frame_count":60,"size":33806,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":160,"height":64,"frame_count":60,"size":33826,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":160,"height":64,"frame_count":60,"size":51786,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":240,"height":96,"frame_count":60,"size":56300,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":240,"height":96,"frame_count":60,"size":85042,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":320,"height":128,"frame_count":60,"size":85267,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":320,"height":128,"frame_count":60,"size":94528,"format":"WEBP"}]}}},{"id":"6487b223aaecac0e2a01fae6","name":"ItJustWorks","flags":0,"timestamp":1693746553051,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"6487b223aaecac0e2a01fae6","name":"ItJustWorks","flags":0,"tags":["bethesda","todd","skyrim","starfield","oblivion","fallout"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60b714aaf09ea88072d4b501","username":"terreezus","display_name":"terreezus","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/dcd6d457-25f3-4e70-9911-0f21c274a626-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6487b223aaecac0e2a01fae6","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":34,"height":32,"frame_count":18,"size":8105,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":34,"height":32,"frame_count":18,"size":11646,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":68,"height":64,"frame_count":18,"size":16510,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":68,"height":64,"frame_count":18,"size":23906,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":102,"height":96,"frame_count":18,"size":24523,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":102,"height":96,"frame_count":18,"size":38176,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":136,"height":128,"frame_count":18,"size":34860,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":136,"height":128,"frame_count":18,"size":53874,"format":"WEBP"}]}}},{"id":"629ebf5936b6f962050a9fea","name":"HappyGachaPlayer","flags":0,"timestamp":1693827425505,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"629ebf5936b6f962050a9fea","name":"HappyGachaPlayer","flags":0,"tags":["epicseven","genshin","bluearchive","nikke","lacari","epic7"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"610172a7c515f125009aee59","username":"bryguy_eh","display_name":"Bryguy_eH","avatar_url":"//cdn.7tv.app/pp/610172a7c515f125009aee59/ff52159856934b7083c3be075ca3e948","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/629ebf5936b6f962050a9fea","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":45,"height":32,"frame_count":1,"size":970,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":45,"height":32,"frame_count":1,"size":1188,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":90,"height":64,"frame_count":1,"size":2230,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":90,"height":64,"frame_count":1,"size":2087,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":135,"height":96,"frame_count":1,"size":3970,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":135,"height":96,"frame_count":1,"size":3080,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":180,"height":128,"frame_count":1,"size":5838,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":180,"height":128,"frame_count":1,"size":4303,"format":"AVIF"}]}}},{"id":"64d6927a1d860423beb3ba2c","name":"catshVibe","flags":0,"timestamp":1693830451579,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"64d6927a1d860423beb3ba2c","name":"catshVibe","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"623bd700323d0026d4ee4a11","username":"bre44d","display_name":"bre44d","avatar_url":"//cdn.7tv.app/user/623bd700323d0026d4ee4a11/av_63d05cd36b665ab1f88625c6/3x.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64d6927a1d860423beb3ba2c","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":88,"size":13851,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":88,"size":29354,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":88,"size":26334,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":88,"size":59102,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":88,"size":42949,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":88,"size":88306,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":88,"size":60280,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":88,"size":118576,"format":"WEBP"}]}}},{"id":"64f5d36a8bef73096908d55e","name":"nymnTick","flags":0,"timestamp":1693832175085,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"64f5d36a8bef73096908d55e","name":"nymnTick","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"6421e1e704bb57ba4db5ba41","username":"kryt3k","display_name":"kryt3k","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/7f303b2e-ff21-4eca-b264-26cebdb7fbb8-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64f5d36a8bef73096908d55e","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1192,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1928,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2299,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":5428,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3488,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":10126,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5322,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":15980,"format":"WEBP"}]}}},{"id":"624e576526e9b290e8ff32bb","name":"JUICED","flags":0,"timestamp":1693849732566,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"624e576526e9b290e8ff32bb","name":"JUICED","flags":0,"tags":["xqc","pvc","xqcl","juiced","scary","stare"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"613f4a277b14fdf700b8bafb","username":"victorleporc_","display_name":"victorleporc_","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/ef2c5658-b3ab-499d-8e6e-7ec9cbfe88aa-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/624e576526e9b290e8ff32bb","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":41,"height":32,"frame_count":243,"size":29568,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":41,"height":32,"frame_count":242,"size":161038,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":82,"height":64,"frame_count":243,"size":48520,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":82,"height":64,"frame_count":243,"size":441478,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":123,"height":96,"frame_count":243,"size":75961,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":123,"height":96,"frame_count":243,"size":674088,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":164,"height":128,"frame_count":243,"size":118133,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":164,"height":128,"frame_count":243,"size":989096,"format":"WEBP"}]}}},{"id":"64a84580d93e4373b39b2f09","name":"yippee","flags":0,"timestamp":1693937733918,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"64a84580d93e4373b39b2f09","name":"NymNing","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"613bdc8feef63902454a4f2d","username":"maxdaxx","display_name":"maxdaxx","avatar_url":"//cdn.7tv.app/user/613bdc8feef63902454a4f2d/av_63b31dc35aa77eca61ec84b8/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64a84580d93e4373b39b2f09","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":53,"height":32,"frame_count":80,"size":44565,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":53,"height":32,"frame_count":80,"size":76638,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":106,"height":64,"frame_count":80,"size":98580,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":106,"height":64,"frame_count":80,"size":146566,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":159,"height":96,"frame_count":80,"size":156949,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":159,"height":96,"frame_count":80,"size":211396,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":212,"height":128,"frame_count":80,"size":219885,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":212,"height":128,"frame_count":80,"size":277994,"format":"WEBP"}]}}},{"id":"64f9c55a52ecd4a6aed64d70","name":"GoalpostInMotion","flags":0,"timestamp":1694090670552,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"64f9c55a52ecd4a6aed64d70","name":"GoalpostInMotion","flags":0,"tags":["nymn","forsen","goalpost"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"6131d9de492022af58394453","username":"jerrythedoctor","display_name":"JerryTheDoctor","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/a4a6f511-4bc7-466b-a73d-f9dc242bdef9-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64f9c55a52ecd4a6aed64d70","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":96,"height":25,"frame_count":87,"size":36197,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":96,"height":25,"frame_count":87,"size":55186,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":192,"height":50,"frame_count":87,"size":105548,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":192,"height":50,"frame_count":87,"size":127818,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":288,"height":75,"frame_count":87,"size":246239,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":288,"height":75,"frame_count":87,"size":265480,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":384,"height":100,"frame_count":87,"size":248499,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":384,"height":100,"frame_count":87,"size":265684,"format":"WEBP"}]}}},{"id":"64cd931ed3cf2f1c8cca5264","name":"juh","flags":0,"timestamp":1694171780247,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"64cd931ed3cf2f1c8cca5264","name":"juh","flags":0,"tags":["buh","duh","wuh","juh","guh","cat"],"lifecycle":3,"state":["NO_PERSONAL","LISTED"],"listed":true,"animated":true,"owner":{"id":"62e6b7ea8ef065b8483251b7","username":"smowuw","display_name":"smowuw","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/cdcab8d9-1a74-41c7-8f2b-ffc9725626a9-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64cd931ed3cf2f1c8cca5264","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":171,"size":61752,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":171,"size":76820,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":171,"size":141431,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":171,"size":177010,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":171,"size":240352,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":171,"size":284944,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":171,"size":395492,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":171,"size":422300,"format":"WEBP"}]}}},{"id":"62551c38b0dfc5aeb040d380","name":"potJAM","flags":0,"timestamp":1694172941818,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"62551c38b0dfc5aeb040d380","name":"potJAM","flags":0,"tags":["dance","potfriend","jam"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"62551a265c62fecb05d62562","username":"digatsby","display_name":"DiGatsby","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/5c79cc6f-6eb1-4c59-9d5d-7e05052d8863-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/62551c38b0dfc5aeb040d380","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":16,"size":9705,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":16,"size":13218,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":16,"size":19423,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":16,"size":31954,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":16,"size":31612,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":16,"size":56098,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":16,"size":44059,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":16,"size":78560,"format":"WEBP"}]}}},{"id":"639a4f0f61d9ea4a7faf49e1","name":"lilbro","flags":0,"timestamp":1694191381666,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"639a4f0f61d9ea4a7faf49e1","name":"lilbro","flags":0,"tags":["deadass","lilbro","aintnoway","nahhh","skeleton"],"lifecycle":3,"state":["PERSONAL","LISTED"],"listed":true,"animated":true,"owner":{"id":"613ee122962a60904864354e","username":"johnny3oak","display_name":"Johnny3Oak","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/c307733e-0145-418f-8f29-4f97d60b62f9-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/639a4f0f61d9ea4a7faf49e1","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":107,"size":31799,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":107,"size":38796,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":107,"size":64172,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":107,"size":88406,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":107,"size":103753,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":107,"size":132376,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":107,"size":139845,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":107,"size":175474,"format":"WEBP"}]}}},{"id":"60aef784361b0164e6c32ff6","name":"2bSway","flags":0,"timestamp":1694260904577,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"60aef784361b0164e6c32ff6","name":"2bSway","flags":0,"lifecycle":3,"state":["NO_PERSONAL","LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae6f08df5735e04a70d65d","username":"maayeto","display_name":"Maayeto","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/2d40076c-96e5-49ae-bd83-8676486780b6-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60aef784361b0164e6c32ff6","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":13,"size":8530,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":13,"size":9996,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":13,"size":21710,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":13,"size":16961,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":13,"size":36800,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":13,"size":28264,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":13,"size":38347,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":13,"size":42764,"format":"WEBP"}]}}},{"id":"64d7c889d37116b21c8f7cf3","name":"docReadyToNotL","flags":0,"timestamp":1694263320724,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"64d7c889d37116b21c8f7cf3","name":"docReadyToNotL","flags":0,"tags":["docnotl","drdisrespect","twotime","forsen"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"62a6175afc2bf6a7be2d2b96","username":"lizor","display_name":"Lizor","avatar_url":"//cdn.7tv.app/user/62a6175afc2bf6a7be2d2b96/av_64c0f1709025a813bb83e95d/3x_static.webp","style":{"color":849892095},"roles":["60724f65e93d828bf8858789","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64d7c889d37116b21c8f7cf3","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1034,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1332,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1630,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":3464,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":2361,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":6310,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":2960,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":9430,"format":"WEBP"}]}}},{"id":"632e42e23999124c6544e3bc","name":"Zombey","flags":0,"timestamp":1694360056486,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"632e42e23999124c6544e3bc","name":"Zombey","flags":0,"tags":["okey","nymn","zombie","halloween"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ff0e7a25bb6dd0b03e40f9","username":"saffybop","display_name":"saffybop","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/fd91a409-b82f-474f-a83f-45ab6e4bc3f1-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/632e42e23999124c6544e3bc","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1618,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":2318,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":3476,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":7062,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":5765,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":13396,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":8545,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":21142,"format":"WEBP"}]}}},{"id":"64fe0418516c80510abd7815","name":"nymca","flags":0,"timestamp":1694368853515,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"64fe0418516c80510abd7815","name":"nymca","flags":0,"tags":["nymn"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"63fb9249a27fda24e806d1cc","username":"abithappy","display_name":"abithappy","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/f7a46513-21a8-46ff-8ef2-d388dc069e8c-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64fe0418516c80510abd7815","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":61,"height":32,"frame_count":1,"size":2755,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":61,"height":32,"frame_count":1,"size":4234,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":122,"height":64,"frame_count":1,"size":6780,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":122,"height":64,"frame_count":1,"size":13054,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":183,"height":96,"frame_count":1,"size":11313,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":183,"height":96,"frame_count":1,"size":25218,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":244,"height":128,"frame_count":1,"size":16012,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":244,"height":128,"frame_count":1,"size":40218,"format":"WEBP"}]}}},{"id":"64fe02659d48c7216c051203","name":"NYMNCA","flags":0,"timestamp":1694368878163,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"64fe02659d48c7216c051203","name":"nymca","flags":0,"tags":["nymn"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"63fb9249a27fda24e806d1cc","username":"abithappy","display_name":"abithappy","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/f7a46513-21a8-46ff-8ef2-d388dc069e8c-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64fe02659d48c7216c051203","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":61,"height":32,"frame_count":1,"size":2886,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":61,"height":32,"frame_count":1,"size":4452,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":122,"height":64,"frame_count":1,"size":6985,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":122,"height":64,"frame_count":1,"size":13996,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":183,"height":96,"frame_count":1,"size":11573,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":183,"height":96,"frame_count":1,"size":27214,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":244,"height":128,"frame_count":1,"size":16553,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":244,"height":128,"frame_count":1,"size":43692,"format":"WEBP"}]}}},{"id":"60545a83f2b3b1000d623f8e","name":"KKaper","flags":0,"timestamp":1694430543194,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60545a83f2b3b1000d623f8e","name":"KKaper","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"6042160396832ffa786fbd8a","username":"noctum2k","display_name":"noctum2k","avatar_url":"//cdn.7tv.app/pp/6042160396832ffa786fbd8a/b6787ae92e55401aa651d208be7563e5","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60545a83f2b3b1000d623f8e","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":46,"size":14496,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":46,"size":33144,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":46,"size":22807,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":46,"size":68302,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":46,"size":37926,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":46,"size":112438,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":46,"size":39899,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":46,"size":141064,"format":"WEBP"}]}}},{"id":"612e3150ea1f0fbaa4748b01","name":"peepoRantbutpeepoisnotrantingandweird","flags":0,"timestamp":1694458252973,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"612e3150ea1f0fbaa4748b01","name":"peepoRantbutpeepoisnotrantingandweird","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60f5fe9d15758a7f9ac8b5a2","username":"johhmar","display_name":"Johhmar","avatar_url":"//cdn.7tv.app/user/60f5fe9d15758a7f9ac8b5a2/av_63a7bfa5333fd616dd6b7781/3x_static.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/612e3150ea1f0fbaa4748b01","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1110,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":912,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1917,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2120,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":2835,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":3338,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":3672,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":4610,"format":"WEBP"}]}}},{"id":"637647509656369ac89d45cc","name":"GIGASOY","flags":0,"timestamp":1694518003536,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"637647509656369ac89d45cc","name":"GIGASOY","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"621e04ca14f489808df5bf87","username":"dankjuicer","display_name":"DankJuicer","avatar_url":"//cdn.7tv.app/user/621e04ca14f489808df5bf87/av_658b6684489e267109462da6/3x.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/637647509656369ac89d45cc","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":104,"size":22653,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":78,"size":37074,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":104,"size":49570,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":93,"size":91166,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":104,"size":80711,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":102,"size":157650,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":104,"size":111225,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":103,"size":231984,"format":"WEBP"}]}}},{"id":"63ad70b0e1f75a1c252b8013","name":"pokiSip","flags":0,"timestamp":1694540406253,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"63ad70b0e1f75a1c252b8013","name":"pokiSip","flags":0,"tags":["cum","verypog","based","sip","coffee","tea"],"lifecycle":3,"state":["NO_PERSONAL","LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae434b5d3fdae58382926a","username":"ayyybubu","display_name":"ayyybubu","avatar_url":"//cdn.7tv.app/user/60ae434b5d3fdae58382926a/av_636fa88735a18fd8b17e7399/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","62d86a8419fdcf401421c5ae","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63ad70b0e1f75a1c252b8013","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":62,"size":14500,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":62,"size":38604,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":62,"size":33821,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":62,"size":75144,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":62,"size":56923,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":62,"size":115778,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":62,"size":140502,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":62,"size":163956,"format":"WEBP"}]}}},{"id":"64e74933e59df5a070a049cf","name":"LETMEIN","flags":0,"timestamp":1694610734684,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"64e74933e59df5a070a049cf","name":"LETMEIN","flags":0,"tags":["apollo","forsen","nymn"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"6428207a36f5ca3539b8ac95","username":"4cdee","display_name":"4cdee","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/ae9420a9-a8f1-4b67-8759-0a6ba7243c77-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64e74933e59df5a070a049cf","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":36,"height":32,"frame_count":1,"size":1096,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":36,"height":32,"frame_count":1,"size":1498,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":72,"height":64,"frame_count":1,"size":1791,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":72,"height":64,"frame_count":1,"size":4112,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":108,"height":96,"frame_count":1,"size":2495,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":108,"height":96,"frame_count":1,"size":7584,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":144,"height":128,"frame_count":1,"size":3172,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":144,"height":128,"frame_count":1,"size":11582,"format":"WEBP"}]}}},{"id":"60b756ea1b94ba7313c8ecd5","name":"doggoArrive","flags":0,"timestamp":1694615078051,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"60b756ea1b94ba7313c8ecd5","name":"doggoArrive","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60aed25862270703e6e13d13","username":"mph2210","display_name":"mph2210","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/f34fbd4a-836e-42dd-a049-b8920d72cc67-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60b756ea1b94ba7313c8ecd5","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":150,"size":53584,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":150,"size":137022,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":150,"size":141948,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":150,"size":310062,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":150,"size":250942,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":150,"size":511480,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":150,"size":391463,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":150,"size":545640,"format":"WEBP"}]}}},{"id":"60fb37535b7deb3de0b09ba8","name":"doggoLeave","flags":0,"timestamp":1694615089047,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"60fb37535b7deb3de0b09ba8","name":"doggoLeave","flags":0,"lifecycle":3,"state":["PERSONAL","LISTED"],"listed":true,"animated":true,"owner":{"id":"60ee0c73a60faa2a918d157f","username":"markzynk","display_name":"MarkZynk","avatar_url":"//cdn.7tv.app/user/60ee0c73a60faa2a918d157f/av_63af1b9d34dd4dc86234f001/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60fb37535b7deb3de0b09ba8","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":150,"size":53776,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":150,"size":131106,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":150,"size":141516,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":150,"size":317422,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":150,"size":257040,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":150,"size":547254,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":150,"size":421281,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":150,"size":682656,"format":"WEBP"}]}}},{"id":"62d94c7b6b8b3d03efebe33a","name":"Working","flags":0,"timestamp":1694679922767,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"62d94c7b6b8b3d03efebe33a","name":"Working","flags":0,"tags":["worker","work","shovel","working","spade","tired"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"61541c3c20eaf897465ad48b","username":"andreimonty","display_name":"AndreiMonty","avatar_url":"//cdn.7tv.app/user/61541c3c20eaf897465ad48b/av_6458e293d3b4256e12d830a9/3x.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/62d94c7b6b8b3d03efebe33a","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":43,"height":32,"frame_count":151,"size":89505,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":43,"height":32,"frame_count":151,"size":182882,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":86,"height":64,"frame_count":151,"size":271756,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":86,"height":64,"frame_count":151,"size":565532,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":129,"height":96,"frame_count":151,"size":509969,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":129,"height":96,"frame_count":151,"size":1036944,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":172,"height":128,"frame_count":151,"size":978597,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":172,"height":128,"frame_count":151,"size":1690442,"format":"WEBP"}]}}},{"id":"62ffe0653d44188601bb3372","name":"Swedge","flags":0,"timestamp":1694680293415,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"62ffe0653d44188601bb3372","name":"Swedge","flags":0,"tags":["sweden","snus","viking","fika"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"62729c305698271cdc3d9b5f","username":"c_judas","display_name":"C_Judas","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/55ccfd7a-3f24-4656-b84c-90cd3cf49f3a-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/62ffe0653d44188601bb3372","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1756,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":2410,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":3503,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":6938,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":5364,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":12766,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":7009,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":20144,"format":"WEBP"}]}}},{"id":"639352b7420768a80c21ea69","name":"bulle","flags":0,"timestamp":1694680305455,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"639352b7420768a80c21ea69","name":"bulle","flags":0,"tags":["fika","bulle","frodomon","kanelbulle"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"61fe9f5a69acfa0715e1fd93","username":"itsbaskeh","display_name":"ItsBaskeh","avatar_url":"//cdn.7tv.app/user/61fe9f5a69acfa0715e1fd93/av_6587950adbf474d8368cec4a/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/639352b7420768a80c21ea69","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1342,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1454,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2599,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":4314,"format":"WEBP"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":7964,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4026,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5569,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":12878,"format":"WEBP"}]}}},{"id":"60afe42eaecc11e86cd4559a","name":"FeelsBeaverMan","flags":0,"timestamp":1694687230974,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60afe42eaecc11e86cd4559a","name":"FeelsBeaverMan","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"6042166196832ffa787051e9","username":"gregoronsen","display_name":"Gregoronsen","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/59667792-f7f2-4992-8e97-fe9b493d27de-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60afe42eaecc11e86cd4559a","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":38,"height":32,"frame_count":1,"size":1467,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":38,"height":32,"frame_count":1,"size":1334,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":76,"height":64,"frame_count":1,"size":2951,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":76,"height":64,"frame_count":1,"size":3312,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":114,"height":96,"frame_count":1,"size":4409,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":114,"height":96,"frame_count":1,"size":5696,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":152,"height":128,"frame_count":1,"size":5898,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":152,"height":128,"frame_count":1,"size":7728,"format":"WEBP"}]}}},{"id":"644b3b82a2c7e4540aabe072","name":"FIRSTTIMECHATTER","flags":0,"timestamp":1694701355109,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"644b3b82a2c7e4540aabe072","name":"FIRSTTIMECHATTER","flags":0,"tags":["chatter","first","time"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60cbd92694befb7c93495a60","username":"crafting_table_","display_name":"crafting_table_","avatar_url":"//cdn.7tv.app/user/60cbd92694befb7c93495a60/av_64794969628540685214d7d3/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/644b3b82a2c7e4540aabe072","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":198,"size":15170,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":198,"size":44026,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":198,"size":18946,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":198,"size":90728,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":198,"size":33881,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":198,"size":135586,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":198,"size":91786,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":198,"size":203024,"format":"WEBP"}]}}},{"id":"6131dd5daf9287c4eb60927f","name":"AlienHips","flags":0,"timestamp":1694711483267,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"6131dd5daf9287c4eb60927f","name":"AlienHips","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"6131dab2af9287c4eb609268","username":"vicneeel","display_name":"vicneeel","avatar_url":"//cdn.7tv.app/user/6131dab2af9287c4eb609268/av_6520576332b1db5b90ef6b24/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6131dd5daf9287c4eb60927f","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":18,"size":9966,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":18,"size":12320,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":18,"size":29852,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":18,"size":17225,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":18,"size":49494,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":18,"size":29097,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":18,"size":41428,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":18,"size":60350,"format":"WEBP"}]}}},{"id":"60b53bb4f3fe830b8fe60723","name":"Backseatgaming","flags":0,"timestamp":1694763109863,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60b53bb4f3fe830b8fe60723","name":"Backseatgaming","flags":0,"tags":["backseatega"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60532ec2b4d31e459f7293dc","username":"marrryanx","display_name":"Marrryanx","avatar_url":"//cdn.7tv.app/user/60532ec2b4d31e459f7293dc/av_6570dc7f834e0a119031a679/3x_static.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60b53bb4f3fe830b8fe60723","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1585,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1246,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":3498,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":3510,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":6412,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":5785,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":8621,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":8784,"format":"WEBP"}]}}},{"id":"62c36bc8afc685668fea320f","name":"peepoPlant","flags":0,"timestamp":1694768950092,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"62c36bc8afc685668fea320f","name":"peepoPlant","flags":0,"tags":["peepo","plant","gardening","farming"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae434b5d3fdae58382926a","username":"ayyybubu","display_name":"ayyybubu","avatar_url":"//cdn.7tv.app/user/60ae434b5d3fdae58382926a/av_636fa88735a18fd8b17e7399/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","62d86a8419fdcf401421c5ae","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/62c36bc8afc685668fea320f","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":51,"height":32,"frame_count":4,"size":6013,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":51,"height":32,"frame_count":4,"size":6628,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":102,"height":64,"frame_count":4,"size":12672,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":102,"height":64,"frame_count":4,"size":17622,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":153,"height":96,"frame_count":4,"size":19781,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":153,"height":96,"frame_count":4,"size":30090,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":204,"height":128,"frame_count":4,"size":27688,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":204,"height":128,"frame_count":4,"size":41912,"format":"WEBP"}]}}},{"id":"627d581349607a2d9d9b6495","name":"Hedge","flags":0,"timestamp":1694770064728,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"627d581349607a2d9d9b6495","name":"Hedge","flags":0,"tags":["hedge","okayge"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60b3723cfdd0ee2f1bd43cad","username":"carnotic","display_name":"carnotic","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/977c1066-cd78-432a-a46e-6c9efd817bb7-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/627d581349607a2d9d9b6495","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":83,"height":32,"frame_count":1,"size":3273,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":83,"height":32,"frame_count":1,"size":2720,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":166,"height":64,"frame_count":1,"size":9792,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":166,"height":64,"frame_count":1,"size":8256,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":249,"height":96,"frame_count":1,"size":18121,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":249,"height":96,"frame_count":1,"size":15846,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":332,"height":128,"frame_count":1,"size":28006,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":332,"height":128,"frame_count":1,"size":25218,"format":"WEBP"}]}}},{"id":"6464e403ea74b7d3bef82e88","name":"SNAILS","flags":0,"timestamp":1694770791312,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"6464e403ea74b7d3bef82e88","name":"SNAILS","flags":0,"tags":["xqc","steamhappy","swag","xdd","nails","forsen"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"613cb52b3f2fa247f2ed46ae","username":"pigswitched","display_name":"pigswitched","avatar_url":"//cdn.7tv.app/user/613cb52b3f2fa247f2ed46ae/av_65035387c9920b6284155ec6/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6464e403ea74b7d3bef82e88","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":47,"height":32,"frame_count":1,"size":2166,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":47,"height":32,"frame_count":1,"size":1790,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":94,"height":64,"frame_count":1,"size":5544,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":94,"height":64,"frame_count":1,"size":3862,"format":"AVIF"},{"name":"3x.avif","static_name":"3x_static.avif","width":141,"height":96,"frame_count":1,"size":6408,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":141,"height":96,"frame_count":1,"size":10386,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":188,"height":128,"frame_count":1,"size":9131,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":188,"height":128,"frame_count":1,"size":15490,"format":"WEBP"}]}}},{"id":"65042a74bf154a991368de41","name":"JustABaby","flags":0,"timestamp":1694771838681,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"65042a74bf154a991368de41","name":"JustABaby","flags":0,"tags":["apollo","sleep"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60ae3e3eb2ecb01505346ae9","username":"fawcan","display_name":"Fawcan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38051bdb-83c6-4716-95e0-731462e02b45-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/65042a74bf154a991368de41","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":63,"height":32,"frame_count":1,"size":1526,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":63,"height":32,"frame_count":1,"size":3056,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":126,"height":64,"frame_count":1,"size":2919,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":126,"height":64,"frame_count":1,"size":8820,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":189,"height":96,"frame_count":1,"size":4348,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":189,"height":96,"frame_count":1,"size":16228,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":252,"height":128,"frame_count":1,"size":5748,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":252,"height":128,"frame_count":1,"size":24130,"format":"WEBP"}]}}},{"id":"640bcebe200ebb849852f1bf","name":"Norse","flags":0,"timestamp":1695043786014,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"640bcebe200ebb849852f1bf","name":"Norse","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"63c835cefc866ebbc80af66f","username":"sjonkonnerie","display_name":"sJoNkOnNeRiE","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38547da9-7866-4d0d-9816-5f9c00e68cc5-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/640bcebe200ebb849852f1bf","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":14,"height":32,"frame_count":1,"size":1182,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":14,"height":32,"frame_count":1,"size":1190,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":28,"height":64,"frame_count":1,"size":2235,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":28,"height":64,"frame_count":1,"size":3426,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":42,"height":96,"frame_count":1,"size":3481,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":42,"height":96,"frame_count":1,"size":6424,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":56,"height":128,"frame_count":1,"size":4747,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":56,"height":128,"frame_count":1,"size":10322,"format":"WEBP"}]}}},{"id":"65088068ef1caad468f482a1","name":"DonkEnterButDonkDoesNotEnterBecauseOfTheApparentReasonsYouCanSeeOnYourScreen","flags":0,"timestamp":1695063123105,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"65088068ef1caad468f482a1","name":"DonkEnterButDonkDoesNotEnterBecauseOfTheApparentReasonsYouCanSeeOnYourScreen","flags":0,"tags":["donk","feelsdonkman","enter","leave"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ff054ffbd646ea3b221dc9","username":"tunari__","display_name":"Tunari__","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/bc530a7a-e04d-4765-a662-bb3efde482e2-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/65088068ef1caad468f482a1","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":45,"height":32,"frame_count":29,"size":8814,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":45,"height":32,"frame_count":29,"size":11594,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":90,"height":64,"frame_count":29,"size":14359,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":90,"height":64,"frame_count":29,"size":19352,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":135,"height":96,"frame_count":29,"size":21094,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":135,"height":96,"frame_count":29,"size":26626,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":180,"height":128,"frame_count":29,"size":28773,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":180,"height":128,"frame_count":29,"size":33440,"format":"WEBP"}]}}},{"id":"63bdfa34ea6c08ef9218f8ca","name":"!voteskip","flags":0,"timestamp":1695135600848,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"63bdfa34ea6c08ef9218f8ca","name":"GoodTake","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"6053853cb4d31e459fdaa2dc","username":"laden","display_name":"Laden","avatar_url":"//cdn.7tv.app/pp/6053853cb4d31e459fdaa2dc/a94c67d7736940feb543e42024b740ef","style":{"color":849892095},"roles":["60724f65e93d828bf8858789","6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63bdfa34ea6c08ef9218f8ca","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":25,"size":4875,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":25,"size":8514,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":25,"size":7823,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":25,"size":36528,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":25,"size":12952,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":25,"size":59754,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":25,"size":22613,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":25,"size":80802,"format":"WEBP"}]}}},{"id":"6509d7bbef1caad468f4cff0","name":"sisyphus","flags":0,"timestamp":1695143921004,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"6509d7bbef1caad468f4cff0","name":"sisyphus","flags":0,"tags":["last","loser","sisyphus","nymn","mario","kart"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60b6746f64faf92496330d3c","username":"littlescampi","display_name":"LittleScampi","avatar_url":"//cdn.7tv.app/pp/60b6746f64faf92496330d3c/a1ff043123944a119d69cf9a7062a442","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6509d7bbef1caad468f4cff0","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":171,"size":29551,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":147,"size":41242,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":171,"size":58809,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":158,"size":92994,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":171,"size":102232,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":162,"size":136576,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":171,"size":169034,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":163,"size":201634,"format":"WEBP"}]}}},{"id":"613262c4fe9116a00d4b648e","name":"VirtualDonkality","flags":0,"timestamp":1695285110447,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"613262c4fe9116a00d4b648e","name":"VirtualDonkality","flags":0,"tags":["donk","virtualreality","woah"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae3e3eb2ecb01505346ae9","username":"fawcan","display_name":"Fawcan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38051bdb-83c6-4716-95e0-731462e02b45-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/613262c4fe9116a00d4b648e","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":34,"height":32,"frame_count":10,"size":6252,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":34,"height":32,"frame_count":10,"size":7482,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":68,"height":64,"frame_count":10,"size":10772,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":68,"height":64,"frame_count":10,"size":13862,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":102,"height":96,"frame_count":10,"size":15277,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":102,"height":96,"frame_count":10,"size":21530,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":136,"height":128,"frame_count":10,"size":20926,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":136,"height":128,"frame_count":10,"size":24720,"format":"WEBP"}]}}},{"id":"614c816a6251d7e000da4706","name":"KKiwi","flags":0,"timestamp":1695458370773,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"614c816a6251d7e000da4706","name":"KKiwi","flags":0,"tags":["new","zealand","kiwi"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"611850f37c65a41e6e0b0224","username":"notgards","display_name":"Notgards","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/ab07e666-45a4-4844-b3a1-3b15294ff85a-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/614c816a6251d7e000da4706","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1544,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1196,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2962,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":3046,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4784,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":5498,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":6838,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":8514,"format":"WEBP"}]}}},{"id":"63b86977ad5d565cef3a2ed7","name":"tablE","flags":0,"timestamp":1695477778074,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"63b86977ad5d565cef3a2ed7","name":"tablE","flags":0,"tags":["lule","table","forsene","forsen"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60b784cd5d373afbd615e12b","username":"cavel1ghter","display_name":"CaveL1ghter","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/8a86cfee-75ae-4794-9149-1c52f188c778-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63b86977ad5d565cef3a2ed7","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1350,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1510,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2363,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":4024,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3652,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":7480,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4764,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":11056,"format":"WEBP"}]}}},{"id":"650d854b714d175392387b32","name":"nymblE","flags":0,"timestamp":1695481280842,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"650d854b714d175392387b32","name":"nymblE","flags":0,"tags":["tablnyme","table","etable","nym","nyme"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"650d851b714d175392387b2b","username":"unga_bungasupaflyer","display_name":"unga_bungasupaflyer","avatar_url":"//static-cdn.jtvnw.net/user-default-pictures-uv/998f01ae-def8-11e9-b95c-784f43822e80-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/650d854b714d175392387b32","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1431,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1516,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2565,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":4130,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3747,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":7678,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4737,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":11804,"format":"WEBP"}]}}},{"id":"650f165958b80a4d09f13110","name":"docReadyToJam","flags":0,"timestamp":1695487595375,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"650f165958b80a4d09f13110","name":"docReadyToJam","flags":0,"tags":["doc","drdisrespect","docjammer"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae3e98b2ecb0150535c6b7","username":"gempir","display_name":"gempir","avatar_url":"//cdn.7tv.app/pp/60ae3e98b2ecb0150535c6b7/4aa1786cec024098be20d7b0683bae72","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/650f165958b80a4d09f13110","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1147,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1430,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2023,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":4126,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3058,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":7698,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4224,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":12162,"format":"WEBP"}]}}},{"id":"64a1d4f0ecdb531b02a33104","name":"forsenUnpleased","flags":0,"timestamp":1695487823951,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"64a1d4f0ecdb531b02a33104","name":"forsenUnpleased","flags":0,"tags":["forsenpls","alienunpleased"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"6042058896832ffa785800fe","username":"zhark","display_name":"Zhark","avatar_url":"//cdn.7tv.app/pp/6042058896832ffa785800fe/37ee95ffaa9846b286cb5554ff0716c5","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64a1d4f0ecdb531b02a33104","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1120,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1442,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1968,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":4290,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":2857,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":8354,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":3759,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":13308,"format":"WEBP"}]}}},{"id":"650f393cef1caad468f5ea1b","name":"AlienVibe","flags":0,"timestamp":1695503793611,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"650f393cef1caad468f5ea1b","name":"AlienVibe","flags":0,"tags":["aliendance","vibe","jam","alienpls","dance"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae434b5d3fdae58382926a","username":"ayyybubu","display_name":"ayyybubu","avatar_url":"//cdn.7tv.app/user/60ae434b5d3fdae58382926a/av_636fa88735a18fd8b17e7399/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","62d86a8419fdcf401421c5ae","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/650f393cef1caad468f5ea1b","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":72,"size":23945,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":72,"size":33850,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":72,"size":46138,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":72,"size":60040,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":72,"size":72452,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":72,"size":96750,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":72,"size":79465,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":72,"size":106932,"format":"WEBP"}]}}},{"id":"62b309fb1fd1e9520172f2a1","name":"Riming","flags":0,"timestamp":1695554239222,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"62b309fb1fd1e9520172f2a1","name":"Riming","flags":0,"tags":["rime","russel","comedy","lime","erobb221"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"61c7196612987d64d6ae7fc6","username":"quaxi13","display_name":"Quaxi13","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/39135e01-6d49-40ad-afe5-2973e6176848-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/62b309fb1fd1e9520172f2a1","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":42,"size":7665,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":42,"size":11786,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":42,"size":12448,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":42,"size":20832,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":42,"size":20114,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":42,"size":31786,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":42,"size":25116,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":42,"size":36740,"format":"WEBP"}]}}},{"id":"651028fdef1caad468f619dc","name":"AlienFloss","flags":0,"timestamp":1695558392570,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"651028fdef1caad468f619dc","name":"AlienFloss","flags":0,"tags":["alienpls","dance","pls","vibe","floss","fortnite"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae434b5d3fdae58382926a","username":"ayyybubu","display_name":"ayyybubu","avatar_url":"//cdn.7tv.app/user/60ae434b5d3fdae58382926a/av_636fa88735a18fd8b17e7399/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","62d86a8419fdcf401421c5ae","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/651028fdef1caad468f619dc","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":80,"size":30511,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":80,"size":44540,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":80,"size":63289,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":80,"size":83516,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":80,"size":101063,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":80,"size":133156,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":80,"size":132474,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":80,"size":153750,"format":"WEBP"}]}}},{"id":"6510474dc9920b628417f683","name":"ReadyToRightThere","flags":0,"timestamp":1695565660720,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"6510474dc9920b628417f683","name":"ReadyToRightThere","flags":0,"tags":["rightthere","leo"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae3e98b2ecb0150535c6b7","username":"gempir","display_name":"gempir","avatar_url":"//cdn.7tv.app/pp/60ae3e98b2ecb0150535c6b7/4aa1786cec024098be20d7b0683bae72","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6510474dc9920b628417f683","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":37,"height":32,"frame_count":1,"size":1193,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":37,"height":32,"frame_count":1,"size":1528,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":74,"height":64,"frame_count":1,"size":2037,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":74,"height":64,"frame_count":1,"size":4162,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":111,"height":96,"frame_count":1,"size":2808,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":111,"height":96,"frame_count":1,"size":7650,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":148,"height":128,"frame_count":1,"size":3828,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":148,"height":128,"frame_count":1,"size":11690,"format":"WEBP"}]}}},{"id":"65107032c9920b6284180082","name":"AlienJam","flags":0,"timestamp":1695582213965,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"65107032c9920b6284180082","name":"AlienJam","flags":0,"tags":["alienpls","dance","jam","vibe"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae434b5d3fdae58382926a","username":"ayyybubu","display_name":"ayyybubu","avatar_url":"//cdn.7tv.app/user/60ae434b5d3fdae58382926a/av_636fa88735a18fd8b17e7399/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","62d86a8419fdcf401421c5ae","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/65107032c9920b6284180082","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":762,"size":278008,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":762,"size":395500,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":762,"size":600813,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":762,"size":734784,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":762,"size":976319,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":762,"size":1174168,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":762,"size":1179634,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":762,"size":1326776,"format":"WEBP"}]}}},{"id":"651036c7ef1caad468f61cf1","name":"AlienSilly","flags":0,"timestamp":1695582262039,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"651036c7ef1caad468f61cf1","name":"AlienSilly","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae434b5d3fdae58382926a","username":"ayyybubu","display_name":"ayyybubu","avatar_url":"//cdn.7tv.app/user/60ae434b5d3fdae58382926a/av_636fa88735a18fd8b17e7399/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","62d86a8419fdcf401421c5ae","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/651036c7ef1caad468f61cf1","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":46,"size":19894,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":46,"size":25264,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":46,"size":39529,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":46,"size":48214,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":46,"size":66695,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":46,"size":77046,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":46,"size":81010,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":46,"size":89738,"format":"WEBP"}]}}},{"id":"65106e01462d22f767cbc661","name":"AlienWorm","flags":0,"timestamp":1695582290174,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"65106e01462d22f767cbc661","name":"AlienWorm","flags":0,"tags":["pls","vibe","jiggle","alienpls","dance","alien"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae434b5d3fdae58382926a","username":"ayyybubu","display_name":"ayyybubu","avatar_url":"//cdn.7tv.app/user/60ae434b5d3fdae58382926a/av_636fa88735a18fd8b17e7399/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","62d86a8419fdcf401421c5ae","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/65106e01462d22f767cbc661","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":272,"size":106119,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":272,"size":150754,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":272,"size":236801,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":272,"size":285280,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":272,"size":387658,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":272,"size":465112,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":272,"size":484810,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":272,"size":529284,"format":"WEBP"}]}}},{"id":"65106d26bf154a99136b5f48","name":"AlienPump","flags":0,"timestamp":1695582292544,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"65106d26bf154a99136b5f48","name":"AlienPump","flags":0,"tags":["pls","dance","vibe","alien","alienpls"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae434b5d3fdae58382926a","username":"ayyybubu","display_name":"ayyybubu","avatar_url":"//cdn.7tv.app/user/60ae434b5d3fdae58382926a/av_636fa88735a18fd8b17e7399/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","62d86a8419fdcf401421c5ae","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/65106d26bf154a99136b5f48","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":234,"size":71834,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":234,"size":123614,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":234,"size":158659,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":234,"size":234740,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":234,"size":266990,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":234,"size":369228,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":234,"size":331836,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":234,"size":427770,"format":"WEBP"}]}}},{"id":"65102ab258b80a4d09f16b5d","name":"AlienDefault","flags":0,"timestamp":1695582293707,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"65102ab258b80a4d09f16b5d","name":"AlienDefault","flags":0,"tags":["fortnite","dance","pls","vibe","alienpls","alien"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae434b5d3fdae58382926a","username":"ayyybubu","display_name":"ayyybubu","avatar_url":"//cdn.7tv.app/user/60ae434b5d3fdae58382926a/av_636fa88735a18fd8b17e7399/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","62d86a8419fdcf401421c5ae","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/65102ab258b80a4d09f16b5d","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":339,"size":107565,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":339,"size":169326,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":339,"size":230067,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":339,"size":325032,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":339,"size":370591,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":339,"size":514796,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":339,"size":478643,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":339,"size":598318,"format":"WEBP"}]}}},{"id":"650f3b0858b80a4d09f13bad","name":"AyyyMacarena","flags":0,"timestamp":1695582302467,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"650f3b0858b80a4d09f13bad","name":"AlienMacarena","flags":0,"tags":["alienpls","alien","dance","jam","pls","vibe"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae434b5d3fdae58382926a","username":"ayyybubu","display_name":"ayyybubu","avatar_url":"//cdn.7tv.app/user/60ae434b5d3fdae58382926a/av_636fa88735a18fd8b17e7399/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","62d86a8419fdcf401421c5ae","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/650f3b0858b80a4d09f13bad","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":248,"size":67699,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":248,"size":111242,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":248,"size":135956,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":248,"size":209060,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":248,"size":213233,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":248,"size":338038,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":248,"size":244674,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":248,"size":378032,"format":"WEBP"}]}}},{"id":"65102db1bf154a99136b50e9","name":"AlienTechno","flags":0,"timestamp":1695586212014,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"65102db1bf154a99136b50e9","name":"AlienTechno","flags":0,"tags":["techno","vibe","fortnite","alien","pls","dance"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae434b5d3fdae58382926a","username":"ayyybubu","display_name":"ayyybubu","avatar_url":"//cdn.7tv.app/user/60ae434b5d3fdae58382926a/av_636fa88735a18fd8b17e7399/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","62d86a8419fdcf401421c5ae","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/65102db1bf154a99136b50e9","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":599,"size":221968,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":599,"size":306566,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":599,"size":486970,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":599,"size":573644,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":599,"size":796073,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":599,"size":918798,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":599,"size":993441,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":599,"size":1055732,"format":"WEBP"}]}}},{"id":"629a2fa547051898ec04d2a8","name":"Upgrade","flags":0,"timestamp":1695648654266,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"629a2fa547051898ec04d2a8","name":"Upgrade","flags":0,"tags":["diabloimmortal","diablo","immortal","upgrade","uparrow"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60afa9d9ebfcf7562e59c242","username":"peacepeoplejr","display_name":"PeacePeopleJr","avatar_url":"//cdn.7tv.app/pp/60afa9d9ebfcf7562e59c242/62199af2f21c47f9a0c3b8578d93d979","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/629a2fa547051898ec04d2a8","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":18,"size":4686,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":18,"size":10438,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":18,"size":6352,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":18,"size":17040,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":18,"size":9103,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":18,"size":24982,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":18,"size":9155,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":18,"size":27624,"format":"WEBP"}]}}},{"id":"621b0ab487952f3b8e5a4f15","name":"PoroPot","flags":0,"timestamp":1695811857425,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"621b0ab487952f3b8e5a4f15","name":"PoroPot","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae7a593c4727d906a841f6","username":"melonify","display_name":"Melonify","avatar_url":"//cdn.7tv.app/user/60ae7a593c4727d906a841f6/av_644f6ae6ffbdc8aa02c17299/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","63124dcf098bd6b8e5a7cb02","60724f65e93d828bf8858789","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/621b0ab487952f3b8e5a4f15","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1282,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1493,"format":"AVIF"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2730,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":3422,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4002,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":5600,"format":"WEBP"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":8004,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5302,"format":"AVIF"}]}}},{"id":"62189328df86bac8c42f87f1","name":"PotL","flags":0,"timestamp":1695811887627,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"62189328df86bac8c42f87f1","name":"PotL","flags":0,"tags":["elden","ring","pot","loser","dark","souls"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"6053853cb4d31e459fdaa2dc","username":"laden","display_name":"Laden","avatar_url":"//cdn.7tv.app/pp/6053853cb4d31e459fdaa2dc/a94c67d7736940feb543e42024b740ef","style":{"color":849892095},"roles":["60724f65e93d828bf8858789","6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/62189328df86bac8c42f87f1","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1456,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1200,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2459,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":3058,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3604,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":5070,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4415,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":7018,"format":"WEBP"}]}}},{"id":"6219b46baff1c45709b4a65f","name":"PotEnemy","flags":0,"timestamp":1695811890109,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"6219b46baff1c45709b4a65f","name":"PotEnemy","flags":0,"tags":["potenemy","pot","friend","potfriend","enemy","gun"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60afb03499923bbe7f9d95ad","username":"lynxeption","display_name":"Lynxeption","avatar_url":"//cdn.7tv.app/user/60afb03499923bbe7f9d95ad/av_657b724890ed04f2389fb7b1/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6219b46baff1c45709b4a65f","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1628,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1304,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":3452,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":3083,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":6010,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4557,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":6038,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":8774,"format":"WEBP"}]}}},{"id":"621f66f4f80b7279c4447d59","name":"PotChest","flags":0,"timestamp":1695811891966,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"621f66f4f80b7279c4447d59","name":"PotChest","flags":0,"tags":["potfriend","pot","friend","batchest"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"61b838faf93abf1d40701ae7","username":"exjne","display_name":"Exjne","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/621f66f4f80b7279c4447d59","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1683,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1376,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":3114,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":3738,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4472,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":5836,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5609,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":7986,"format":"WEBP"}]}}},{"id":"621f9b23c286e9a2617ae546","name":"PotLove","flags":0,"timestamp":1695811893614,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"621f9b23c286e9a2617ae546","name":"PotLove","flags":0,"tags":["potfriend","elden","ring"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60ae21c0aee2aa55388ba741","username":"farbrorbarbro","display_name":"FarbrorBarbro","avatar_url":"//cdn.7tv.app/pp/60ae21c0aee2aa55388ba741/2aa45e11e4474bf8a04c74fe9157bd53","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/621f9b23c286e9a2617ae546","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1310,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1556,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":3244,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2829,"format":"AVIF"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4260,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":5576,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5435,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":7700,"format":"WEBP"}]}}},{"id":"6230a0442cbc7e45d4cacf84","name":"KEKPot","flags":0,"timestamp":1695811919037,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"6230a0442cbc7e45d4cacf84","name":"KEKPot","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"610d9d28e2fbd2e210e0740f","username":"turtledoves","display_name":"turtledoves","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/b437b23e-e993-4ca1-b500-71747c0c405c-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6230a0442cbc7e45d4cacf84","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1394,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1671,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":3666,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2939,"format":"AVIF"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4232,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":5624,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5080,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":7522,"format":"WEBP"}]}}},{"id":"621f65c1c286e9a2617ae2f7","name":"PotBased","flags":0,"timestamp":1695811931533,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"621f65c1c286e9a2617ae2f7","name":"PotBased","flags":0,"tags":["potfriend","pot","friend","based"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"61b838faf93abf1d40701ae7","username":"exjne","display_name":"Exjne","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/621f65c1c286e9a2617ae2f7","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1527,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1318,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2695,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":3544,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3913,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":5448,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4822,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":7398,"format":"WEBP"}]}}},{"id":"621c4e293808dfe5c465fec4","name":"PotShy","flags":0,"timestamp":1695811936538,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"621c4e293808dfe5c465fec4","name":"PotShy","flags":0,"tags":["pot"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae750eb351b8d1c083f5ec","username":"znixp","display_name":"zNIXp","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/8569f469-d7ee-468b-b718-e87459b2278a-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/621c4e293808dfe5c465fec4","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1104,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1485,"format":"AVIF"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":3014,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":3110,"format":"WEBP"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":5270,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4599,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":6196,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":8382,"format":"WEBP"}]}}},{"id":"62352999b1ac2f158886304a","name":"PotFeet","flags":0,"timestamp":1695811959285,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"62352999b1ac2f158886304a","name":"PotFeet","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"6053853cb4d31e459fdaa2dc","username":"laden","display_name":"Laden","avatar_url":"//cdn.7tv.app/pp/6053853cb4d31e459fdaa2dc/a94c67d7736940feb543e42024b740ef","style":{"color":849892095},"roles":["60724f65e93d828bf8858789","6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/62352999b1ac2f158886304a","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1363,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1126,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2743,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2964,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4371,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":5132,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":6383,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":7966,"format":"WEBP"}]}}},{"id":"62f858aed46fdda0ac66a5e6","name":"DancingPotato","flags":0,"timestamp":1695812000540,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"62f858aed46fdda0ac66a5e6","name":"DancingPotato","flags":0,"tags":["dancing","potato","potatodancing"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"6201ffa0d668c38c1673ccac","username":"narikonakamura","display_name":"NarikoNakamura","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/548cf9da-e09e-4c61-83c2-a5de4e9691a6-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/62f858aed46fdda0ac66a5e6","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":60,"size":14723,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":60,"size":50052,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":60,"size":28736,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":60,"size":108136,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":60,"size":42395,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":60,"size":170556,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":60,"size":54397,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":60,"size":235096,"format":"WEBP"}]}}},{"id":"64fb6a0ecef5572ce1b8b5a8","name":"IDontCareIJustWannaSleep","flags":0,"timestamp":1695889381240,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"64fb6a0ecef5572ce1b8b5a8","name":"IDontCareIJustWannaSleep","flags":0,"tags":["cat","kitty","kitten","sleep","sleeping","meow"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"6169d4c6474b9b7b59a37f56","username":"eropbl4_","display_name":"Eropbl4_","avatar_url":"//cdn.7tv.app/user/6169d4c6474b9b7b59a37f56/av_64305076b0b346d623214893/3x.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64fb6a0ecef5572ce1b8b5a8","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":48,"height":32,"frame_count":216,"size":42904,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":48,"height":32,"frame_count":216,"size":112522,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":96,"height":64,"frame_count":216,"size":100801,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":96,"height":64,"frame_count":216,"size":237796,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":144,"height":96,"frame_count":216,"size":185828,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":144,"height":96,"frame_count":216,"size":379546,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":192,"height":128,"frame_count":216,"size":564491,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":192,"height":128,"frame_count":216,"size":634344,"format":"WEBP"}]}}},{"id":"6514673b462d22f767cc99d3","name":"Ohnoge","flags":0,"timestamp":1695889885984,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"6514673b462d22f767cc99d3","name":"Ohnoge","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"613ba5d090c03f6155d42066","username":"oggyerino","display_name":"Oggyerino","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/4526524c-7f63-4bb8-bb38-6081d49b3b00-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6514673b462d22f767cc99d3","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1768,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1318,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":4710,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2455,"format":"AVIF"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3647,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":9092,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4894,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":14662,"format":"WEBP"}]}}},{"id":"6516bb6de71d2b64940d81b8","name":"WatchingForsen","flags":0,"timestamp":1695988793357,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"6516bb6de71d2b64940d81b8","name":"WatchingForsen","flags":0,"tags":["forsen","nymn","k4yfour","psp1g","psp","steamhappy"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60b6746f64faf92496330d3c","username":"littlescampi","display_name":"LittleScampi","avatar_url":"//cdn.7tv.app/pp/60b6746f64faf92496330d3c/a1ff043123944a119d69cf9a7062a442","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6516bb6de71d2b64940d81b8","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":96,"height":32,"frame_count":205,"size":73563,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":96,"height":32,"frame_count":205,"size":220088,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":192,"height":64,"frame_count":205,"size":264419,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":192,"height":64,"frame_count":205,"size":512010,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":288,"height":96,"frame_count":205,"size":586048,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":288,"height":96,"frame_count":205,"size":859004,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":384,"height":128,"frame_count":205,"size":1353808,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":384,"height":128,"frame_count":205,"size":1452732,"format":"WEBP"}]}}},{"id":"650cfe4c714d175392386870","name":"moon2Spin","flags":0,"timestamp":1696005219182,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"650cfe4c714d175392386870","name":"moon2Spin","flags":0,"tags":["moonmoon","cum","spin"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60b8f6b2d115b05311d90e2c","username":"thelahal","display_name":"TheLahal","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/5ce15084-b131-4fda-bbf1-5449e9354712-profile_image-70x70.jpeg","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/650cfe4c714d175392386870","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":6,"size":5813,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":6,"size":4672,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":6,"size":10891,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":6,"size":10098,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":6,"size":16219,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":6,"size":15476,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":6,"size":20949,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":6,"size":20540,"format":"WEBP"}]}}},{"id":"646dd022577dcd2c80ef1e75","name":"glorp","flags":0,"timestamp":1696016234867,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"646dd022577dcd2c80ef1e75","name":"glorp","flags":0,"tags":["alien","cute","cat","glorp"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"61e824acf20dcd151f05eca2","username":"ltsnickiminaj","display_name":"ltsnickiminaj","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/fbf5625c-7370-4023-820a-e002b171a988-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/646dd022577dcd2c80ef1e75","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1155,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1732,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1989,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":5004,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":2906,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":9102,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":3759,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":14204,"format":"WEBP"}]}}},{"id":"6420b5d22b8217416a236961","name":"plinktosis","flags":0,"timestamp":1696024005011,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"6420b5d22b8217416a236961","name":"plinktosis","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"6143b4a97b14fdf700b92cb5","username":"hypnocorn","display_name":"Hypnocorn","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/dd5c5a0c-5f3a-4ff3-979b-b6d03249b80e-profile_image-70x70.png","style":{"color":-1857617921},"roles":["6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6420b5d22b8217416a236961","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":60,"height":32,"frame_count":186,"size":83716,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":60,"height":32,"frame_count":186,"size":133788,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":120,"height":64,"frame_count":186,"size":183482,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":120,"height":64,"frame_count":186,"size":268392,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":180,"height":96,"frame_count":186,"size":314756,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":180,"height":96,"frame_count":186,"size":420886,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":240,"height":128,"frame_count":186,"size":596603,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":240,"height":128,"frame_count":186,"size":611078,"format":"WEBP"}]}}},{"id":"631b3f3c8cf0978e2955accd","name":"NymNsComputer","flags":0,"timestamp":1696091379568,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"631b3f3c8cf0978e2955accd","name":"NymNsComputer","flags":0,"tags":["nymn","computer"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae3e3eb2ecb01505346ae9","username":"fawcan","display_name":"Fawcan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38051bdb-83c6-4716-95e0-731462e02b45-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/631b3f3c8cf0978e2955accd","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":77,"height":32,"frame_count":1,"size":938,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":77,"height":32,"frame_count":1,"size":1622,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":154,"height":64,"frame_count":1,"size":1975,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":154,"height":64,"frame_count":1,"size":4850,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":231,"height":96,"frame_count":1,"size":3719,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":231,"height":96,"frame_count":1,"size":9340,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":308,"height":128,"frame_count":1,"size":5094,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":308,"height":128,"frame_count":1,"size":14634,"format":"WEBP"}]}}},{"id":"651af7d6461f7419e54eb6b0","name":"Jackass","flags":0,"timestamp":1696266218993,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"651af7d6461f7419e54eb6b0","name":"Jackass","flags":0,"tags":["nymn"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae3e98b2ecb0150535c6b7","username":"gempir","display_name":"gempir","avatar_url":"//cdn.7tv.app/pp/60ae3e98b2ecb0150535c6b7/4aa1786cec024098be20d7b0683bae72","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/651af7d6461f7419e54eb6b0","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1153,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1844,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2297,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":5992,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3912,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":11658,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5490,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":19438,"format":"WEBP"}]}}},{"id":"60d3255291b6751bc1a24220","name":"peepoGermany","flags":0,"timestamp":1696324593491,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"60d3255291b6751bc1a24220","name":"peepoGermany","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae8a63f39a7552b62fa966","username":"ch4mpjon","display_name":"ch4mpjon","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/7818e1c4-0d4e-43de-a5d0-fef74e905976-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60d3255291b6751bc1a24220","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":4,"size":4562,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":4,"size":5250,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":4,"size":10130,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":4,"size":9542,"format":"AVIF"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":4,"size":14312,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":4,"size":16280,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":4,"size":18760,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":4,"size":17544,"format":"WEBP"}]}}},{"id":"651c075f1f6d3af5585223c2","name":"MrNime","flags":0,"timestamp":1696364026904,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"651c075f1f6d3af5585223c2","name":"Nime","flags":0,"tags":["nime","erobb221","nymn"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"629d19b03cfb54ec859b8a39","username":"23pacs","display_name":"23pacs","avatar_url":"//cdn.7tv.app/user/629d19b03cfb54ec859b8a39/av_65989c3c8c708b97a921293a/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/651c075f1f6d3af5585223c2","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":57,"height":32,"frame_count":1,"size":2108,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":57,"height":32,"frame_count":1,"size":3220,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":114,"height":64,"frame_count":1,"size":4508,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":114,"height":64,"frame_count":1,"size":9502,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":171,"height":96,"frame_count":1,"size":7131,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":171,"height":96,"frame_count":1,"size":18178,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":228,"height":128,"frame_count":1,"size":9790,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":228,"height":128,"frame_count":1,"size":27810,"format":"WEBP"}]}}},{"id":"61fcc40c690425de3c63d907","name":"CALCULATING","flags":0,"timestamp":1696450159901,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"61fcc40c690425de3c63d907","name":"CALCULATING","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60aeab21229664e8663345dd","username":"barricade0_","display_name":"BARRICADE0_","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/948ce321-c188-4c7a-90c0-16169e190ac2-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61fcc40c690425de3c63d907","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":150,"size":81314,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":150,"size":140246,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":150,"size":288548,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":150,"size":418762,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":150,"size":541661,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":150,"size":765930,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":150,"size":882844,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":150,"size":889458,"format":"WEBP"}]}}},{"id":"63c608a3f5732004e116197a","name":"Pipe","flags":0,"timestamp":1696511393728,"actor_id":"60ae518c0e35477634c151f1","data":{"id":"63c608a3f5732004e116197a","name":"Pipe","flags":0,"tags":["steel","pipe"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"61c2b4845be2d01acc98e2b6","username":"hatsooo","display_name":"Hatsooo","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63c608a3f5732004e116197a","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":694,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":887,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":1678,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1323,"format":"AVIF"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":1835,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":3114,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":2566,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":4904,"format":"WEBP"}]}}},{"id":"63723f479ac5ff30f76fe478","name":"STOCKS","flags":0,"timestamp":1696596330384,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"63723f479ac5ff30f76fe478","name":"STOCKS","flags":0,"tags":["nymn","pepew","stock"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae3e3eb2ecb01505346ae9","username":"fawcan","display_name":"Fawcan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38051bdb-83c6-4716-95e0-731462e02b45-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63723f479ac5ff30f76fe478","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1185,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1688,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2289,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":4974,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3466,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":9116,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5116,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":14104,"format":"WEBP"}]}}},{"id":"63508ebd7e96ced99fe04e7e","name":"nymnPlayingMarioKart","flags":0,"timestamp":1696768554143,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"63508ebd7e96ced99fe04e7e","name":"Cope","flags":0,"tags":["mario","kart","mariokart","copium","coping","lastplace"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"603c7fca96832ffa788a5f14","username":"hyruverse","display_name":"hyruverse","avatar_url":"//cdn.7tv.app/pp/603c7fca96832ffa788a5f14/2ed3bd237882444ebccf38ae918e8df6","style":{"color":849892095},"roles":["60724f65e93d828bf8858789","6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63508ebd7e96ced99fe04e7e","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":190,"size":59907,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":190,"size":76284,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":190,"size":184890,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":190,"size":186536,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":190,"size":379006,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":190,"size":312498,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":190,"size":1171696,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":190,"size":624222,"format":"WEBP"}]}}},{"id":"63c079ff3977df67e32d0c56","name":"RightThere","flags":0,"timestamp":1696852857204,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"63c079ff3977df67e32d0c56","name":"RightThere","flags":0,"tags":["dicaprio","pointing","movienight","movie","artifact","meme"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae434b5d3fdae58382926a","username":"ayyybubu","display_name":"ayyybubu","avatar_url":"//cdn.7tv.app/user/60ae434b5d3fdae58382926a/av_636fa88735a18fd8b17e7399/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","62d86a8419fdcf401421c5ae","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63c079ff3977df67e32d0c56","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":37,"height":32,"frame_count":56,"size":23600,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":37,"height":32,"frame_count":56,"size":44950,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":74,"height":64,"frame_count":56,"size":52751,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":74,"height":64,"frame_count":56,"size":89464,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":111,"height":96,"frame_count":56,"size":85565,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":111,"height":96,"frame_count":56,"size":132578,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":148,"height":128,"frame_count":56,"size":121361,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":148,"height":128,"frame_count":56,"size":174096,"format":"WEBP"}]}}},{"id":"60bb6cfe5575fda21c9fef57","name":"SirO","flags":0,"timestamp":1696852861329,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60bb6cfe5575fda21c9fef57","name":"SirO","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60afc6b060e24df01aa39207","username":"raydynn","display_name":"Raydynn","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/1c863607-c5f8-4a28-a558-174454984cbf-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60bb6cfe5575fda21c9fef57","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1119,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":864,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1903,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":1884,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":2666,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":3008,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":3374,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":3110,"format":"WEBP"}]}}},{"id":"611a4f329fa9a9dd99b69750","name":"Susge","flags":0,"timestamp":1696852866555,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"611a4f329fa9a9dd99b69750","name":"Susge","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60c73dab77f997672644ac33","username":"mahan_5000","display_name":"MAHAN_5000","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/bab41002-ed60-4227-9023-acfb71793143-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/611a4f329fa9a9dd99b69750","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1278,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":914,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2346,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2425,"format":"AVIF"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3806,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4226,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5260,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":6268,"format":"WEBP"}]}}},{"id":"652571c5abd4cd85c81e38e6","name":"Glime","flags":0,"timestamp":1696952854553,"actor_id":"60ae518c0e35477634c151f1","data":{"id":"652571c5abd4cd85c81e38e6","name":"Glorpnime","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae518c0e35477634c151f1","username":"fabulouspotato69","display_name":"FabulousPotato69","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/652571c5abd4cd85c81e38e6","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1326,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1726,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2610,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":5252,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4142,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":10080,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":6475,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":15952,"format":"WEBP"}]}}},{"id":"618f440d17e4d50afc0d414f","name":"doctorLeMonkePls","flags":0,"timestamp":1696954637288,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"618f440d17e4d50afc0d414f","name":"monkePls","flags":0,"tags":["monke"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ee19b6242437446f4a501b","username":"lubilive","display_name":"LubiLIVE","avatar_url":"//cdn.7tv.app/user/60ee19b6242437446f4a501b/av_64cad795ee451bd1791e78ca/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/618f440d17e4d50afc0d414f","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":87,"size":42041,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":87,"size":67640,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":87,"size":87061,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":87,"size":148328,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":87,"size":143291,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":87,"size":247824,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":87,"size":199643,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":87,"size":307760,"format":"WEBP"}]}}},{"id":"636ac4fc6170e43a8c65ab9a","name":"Loota","flags":0,"timestamp":1697140538178,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"636ac4fc6170e43a8c65ab9a","name":"Loota","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60c09f2478e83358fa8a83af","username":"eclipsy113","display_name":"eclipsy113","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/cf1ef411-5dd0-4738-8039-71842facca93-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/636ac4fc6170e43a8c65ab9a","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1370,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1758,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2529,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":4982,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3715,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":9110,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4558,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":13828,"format":"WEBP"}]}}},{"id":"652740549ffed7dc1855d4d7","name":"Humping","flags":0,"timestamp":1697210594774,"actor_id":"60ae518c0e35477634c151f1","data":{"id":"652740549ffed7dc1855d4d7","name":"EmotiHump","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"61d0b944c29ed2aed7544871","username":"deividmartini","display_name":"DeividMartini","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/ff790138-083a-49c3-8456-52562077e823-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/652740549ffed7dc1855d4d7","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":37,"height":32,"frame_count":21,"size":10731,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":37,"height":32,"frame_count":21,"size":18526,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":74,"height":64,"frame_count":21,"size":20001,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":74,"height":64,"frame_count":21,"size":38584,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":111,"height":96,"frame_count":21,"size":30349,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":111,"height":96,"frame_count":21,"size":59826,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":148,"height":128,"frame_count":21,"size":41210,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":148,"height":128,"frame_count":21,"size":81246,"format":"WEBP"}]}}},{"id":"61e625083441abfa431d1989","name":"Clown","flags":1,"timestamp":1697215702058,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"61e625083441abfa431d1989","name":"Clown","flags":256,"tags":["clown","wig","honk","costume","rainbow","zerowidth"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60f39e8bc07d1ac193652def","username":"shmovy","display_name":"Shmovy","avatar_url":"//cdn.7tv.app/user/60f39e8bc07d1ac193652def/av_63a4e84f5c2aba9b3b60bf46/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61e625083441abfa431d1989","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1232,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":800,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1917,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":1710,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":2809,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":2752,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":3580,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":3896,"format":"WEBP"}]}}},{"id":"63ab5fac4e8dd89273fe9b61","name":"DRAMA","flags":0,"timestamp":1697216415814,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"63ab5fac4e8dd89273fe9b61","name":"DRAMA","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"613bdc8feef63902454a4f2d","username":"maxdaxx","display_name":"maxdaxx","avatar_url":"//cdn.7tv.app/user/613bdc8feef63902454a4f2d/av_63b31dc35aa77eca61ec84b8/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63ab5fac4e8dd89273fe9b61","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":34,"height":32,"frame_count":65,"size":19409,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":34,"height":32,"frame_count":65,"size":36140,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":68,"height":64,"frame_count":65,"size":40861,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":68,"height":64,"frame_count":65,"size":68820,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":102,"height":96,"frame_count":65,"size":68383,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":102,"height":96,"frame_count":65,"size":99308,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":136,"height":128,"frame_count":65,"size":101183,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":136,"height":128,"frame_count":65,"size":131678,"format":"WEBP"}]}}},{"id":"60af9e0c52a13d1adb77dc98","name":"pepeRun","flags":0,"timestamp":1697385307327,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60af9e0c52a13d1adb77dc98","name":"pepeRun","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"6053853cb4d31e459fdaa2dc","username":"laden","display_name":"Laden","avatar_url":"//cdn.7tv.app/pp/6053853cb4d31e459fdaa2dc/a94c67d7736940feb543e42024b740ef","style":{"color":849892095},"roles":["60724f65e93d828bf8858789","6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60af9e0c52a13d1adb77dc98","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":17,"size":9236,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":17,"size":15108,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":17,"size":18212,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":17,"size":34190,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":17,"size":29524,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":17,"size":57016,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":17,"size":44004,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":17,"size":67170,"format":"WEBP"}]}}},{"id":"60b0d99b8fb21a01bc62782d","name":"1G","flags":0,"timestamp":1697385313026,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60b0d99b8fb21a01bc62782d","name":"Bald1G","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60aeb373955615deef63b6ac","username":"kaserioxx","display_name":"kaserioxx","avatar_url":"//static-cdn.jtvnw.net/user-default-pictures-uv/13e5fa74-defa-11e9-809c-784f43822e80-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60b0d99b8fb21a01bc62782d","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":42,"height":32,"frame_count":1,"size":1150,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":42,"height":32,"frame_count":1,"size":926,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":84,"height":64,"frame_count":1,"size":2049,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":84,"height":64,"frame_count":1,"size":2202,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":126,"height":96,"frame_count":1,"size":2882,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":126,"height":96,"frame_count":1,"size":3418,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":168,"height":128,"frame_count":1,"size":3863,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":168,"height":128,"frame_count":1,"size":3268,"format":"WEBP"}]}}},{"id":"652410135795d60c78b805ca","name":"ItsglorpingTime","flags":0,"timestamp":1697387325472,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"652410135795d60c78b805ca","name":"ItsglorpingTime","flags":0,"tags":["glorp","alien","cute","cat"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"6509a09358b80a4d09f012c7","username":"chewbriel","display_name":"CHEWBRIEL","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/652410135795d60c78b805ca","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":39,"size":18120,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":39,"size":34660,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":39,"size":48400,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":39,"size":86634,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":39,"size":82665,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":39,"size":145074,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":39,"size":112925,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":39,"size":202738,"format":"WEBP"}]}}},{"id":"62f23f0f5591c76fde22c341","name":"peepoPoland","flags":0,"timestamp":1697400938138,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"62f23f0f5591c76fde22c341","name":"peepoPoland","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"610399d141ab14baee7e2c55","username":"nugiar","display_name":"nugIar","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/7c768ccc-9284-4f70-8323-967f2960c81c-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/62f23f0f5591c76fde22c341","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":4,"size":5197,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":4,"size":4918,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":4,"size":9693,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":4,"size":11072,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":4,"size":14703,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":4,"size":17480,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":4,"size":19878,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":4,"size":24064,"format":"WEBP"}]}}},{"id":"614615a57b14fdf700b96dca","name":"guzunya","flags":0,"timestamp":1697553214769,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"614615a57b14fdf700b96dca","name":"guzunya","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"6118ea8ab3484289bace553b","username":"alakablam","display_name":"Alakablam","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/241bba47-c8e7-4712-910c-c3c513b7a56f-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/614615a57b14fdf700b96dca","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":43,"height":32,"frame_count":71,"size":18323,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":43,"height":32,"frame_count":71,"size":60626,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":86,"height":64,"frame_count":71,"size":47373,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":86,"height":64,"frame_count":71,"size":141188,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":129,"height":96,"frame_count":71,"size":91466,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":129,"height":96,"frame_count":71,"size":228252,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":172,"height":128,"frame_count":71,"size":154372,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":172,"height":128,"frame_count":71,"size":283304,"format":"WEBP"}]}}},{"id":"65289a280282011b87739a14","name":"bench0","flags":1,"timestamp":1697630264021,"actor_id":"60ae518c0e35477634c151f1","data":{"id":"65289a280282011b87739a14","name":"lift0","flags":256,"tags":["billy","gachi"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60b030c1b3e1671e27940d52","username":"mellen","display_name":"mellen","avatar_url":"//cdn.7tv.app/user/60b030c1b3e1671e27940d52/av_64f429b5592e30195214ca0b/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/65289a280282011b87739a14","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":96,"height":32,"frame_count":10,"size":9174,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":96,"height":32,"frame_count":10,"size":8820,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":192,"height":64,"frame_count":10,"size":15554,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":192,"height":64,"frame_count":10,"size":13554,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":288,"height":96,"frame_count":10,"size":23402,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":288,"height":96,"frame_count":10,"size":20518,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":384,"height":128,"frame_count":10,"size":25835,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":384,"height":128,"frame_count":10,"size":23192,"format":"WEBP"}]}}},{"id":"60f0544991085fa44058758f","name":"vegan","flags":0,"timestamp":1697643281915,"actor_id":"60ae518c0e35477634c151f1","data":{"id":"60f0544991085fa44058758f","name":"vegan","flags":0,"lifecycle":3,"state":["NO_PERSONAL"],"listed":false,"animated":true,"owner":{"id":"60ae653c9627f9aff4f5ccd1","username":"xoo_6119","display_name":"xoo_6119","avatar_url":"//cdn.7tv.app/user/60ae653c9627f9aff4f5ccd1/av_63ca0eccdedb49b24383ae5c/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60f0544991085fa44058758f","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":90,"size":27001,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":90,"size":86936,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":90,"size":63886,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":90,"size":170068,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":90,"size":101430,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":90,"size":270654,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":90,"size":158681,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":90,"size":295472,"format":"WEBP"}]}}},{"id":"62e94cec80419c32e6821ea1","name":"MONKE","flags":0,"timestamp":1697646157607,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"62e94cec80419c32e6821ea1","name":"MONKE","flags":0,"tags":["monkey","ape","mods"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae3dd8aee2aa55382a4b5b","username":"benastro","display_name":"benASTRO","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/f8cf25de-bbb0-4e7d-9ca5-14e9a15fbd56-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/62e94cec80419c32e6821ea1","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":158,"size":61272,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":158,"size":126292,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":158,"size":127304,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":158,"size":250206,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":158,"size":204453,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":158,"size":397584,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":158,"size":271736,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":158,"size":427868,"format":"WEBP"}]}}},{"id":"64fa14d59524349b3b8b0aac","name":"CitiesSkylines","flags":0,"timestamp":1697730879172,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"64fa14d59524349b3b8b0aac","name":"CitiesSkylines","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"61f18ad3f933d586cddaa141","username":"hikoeu","display_name":"HikoEU","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/151bce6c-d165-48fb-9dc4-fbb6c4e1296a-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64fa14d59524349b3b8b0aac","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":2,"size":3212,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":2,"size":1856,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":2,"size":5476,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":2,"size":3956,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":2,"size":8202,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":2,"size":6272,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":2,"size":11337,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":2,"size":8380,"format":"WEBP"}]}}},{"id":"6420b3f9fdd6b1a122191cbf","name":"tayPls","flags":0,"timestamp":1697743229078,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"6420b3f9fdd6b1a122191cbf","name":"tayPls","flags":0,"tags":["taylor","swift"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae3e98b2ecb0150535c6b7","username":"gempir","display_name":"gempir","avatar_url":"//cdn.7tv.app/pp/60ae3e98b2ecb0150535c6b7/4aa1786cec024098be20d7b0683bae72","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6420b3f9fdd6b1a122191cbf","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":28,"height":32,"frame_count":345,"size":155661,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":28,"height":32,"frame_count":345,"size":240998,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":56,"height":64,"frame_count":345,"size":391084,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":56,"height":64,"frame_count":345,"size":524968,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":84,"height":96,"frame_count":345,"size":715622,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":84,"height":96,"frame_count":345,"size":826410,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":112,"height":128,"frame_count":345,"size":1084178,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":112,"height":128,"frame_count":345,"size":1134476,"format":"WEBP"}]}}},{"id":"64fb2926af8646950b5865d9","name":"nymnLooking","flags":1,"timestamp":1697743282283,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"64fb2926af8646950b5865d9","name":"nymnLooking","flags":256,"tags":["looking","nymn"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"63fb9249a27fda24e806d1cc","username":"abithappy","display_name":"abithappy","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/f7a46513-21a8-46ff-8ef2-d388dc069e8c-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64fb2926af8646950b5865d9","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":60,"height":32,"frame_count":1,"size":987,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":60,"height":32,"frame_count":1,"size":1122,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":120,"height":64,"frame_count":1,"size":1552,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":120,"height":64,"frame_count":1,"size":3006,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":180,"height":96,"frame_count":1,"size":2402,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":180,"height":96,"frame_count":1,"size":5590,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":240,"height":128,"frame_count":1,"size":3140,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":240,"height":128,"frame_count":1,"size":8102,"format":"WEBP"}]}}},{"id":"62f8c9d9b3c3b0ac703cd1b0","name":"Princess","flags":0,"timestamp":1697806601066,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"62f8c9d9b3c3b0ac703cd1b0","name":"Princess","flags":0,"tags":["pitbull","pet","dog","goodboy","puppy","hedoesntbiteiswear"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60f39e8bc07d1ac193652def","username":"shmovy","display_name":"Shmovy","avatar_url":"//cdn.7tv.app/user/60f39e8bc07d1ac193652def/av_63a4e84f5c2aba9b3b60bf46/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/62f8c9d9b3c3b0ac703cd1b0","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":48,"height":32,"frame_count":1,"size":1590,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":48,"height":32,"frame_count":1,"size":2230,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":96,"height":64,"frame_count":1,"size":3451,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":96,"height":64,"frame_count":1,"size":6776,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":144,"height":96,"frame_count":1,"size":5813,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":144,"height":96,"frame_count":1,"size":13218,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":192,"height":128,"frame_count":1,"size":8208,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":192,"height":128,"frame_count":1,"size":20636,"format":"WEBP"}]}}},{"id":"60ae681a117ec68ca4fc95c2","name":"YOURMOM","flags":0,"timestamp":1697820947825,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"60ae681a117ec68ca4fc95c2","name":"YOURMOM","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae57730e354776340c504f","username":"ronic76","display_name":"Ronic76","avatar_url":"//cdn.7tv.app/pp/60ae57730e354776340c504f/d8774f25d7cd49c18763691a00f8eaf6","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60ae681a117ec68ca4fc95c2","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":21,"size":6505,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":21,"size":10724,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":21,"size":10944,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":21,"size":19390,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":21,"size":16944,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":21,"size":29332,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":21,"size":21314,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":21,"size":31352,"format":"WEBP"}]}}},{"id":"6532ba3e24551a57d9a0b060","name":"1528","flags":0,"timestamp":1697823410349,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"6532ba3e24551a57d9a0b060","name":"1528","flags":0,"tags":["speedrun","godgamer","forsensmug","forsen","minecraft"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae3cb1b2ecb0150521fa1f","username":"waterboiledpizza","display_name":"WaterBoiledPizza","avatar_url":"//cdn.7tv.app/user/60ae3cb1b2ecb0150521fa1f/av_652806843e9323c51e05082e/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6532ba3e24551a57d9a0b060","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":57,"height":32,"frame_count":300,"size":166782,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":57,"height":32,"frame_count":300,"size":169778,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":114,"height":64,"frame_count":300,"size":448829,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":114,"height":64,"frame_count":300,"size":429256,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":171,"height":96,"frame_count":300,"size":783936,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":171,"height":96,"frame_count":300,"size":720706,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":228,"height":128,"frame_count":300,"size":1154149,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":228,"height":128,"frame_count":300,"size":1043016,"format":"WEBP"}]}}},{"id":"650b591d714d1753923814f2","name":"docOOOO","flags":0,"timestamp":1697823934616,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"650b591d714d1753923814f2","name":"OOOO","flags":0,"tags":["his","face","off","pagging"],"lifecycle":3,"state":["PERSONAL","LISTED"],"listed":true,"animated":true,"owner":{"id":"61ce216cf644a864b441c7fb","username":"fistymart","display_name":"FistyMart","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/fistymart-profile_image-63bb6503cd5238a7-70x70.jpeg","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/650b591d714d1753923814f2","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":48,"height":32,"frame_count":296,"size":147778,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":48,"height":32,"frame_count":296,"size":258184,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":96,"height":64,"frame_count":296,"size":395421,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":96,"height":64,"frame_count":296,"size":543636,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":144,"height":96,"frame_count":296,"size":635360,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":144,"height":96,"frame_count":296,"size":896030,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":192,"height":128,"frame_count":296,"size":1228060,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":192,"height":128,"frame_count":296,"size":1305730,"format":"WEBP"}]}}},{"id":"648dc5af0ee2f9ddd76306da","name":"cutecatpet","flags":0,"timestamp":1697983493587,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"648dc5af0ee2f9ddd76306da","name":"cutecatpet","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"614cb75a6251d7e000da4ce7","username":"eljugay","display_name":"eljuGay","avatar_url":"//cdn.7tv.app/user/614cb75a6251d7e000da4ce7/av_648f957bb3fdb6379f1e9b9b/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/648dc5af0ee2f9ddd76306da","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":96,"size":31938,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":96,"size":50916,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":96,"size":78592,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":96,"size":129006,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":96,"size":119224,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":96,"size":204822,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":96,"size":180847,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":96,"size":329308,"format":"WEBP"}]}}},{"id":"623a1fef51efa34ad8512bd5","name":"PotPls","flags":0,"timestamp":1698065151299,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"623a1fef51efa34ad8512bd5","name":"PotPls","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"623a1fab77e5ecc084903bf7","username":"ductile_goose","display_name":"ductile_goose","avatar_url":"//static-cdn.jtvnw.net/user-default-pictures-uv/cdd517fe-def4-11e9-948e-784f43822e80-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/623a1fef51efa34ad8512bd5","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":10,"size":8347,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":10,"size":10552,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":10,"size":16362,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":10,"size":25586,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":10,"size":27563,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":10,"size":45446,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":10,"size":38994,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":10,"size":57232,"format":"WEBP"}]}}},{"id":"65368bce45b19ffda80fcc2d","name":"mondayChat","flags":0,"timestamp":1698073578192,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"65368bce45b19ffda80fcc2d","name":"mondayChat","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae3e3eb2ecb01505346ae9","username":"fawcan","display_name":"Fawcan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38051bdb-83c6-4716-95e0-731462e02b45-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/65368bce45b19ffda80fcc2d","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":96,"height":10,"frame_count":1,"size":1047,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":96,"height":10,"frame_count":1,"size":1090,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":192,"height":20,"frame_count":1,"size":2952,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":192,"height":20,"frame_count":1,"size":2727,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":288,"height":30,"frame_count":1,"size":5922,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":288,"height":30,"frame_count":1,"size":5129,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":384,"height":40,"frame_count":1,"size":8104,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":384,"height":40,"frame_count":1,"size":6830,"format":"WEBP"}]}}},{"id":"626bac5655df243a4fa819cd","name":"parasocial","flags":0,"timestamp":1698349550070,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"626bac5655df243a4fa819cd","name":"parasocial","flags":0,"tags":["nerd"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60ae3e3eb2ecb01505346ae9","username":"fawcan","display_name":"Fawcan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38051bdb-83c6-4716-95e0-731462e02b45-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/626bac5655df243a4fa819cd","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1416,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1022,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2366,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2294,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3114,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":3070,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":3888,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":4158,"format":"WEBP"}]}}},{"id":"63c615ccfc866ebbc80ab61a","name":"soEepy","flags":0,"timestamp":1698393579296,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"63c615ccfc866ebbc80ab61a","name":"soEepy","flags":0,"tags":["eepy","sleepy","tired","asleep","soeepy"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"612a00433bf9791535a8f79c","username":"d_uc","display_name":"d_uc","avatar_url":"//cdn.7tv.app/user/612a00433bf9791535a8f79c/av_638d80c743e5e1ae07e695b1/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63c615ccfc866ebbc80ab61a","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":152,"size":52239,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":151,"size":69134,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":152,"size":118356,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":152,"size":169822,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":152,"size":188755,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":152,"size":278456,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":152,"size":260018,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":152,"size":392058,"format":"WEBP"}]}}},{"id":"652173aa35c8eec3eb42ac0d","name":"Despair","flags":0,"timestamp":1698393647885,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"652173aa35c8eec3eb42ac0d","name":"Despair","flags":0,"tags":["apollo","nymn"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"604137ab96832ffa784e1164","username":"bacond_","display_name":"Bacond_","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/e800e132-9d8f-4188-856a-ae17a061f0c3-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/652173aa35c8eec3eb42ac0d","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":35,"height":32,"frame_count":1,"size":915,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":35,"height":32,"frame_count":1,"size":1690,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":70,"height":64,"frame_count":1,"size":1557,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":70,"height":64,"frame_count":1,"size":5020,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":105,"height":96,"frame_count":1,"size":2324,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":105,"height":96,"frame_count":1,"size":9806,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":140,"height":128,"frame_count":1,"size":3036,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":140,"height":128,"frame_count":1,"size":15356,"format":"WEBP"}]}}},{"id":"60bb1989778afa8ce9828378","name":"CatNotLikeThisMeow","flags":0,"timestamp":1698507733994,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60bb1989778afa8ce9828378","name":"CatNotLikeThisMeow","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60b04e1b3cadd71dff96af6e","username":"dekuu__","display_name":"Dekuu__","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/f2c40c31-9d68-481c-bc1c-6f19796e552f-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60bb1989778afa8ce9828378","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":60,"size":22373,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":60,"size":39434,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":60,"size":54325,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":60,"size":88666,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":60,"size":86193,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":60,"size":145714,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":60,"size":115446,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":60,"size":96486,"format":"WEBP"}]}}},{"id":"651d5c78e5e8e598e85f8e5c","name":"buhKisser","flags":0,"timestamp":1698511454794,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"651d5c78e5e8e598e85f8e5c","name":"buhKisser","flags":0,"tags":["boykisser","buh"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"613cb52b3f2fa247f2ed46ae","username":"pigswitched","display_name":"pigswitched","avatar_url":"//cdn.7tv.app/user/613cb52b3f2fa247f2ed46ae/av_65035387c9920b6284155ec6/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/651d5c78e5e8e598e85f8e5c","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1240,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1824,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2241,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":5196,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3353,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":9942,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4337,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":15234,"format":"WEBP"}]}}},{"id":"653e2eacee0d08affebcb55d","name":"soyKisser","flags":0,"timestamp":1698582376857,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"653e2eacee0d08affebcb55d","name":"soyKisser","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60ae434b5d3fdae58382926a","username":"ayyybubu","display_name":"ayyybubu","avatar_url":"//cdn.7tv.app/user/60ae434b5d3fdae58382926a/av_636fa88735a18fd8b17e7399/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","62d86a8419fdcf401421c5ae","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/653e2eacee0d08affebcb55d","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1533,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1936,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":3203,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":5796,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":5331,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":11440,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":8353,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":18738,"format":"WEBP"}]}}},{"id":"63ae2a9041eeaa66119a2ccd","name":"Erm","flags":0,"timestamp":1698583497701,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"63ae2a9041eeaa66119a2ccd","name":"Erm","flags":0,"tags":["erm","caterm","cat","cute","uhm","catstare"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"614cb75a6251d7e000da4ce7","username":"eljugay","display_name":"eljuGay","avatar_url":"//cdn.7tv.app/user/614cb75a6251d7e000da4ce7/av_648f957bb3fdb6379f1e9b9b/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63ae2a9041eeaa66119a2ccd","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":51,"size":13432,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":51,"size":35064,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":51,"size":26673,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":51,"size":70654,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":51,"size":44954,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":51,"size":109382,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":51,"size":66785,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":51,"size":149410,"format":"WEBP"}]}}},{"id":"62925d6be81cfdc30c905df1","name":"WideSnow","flags":1,"timestamp":1698670033481,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"62925d6be81cfdc30c905df1","name":"WideSnow","flags":256,"tags":["snowtime","sosnowy","snow","widesnowtime","wide"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60f5e290e57bec021618c4a4","username":"ansonx10","display_name":"AnsonX10","avatar_url":"//cdn.7tv.app/user/60f5e290e57bec021618c4a4/av_63617cc39018da6429bc0298/3x_static.webp","style":{"color":401323775},"roles":["60b3f1ea886e63449c5263b1","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/62925d6be81cfdc30c905df1","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":96,"height":32,"frame_count":250,"size":404858,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":96,"height":32,"frame_count":250,"size":723288,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":192,"height":64,"frame_count":250,"size":925168,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":192,"height":64,"frame_count":250,"size":1640538,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":288,"height":96,"frame_count":250,"size":1640095,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":288,"height":96,"frame_count":250,"size":2933596,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":384,"height":128,"frame_count":250,"size":1602881,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":384,"height":128,"frame_count":250,"size":1699058,"format":"WEBP"}]}}},{"id":"60f4b43cc07d1ac1937d06aa","name":"SoSnowy","flags":1,"timestamp":1698670043610,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60f4b43cc07d1ac1937d06aa","name":"SoSnowy","flags":256,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60b175f74faf982ecac92159","username":"isaiahdasparkler","display_name":"isaiahdasparkler","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/61feff5b-d4bd-4882-b5d4-bf760a1f6025-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60f4b43cc07d1ac1937d06aa","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":20,"size":8565,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":20,"size":7138,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":20,"size":13785,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":20,"size":16106,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":20,"size":24379,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":20,"size":35676,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":20,"size":27685,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":20,"size":38310,"format":"WEBP"}]}}},{"id":"64dca583b7ce014343af3f22","name":"needmoreboulets","flags":0,"timestamp":1698765923760,"actor_id":"60ae518c0e35477634c151f1","data":{"id":"64dca583b7ce014343af3f22","name":"needmoreboulets","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"620eb134e7b1f24a7a9a3b57","username":"heinz1g","display_name":"heinz1g","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/0fde30af-5f2d-405d-9e0b-c294c124e126-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64dca583b7ce014343af3f22","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":96,"height":32,"frame_count":113,"size":125254,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":96,"height":32,"frame_count":113,"size":152700,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":192,"height":64,"frame_count":113,"size":336013,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":192,"height":64,"frame_count":113,"size":435622,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":288,"height":96,"frame_count":113,"size":579468,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":288,"height":96,"frame_count":113,"size":775346,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":384,"height":128,"frame_count":113,"size":842941,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":384,"height":128,"frame_count":113,"size":1129118,"format":"WEBP"}]}}},{"id":"6536ca6545b19ffda80fd6e2","name":"HastalavistaBaby","flags":0,"timestamp":1698766082898,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"6536ca6545b19ffda80fd6e2","name":"HastalavistaBaby","flags":0,"tags":["seeyouinthenextbattlefield","boolets","isrhaul","hastalavista","dontneedmorebullets","thanksforallthebulletssir"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"61b317b26888fd316ae492e0","username":"sunowi","display_name":"Sunowi","avatar_url":"//cdn.7tv.app/user/61b317b26888fd316ae492e0/av_6593722a64bf757bb2166bdf/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6536ca6545b19ffda80fd6e2","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":35,"height":32,"frame_count":240,"size":89414,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":35,"height":32,"frame_count":240,"size":145026,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":70,"height":64,"frame_count":240,"size":215936,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":70,"height":64,"frame_count":240,"size":339942,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":105,"height":96,"frame_count":240,"size":414100,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":105,"height":96,"frame_count":240,"size":549964,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":140,"height":128,"frame_count":240,"size":699920,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":140,"height":128,"frame_count":240,"size":777108,"format":"WEBP"}]}}},{"id":"6541281823429708afa44d40","name":"buhFriend","flags":0,"timestamp":1698769281395,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"6541281823429708afa44d40","name":"buhFriend","flags":0,"tags":["buh","cat","guh","potfriend"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60ae434b5d3fdae58382926a","username":"ayyybubu","display_name":"ayyybubu","avatar_url":"//cdn.7tv.app/user/60ae434b5d3fdae58382926a/av_636fa88735a18fd8b17e7399/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","62d86a8419fdcf401421c5ae","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6541281823429708afa44d40","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1440,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1998,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2617,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":5884,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3716,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":10910,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4609,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":16720,"format":"WEBP"}]}}},{"id":"6431d3b16fba94182ee1ae42","name":"NAILSING","flags":0,"timestamp":1698828129995,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"6431d3b16fba94182ee1ae42","name":"NAILSING","flags":0,"tags":["monka","shake","youtube","shock","tears"],"lifecycle":3,"state":["PERSONAL","LISTED"],"listed":true,"animated":true,"owner":{"id":"60b0faaa8fb21a01bc3c0385","username":"enzo_supercraftz","display_name":"Enzo_SuperCraftZ","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6431d3b16fba94182ee1ae42","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":25,"height":32,"frame_count":32,"size":16750,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":25,"height":32,"frame_count":32,"size":23130,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":50,"height":64,"frame_count":32,"size":30341,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":50,"height":64,"frame_count":32,"size":44888,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":75,"height":96,"frame_count":32,"size":38025,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":75,"height":96,"frame_count":32,"size":63790,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":100,"height":128,"frame_count":32,"size":43523,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":100,"height":128,"frame_count":32,"size":82348,"format":"WEBP"}]}}},{"id":"640cf0feb2a921bacda40f57","name":"TolatosPleasePayYourChildSupport","flags":0,"timestamp":1698828291531,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"640cf0feb2a921bacda40f57","name":"PoroFamily","flags":0,"tags":["poropregnant","preggy","poro","porosad","family","pregnant"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60ae759bdf5735e04acb69d9","username":"hotbear1110","display_name":"HotBear1110","avatar_url":"//cdn.7tv.app/pp/60ae759bdf5735e04acb69d9/80e2b49378c14dc6914fde8cb72fa673","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/640cf0feb2a921bacda40f57","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":48,"height":32,"frame_count":1,"size":1733,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":48,"height":32,"frame_count":1,"size":2526,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":96,"height":64,"frame_count":1,"size":3842,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":96,"height":64,"frame_count":1,"size":7420,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":144,"height":96,"frame_count":1,"size":6235,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":144,"height":96,"frame_count":1,"size":14226,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":192,"height":128,"frame_count":1,"size":8915,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":192,"height":128,"frame_count":1,"size":22550,"format":"WEBP"}]}}},{"id":"61afbf6dffa9aba101bd4b4e","name":"Backous","flags":0,"timestamp":1698828311827,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"61afbf6dffa9aba101bd4b4e","name":"peepoCumsOnBackous","flags":0,"tags":["backous","peepo"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60b0ae0a007f7e8b0e674a70","username":"dagaugi","display_name":"DaGaugI","avatar_url":"//cdn.7tv.app/pp/60b0ae0a007f7e8b0e674a70/bb5d50a0c2d54f7ba7ba9c81b40de191","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61afbf6dffa9aba101bd4b4e","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":96,"height":23,"frame_count":1,"size":1855,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":96,"height":23,"frame_count":1,"size":1678,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":192,"height":46,"frame_count":1,"size":3763,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":192,"height":46,"frame_count":1,"size":4076,"format":"WEBP"},{"name":"3x.webp","static_name":"3x_static.webp","width":288,"height":69,"frame_count":1,"size":7036,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":288,"height":69,"frame_count":1,"size":5753,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":384,"height":92,"frame_count":1,"size":10328,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":384,"height":92,"frame_count":1,"size":7764,"format":"AVIF"}]}}},{"id":"647ba63fd4b5d6083e91db76","name":"PLAAAY","flags":0,"timestamp":1698834569375,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"647ba63fd4b5d6083e91db76","name":"PLAAAY","flags":0,"tags":["rat","play","bbvibe","lulebb","peppah","forsen"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60ae434b5d3fdae58382926a","username":"ayyybubu","display_name":"ayyybubu","avatar_url":"//cdn.7tv.app/user/60ae434b5d3fdae58382926a/av_636fa88735a18fd8b17e7399/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","62d86a8419fdcf401421c5ae","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/647ba63fd4b5d6083e91db76","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1323,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":2228,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2748,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":6882,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4075,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":12814,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5730,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":21248,"format":"WEBP"}]}}},{"id":"63664a29babd459d5c2bbd56","name":"peepoWinter","flags":0,"timestamp":1698858254437,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"63664a29babd459d5c2bbd56","name":"peepoWinter","flags":0,"tags":["peepo","xmas","winter","christmas","snow"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae434b5d3fdae58382926a","username":"ayyybubu","display_name":"ayyybubu","avatar_url":"//cdn.7tv.app/user/60ae434b5d3fdae58382926a/av_636fa88735a18fd8b17e7399/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","62d86a8419fdcf401421c5ae","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63664a29babd459d5c2bbd56","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":48,"size":14449,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":48,"size":35226,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":48,"size":50135,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":48,"size":103164,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":48,"size":99165,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":48,"size":178552,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":48,"size":177787,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":48,"size":257134,"format":"WEBP"}]}}},{"id":"653f4440ee0d08affebce574","name":"RUNNN","flags":0,"timestamp":1698871292926,"actor_id":"60ae518c0e35477634c151f1","data":{"id":"653f4440ee0d08affebce574","name":"RUNNpolentero","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"613ea0837b14fdf700b8aec2","username":"lukqbt","display_name":"lukqbt","avatar_url":"//cdn.7tv.app/user/613ea0837b14fdf700b8aec2/av_651a2d651ad886eb9efe010b/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/653f4440ee0d08affebce574","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":11,"size":7436,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":11,"size":5452,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":11,"size":10224,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":11,"size":14842,"format":"AVIF"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":11,"size":24417,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":11,"size":17288,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":11,"size":29698,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":11,"size":19936,"format":"WEBP"}]}}},{"id":"611cb0c5f20f644c3fadb992","name":"HYPERYump","flags":0,"timestamp":1698920194260,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"611cb0c5f20f644c3fadb992","name":"HYPERYump","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60afab3e99923bbe7f7f62bc","username":"mvrkzs","display_name":"MvrkZS","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/611cb0c5f20f644c3fadb992","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":10,"size":5987,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":10,"size":9634,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":10,"size":12661,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":10,"size":22664,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":10,"size":20316,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":10,"size":38022,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":10,"size":30450,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":10,"size":45064,"format":"WEBP"}]}}},{"id":"613c7d9a2d7724a96175c268","name":"peepoTalkbutpeepoisnottalking","flags":0,"timestamp":1698928419153,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"613c7d9a2d7724a96175c268","name":"peepoTalkbutpeepoisnottalking","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60aef51812d770149183f968","username":"caz1_","display_name":"Caz1_","avatar_url":"//cdn.7tv.app/pp/60aef51812d770149183f968/1c921aa97b5042ffa2b1bb73287b1337","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/613c7d9a2d7724a96175c268","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1160,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":870,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1971,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2014,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3067,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":3434,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4292,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":5338,"format":"WEBP"}]}}},{"id":"6543f2484c11f20d6c9b38d3","name":"nymnPotFaint","flags":0,"timestamp":1698951820100,"actor_id":"60ae8fc0ea50f43c9e3ae255","data":{"id":"6543f2484c11f20d6c9b38d3","name":"nymnFaint","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae8fc0ea50f43c9e3ae255","username":"agenttud","display_name":"agenttud","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/273db808-d42f-4dab-9b39-9780ef2777b0-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6543f2484c11f20d6c9b38d3","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":36,"height":32,"frame_count":39,"size":15228,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":36,"height":32,"frame_count":39,"size":25678,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":72,"height":64,"frame_count":39,"size":31561,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":72,"height":64,"frame_count":39,"size":53938,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":108,"height":96,"frame_count":39,"size":50971,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":108,"height":96,"frame_count":39,"size":83044,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":144,"height":128,"frame_count":39,"size":72495,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":144,"height":128,"frame_count":39,"size":113134,"format":"WEBP"}]}}},{"id":"622f189214f489808df699e6","name":"PoroDisco","flags":0,"timestamp":1699099495505,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"622f189214f489808df699e6","name":"PoroDisco","flags":0,"tags":["poro","porosad","poroshuffle","porodisco"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"61e126106e676399a0ff97ce","username":"newsmaxintern","display_name":"NewsmaxIntern","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/ebf8b450-cf6e-42d2-9f08-fb0c643743ce-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/622f189214f489808df699e6","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":26,"size":25619,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":26,"size":28252,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":26,"size":58265,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":26,"size":68890,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":26,"size":95329,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":26,"size":121044,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":26,"size":133783,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":26,"size":153290,"format":"WEBP"}]}}},{"id":"64804b36cde3496c398621f0","name":"PoroEZ","flags":0,"timestamp":1699099582386,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"64804b36cde3496c398621f0","name":"PoroEZ","flags":0,"tags":["poro"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"626404f9a456cdaf745f9d3b","username":"qulibyash","display_name":"qulibyash","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/qulibyash-profile_image-1ffd26dd101e419f-70x70.jpeg","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64804b36cde3496c398621f0","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1383,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1934,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2898,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":5850,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4467,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":11458,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5964,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":17030,"format":"WEBP"}]}}},{"id":"64d72da602c01a29ab954974","name":"EULKEKUNJEF","flags":0,"timestamp":1699099961091,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"64d72da602c01a29ab954974","name":"EULKEKUNJEF","flags":0,"tags":["forsen","lule","kekw"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"64cef037d6294fb8a544e5d6","username":"constantoscillations","display_name":"ConstantOscillations","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/169554ad-7662-4475-8b08-5f92c42d765e-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64d72da602c01a29ab954974","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1129,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":2016,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2110,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":6054,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3099,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":11652,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4591,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":18648,"format":"WEBP"}]}}},{"id":"651b31a2941bcb5a83193f25","name":"PerfectSolo","flags":0,"timestamp":1699195045940,"actor_id":"60ae518c0e35477634c151f1","data":{"id":"651b31a2941bcb5a83193f25","name":"PerfectSolo","flags":0,"tags":["rockband","perfect","solo","clonehero","guitarhero"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"62379be393f0c6c9106eb2a1","username":"krazygh","display_name":"KrazyGH","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/6409e95e-93b0-4c1b-b87e-846ea859722c-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/651b31a2941bcb5a83193f25","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":59,"height":32,"frame_count":1,"size":1038,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":59,"height":32,"frame_count":1,"size":824,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":118,"height":64,"frame_count":1,"size":2790,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":118,"height":64,"frame_count":1,"size":1935,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":177,"height":96,"frame_count":1,"size":5768,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":177,"height":96,"frame_count":1,"size":3314,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":236,"height":128,"frame_count":1,"size":4567,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":236,"height":128,"frame_count":1,"size":7164,"format":"WEBP"}]}}},{"id":"651b3094e5e8e598e85eca28","name":"AwesomeChoke","flags":0,"timestamp":1699195093271,"actor_id":"60ae518c0e35477634c151f1","data":{"id":"651b3094e5e8e598e85eca28","name":"AwesomeChoke","flags":0,"tags":["choke","guitarhero","rockband","clonehero"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"62379be393f0c6c9106eb2a1","username":"krazygh","display_name":"KrazyGH","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/6409e95e-93b0-4c1b-b87e-846ea859722c-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/651b3094e5e8e598e85eca28","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":60,"height":32,"frame_count":1,"size":1131,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":60,"height":32,"frame_count":1,"size":1068,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":120,"height":64,"frame_count":1,"size":2305,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":120,"height":64,"frame_count":1,"size":3644,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":180,"height":96,"frame_count":1,"size":4027,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":180,"height":96,"frame_count":1,"size":6280,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":240,"height":128,"frame_count":1,"size":5633,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":240,"height":128,"frame_count":1,"size":8360,"format":"WEBP"}]}}},{"id":"6547a97fd4c66f065550eb2c","name":"0Points","flags":0,"timestamp":1699195270426,"actor_id":"60ae518c0e35477634c151f1","data":{"id":"6547a97fd4c66f065550eb2c","name":"0Points","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae518c0e35477634c151f1","username":"fabulouspotato69","display_name":"FabulousPotato69","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6547a97fd4c66f065550eb2c","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":61,"height":32,"frame_count":1,"size":940,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":61,"height":32,"frame_count":1,"size":564,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":122,"height":64,"frame_count":1,"size":1590,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":122,"height":64,"frame_count":1,"size":1539,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":183,"height":96,"frame_count":1,"size":2890,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":183,"height":96,"frame_count":1,"size":2115,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":244,"height":128,"frame_count":1,"size":2763,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":244,"height":128,"frame_count":1,"size":4850,"format":"WEBP"}]}}},{"id":"632783ffb2e31c945a54434c","name":"docGuitar","flags":0,"timestamp":1699195886891,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"632783ffb2e31c945a54434c","name":"docGuitar","flags":0,"tags":["guitar","this","doc","drdisrespect"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"629b922fa20ff2b602b2f581","username":"arhamsaa","display_name":"ArhamSAA","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/44ce7a12-db10-4316-a54d-d083ed9db6f0-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/632783ffb2e31c945a54434c","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":60,"height":32,"frame_count":23,"size":15283,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":60,"height":32,"frame_count":23,"size":25006,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":120,"height":64,"frame_count":23,"size":49866,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":120,"height":64,"frame_count":23,"size":48356,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":180,"height":96,"frame_count":23,"size":87670,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":180,"height":96,"frame_count":23,"size":79376,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":240,"height":128,"frame_count":23,"size":215290,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":240,"height":128,"frame_count":23,"size":117444,"format":"WEBP"}]}}},{"id":"6548045ccf586d12ce2e7ae1","name":"PEEET","flags":0,"timestamp":1699281066144,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"6548045ccf586d12ce2e7ae1","name":"APLOPLO","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60ae434b5d3fdae58382926a","username":"ayyybubu","display_name":"ayyybubu","avatar_url":"//cdn.7tv.app/user/60ae434b5d3fdae58382926a/av_636fa88735a18fd8b17e7399/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","62d86a8419fdcf401421c5ae","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6548045ccf586d12ce2e7ae1","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":980,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1684,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1722,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":5250,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":2460,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":9956,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":3249,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":17106,"format":"WEBP"}]}}},{"id":"6548fceb6c3a42995caae6c4","name":"OMGAREYOUAREDDITORANDADISCORDMOD","flags":0,"timestamp":1699282200259,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"6548fceb6c3a42995caae6c4","name":"OMGAREYOUAREDDITORANDADISCORDMOD","flags":0,"lifecycle":3,"state":["NO_PERSONAL","LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae434b5d3fdae58382926a","username":"ayyybubu","display_name":"ayyybubu","avatar_url":"//cdn.7tv.app/user/60ae434b5d3fdae58382926a/av_636fa88735a18fd8b17e7399/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","62d86a8419fdcf401421c5ae","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6548fceb6c3a42995caae6c4","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":235,"size":50975,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":235,"size":138918,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":235,"size":148886,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":235,"size":309856,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":235,"size":296712,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":235,"size":498308,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":235,"size":467091,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":235,"size":670366,"format":"WEBP"}]}}},{"id":"654a596eb8a5c515ec1c93a9","name":"Focused","flags":0,"timestamp":1699371858791,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"654a596eb8a5c515ec1c93a9","name":"Focused","flags":0,"tags":["nymn","balding","hector"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"623bb7e3bc7636f2937da399","username":"papacristobal","display_name":"PapaCristobal","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/20dda529-d361-48aa-a593-d56d6c93dd22-profile_image-70x70.jpg","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/654a596eb8a5c515ec1c93a9","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":143,"size":14850,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":135,"size":39528,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":143,"size":27487,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":143,"size":149392,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":143,"size":51494,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":143,"size":262574,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":143,"size":90479,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":143,"size":423056,"format":"WEBP"}]}}},{"id":"654a5b6feec810124e2dc851","name":"nymnBang","flags":0,"timestamp":1699373871856,"actor_id":"60ae8fc0ea50f43c9e3ae255","data":{"id":"654a5b6feec810124e2dc851","name":"nymnBang","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae8fc0ea50f43c9e3ae255","username":"agenttud","display_name":"agenttud","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/273db808-d42f-4dab-9b39-9780ef2777b0-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/654a5b6feec810124e2dc851","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":15,"size":9112,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":15,"size":8908,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":15,"size":17750,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":15,"size":17518,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":15,"size":28142,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":15,"size":25910,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":15,"size":36648,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":15,"size":34158,"format":"WEBP"}]}}},{"id":"654cc5ef065a20194ab18dd3","name":"InstaPotFriend","flags":0,"timestamp":1699530384706,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"654cc5ef065a20194ab18dd3","name":"InstaPotFriend","flags":0,"tags":["potfriend","instapot"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"6220fc75b825598c205c9b50","username":"okense","display_name":"Okense","avatar_url":"//cdn.7tv.app/pp/6220fc75b825598c205c9b50/468b9e5fef53426fb99076438df891fa","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/654cc5ef065a20194ab18dd3","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1461,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1828,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2888,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":5074,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4585,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":9726,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":6582,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":15138,"format":"WEBP"}]}}},{"id":"654cdf283071760559efb03d","name":"ApolFriend","flags":0,"timestamp":1699536801984,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"654cdf283071760559efb03d","name":"ApolFriend","flags":0,"tags":["nymn","cat","pot","aploplo","apollo"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"6131d9de492022af58394453","username":"jerrythedoctor","display_name":"JerryTheDoctor","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/a4a6f511-4bc7-466b-a73d-f9dc242bdef9-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/654cdf283071760559efb03d","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1657,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":2096,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":3328,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":6098,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":5117,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":11544,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":6557,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":17902,"format":"WEBP"}]}}},{"id":"62b9f4b4cb8e6bebae27d963","name":"ForsenSingingAtYou","flags":0,"timestamp":1699628488593,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"62b9f4b4cb8e6bebae27d963","name":"ForsenSingingAtYou","flags":0,"tags":["forsenlookingatyou","forsene","forsencd","starege","stare"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae7717dc23eca68e6e13b9","username":"posturelesshobo","display_name":"PosturelessHobo","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/9c1c84f1-ed58-44a3-8815-613d69683361-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/62b9f4b4cb8e6bebae27d963","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":300,"size":80679,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":300,"size":235082,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":300,"size":268438,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":300,"size":591364,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":300,"size":583846,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":300,"size":1083172,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":300,"size":2257278,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":300,"size":2191378,"format":"WEBP"}]}}},{"id":"654e44d66c3a42995cabdd47","name":"apolloGrumpy","flags":0,"timestamp":1699630776309,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"654e44d66c3a42995cabdd47","name":"apolloGrumpy","flags":0,"tags":["apollo","cat","nymn"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"63e11dca559159f548f2b4a6","username":"marv_023","display_name":"marv_023","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/a5e6ea14-adcd-427b-ba15-3f7ec6b9d91e-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/654e44d66c3a42995cabdd47","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":33,"height":32,"frame_count":1,"size":1069,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":33,"height":32,"frame_count":1,"size":1950,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":66,"height":64,"frame_count":1,"size":1865,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":66,"height":64,"frame_count":1,"size":5876,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":99,"height":96,"frame_count":1,"size":2886,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":99,"height":96,"frame_count":1,"size":11660,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":132,"height":128,"frame_count":1,"size":3877,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":132,"height":128,"frame_count":1,"size":17776,"format":"WEBP"}]}}},{"id":"611f8a4eabdf5176a9794b2e","name":"SODAING","flags":0,"timestamp":1699824681513,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"611f8a4eabdf5176a9794b2e","name":"SODAING","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60b3ce20e08fe2d22d18f125","username":"razfinch","display_name":"RazFinch","avatar_url":"//cdn.7tv.app/pp/60b3ce20e08fe2d22d18f125/496740f813e14e14a1bc62855131ff83","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/611f8a4eabdf5176a9794b2e","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":94,"size":23745,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":94,"size":69024,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":94,"size":56040,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":94,"size":158322,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":94,"size":106956,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":94,"size":262516,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":94,"size":163648,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":94,"size":300236,"format":"WEBP"}]}}},{"id":"6553735a9e081c7db7cb5ea8","name":"apolloLove","flags":0,"timestamp":1699969876877,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"6553735a9e081c7db7cb5ea8","name":"apolloLove","flags":0,"tags":["nymn"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"643c881fd2f582316651c1ae","username":"spinynorman","display_name":"spinynorman","avatar_url":"//static-cdn.jtvnw.net/user-default-pictures-uv/294c98b5-e34d-42cd-a8f0-140b72fba9b0-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6553735a9e081c7db7cb5ea8","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":37,"height":32,"frame_count":1,"size":1331,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":37,"height":32,"frame_count":1,"size":2290,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":74,"height":64,"frame_count":1,"size":2602,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":74,"height":64,"frame_count":1,"size":6802,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":111,"height":96,"frame_count":1,"size":4031,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":111,"height":96,"frame_count":1,"size":13180,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":148,"height":128,"frame_count":1,"size":5541,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":148,"height":128,"frame_count":1,"size":21568,"format":"WEBP"}]}}},{"id":"610c3d9bd53540d5aad10a2f","name":"Nymntaughtme","flags":0,"timestamp":1700077247298,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"610c3d9bd53540d5aad10a2f","name":"Nymntaughtme","flags":0,"tags":["nymn","peeposmoke","forsen"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"610c3c6ed53540d5aad10a18","username":"kojikon","display_name":"kojikon","avatar_url":"//cdn.7tv.app/user/610c3c6ed53540d5aad10a18/av_656a5edae1df7ad680396513/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/610c3d9bd53540d5aad10a2f","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":33,"height":32,"frame_count":1,"size":1221,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":33,"height":32,"frame_count":1,"size":946,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":66,"height":64,"frame_count":1,"size":2563,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":66,"height":64,"frame_count":1,"size":2604,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":99,"height":96,"frame_count":1,"size":3785,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":99,"height":96,"frame_count":1,"size":4592,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":132,"height":128,"frame_count":1,"size":5786,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":132,"height":128,"frame_count":1,"size":6884,"format":"WEBP"}]}}},{"id":"651c3ed80812f4f6b96eef31","name":"danPanic","flags":0,"timestamp":1700150151755,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"651c3ed80812f4f6b96eef31","name":"danPanic","flags":0,"tags":["panic","dan","dansgaming"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"618bca3617e4d50afc0cd8ba","username":"marima_a","display_name":"marima_a","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/fb236a6f-3c29-4bda-b300-1f985ea0abae-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/651c3ed80812f4f6b96eef31","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":52,"size":31514,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":52,"size":33368,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":52,"size":76458,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":52,"size":72726,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":52,"size":138858,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":52,"size":119366,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":52,"size":204997,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":52,"size":170060,"format":"WEBP"}]}}},{"id":"64520d3c2f52b8e0606e3ff3","name":"notRime","flags":0,"timestamp":1700229385924,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"64520d3c2f52b8e0606e3ff3","name":"notRime","flags":0,"tags":["rime","russel","remon","troy","disguise"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"629994a247051898ec04cad2","username":"clarkpls","display_name":"clarkpls","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/880ec631-b5da-441d-9528-5902d39a5846-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64520d3c2f52b8e0606e3ff3","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1344,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1816,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2457,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":5196,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3900,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":9738,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5334,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":14876,"format":"WEBP"}]}}},{"id":"6558dc4e51da2a96e6f1cae0","name":"forsenClassic","flags":0,"timestamp":1700322407733,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"6558dc4e51da2a96e6f1cae0","name":"forsenClassic","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae434b5d3fdae58382926a","username":"ayyybubu","display_name":"ayyybubu","avatar_url":"//cdn.7tv.app/user/60ae434b5d3fdae58382926a/av_636fa88735a18fd8b17e7399/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","62d86a8419fdcf401421c5ae","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6558dc4e51da2a96e6f1cae0","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":37,"height":32,"frame_count":81,"size":25378,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":37,"height":32,"frame_count":81,"size":52800,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":74,"height":64,"frame_count":81,"size":70616,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":74,"height":64,"frame_count":81,"size":97572,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":111,"height":96,"frame_count":81,"size":119370,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":111,"height":96,"frame_count":81,"size":140866,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":148,"height":128,"frame_count":81,"size":169159,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":148,"height":128,"frame_count":81,"size":188474,"format":"WEBP"}]}}},{"id":"649c47fc3b4504dd621e735a","name":"LICK","flags":0,"timestamp":1700334021934,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"649c47fc3b4504dd621e735a","name":"LIZUN","flags":0,"tags":["segz","ebu","slime","cat","lick","60fps"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"62442d16030f9bf3f9d4caac","username":"viba","display_name":"VIBA","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/e52dd05a-0171-46a5-8b1e-4827d4cab818-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/649c47fc3b4504dd621e735a","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":57,"height":32,"frame_count":148,"size":55276,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":57,"height":32,"frame_count":148,"size":66112,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":114,"height":64,"frame_count":148,"size":130931,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":114,"height":64,"frame_count":148,"size":144776,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":171,"height":96,"frame_count":148,"size":284452,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":171,"height":96,"frame_count":148,"size":243454,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":228,"height":128,"frame_count":148,"size":537526,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":228,"height":128,"frame_count":148,"size":391632,"format":"WEBP"}]}}},{"id":"6532607b1ef5bf2c0c18554c","name":"WEIBOZO","flags":0,"timestamp":1700381675214,"actor_id":"60ae3e98b2ecb0150535c6b7","data":{"id":"6532607b1ef5bf2c0c18554c","name":"WEIBOZO","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"643326208bba52143026d5c0","username":"bonculars","display_name":"bonculars","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/aa5e4d21-2aa3-41b1-ab72-745ceb885281-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6532607b1ef5bf2c0c18554c","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":48,"height":32,"frame_count":1,"size":1614,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":48,"height":32,"frame_count":1,"size":3016,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":96,"height":64,"frame_count":1,"size":3515,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":96,"height":64,"frame_count":1,"size":9632,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":144,"height":96,"frame_count":1,"size":5916,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":144,"height":96,"frame_count":1,"size":18644,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":192,"height":128,"frame_count":1,"size":8301,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":192,"height":128,"frame_count":1,"size":29180,"format":"WEBP"}]}}},{"id":"60eefddf2c24e9e0e6ec9141","name":"peepoComfy","flags":0,"timestamp":1700394836817,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"60eefddf2c24e9e0e6ec9141","name":"peepoComfy","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60eef186119bd10947a2d8ba","username":"zjawa","display_name":"Zjawa","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/ab9efacb-da24-42bd-a9af-6f0fdf35a2a6-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60eefddf2c24e9e0e6ec9141","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":6,"size":3781,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":6,"size":4790,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":6,"size":11712,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":6,"size":6214,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":6,"size":19572,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":6,"size":9746,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":6,"size":15643,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":6,"size":24752,"format":"WEBP"}]}}},{"id":"6133eef7f1ff750fb9b4f437","name":"peepoSitHey","flags":0,"timestamp":1700394838793,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"6133eef7f1ff750fb9b4f437","name":"peepoSitHey","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60f0094ce48dc1dc2fa8f4d9","username":"sylviadark","display_name":"SylviaDark","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/5cbc36c8-c412-431b-aadf-f0faa5fa1cd6-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6133eef7f1ff750fb9b4f437","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":4,"size":3561,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":4,"size":2200,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":4,"size":5814,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":4,"size":4682,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":4,"size":8640,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":4,"size":7840,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":4,"size":11562,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":4,"size":9330,"format":"WEBP"}]}}},{"id":"63f3d4e304e4a9fd8ee12dc5","name":"Corncerned","flags":0,"timestamp":1700401515726,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"63f3d4e304e4a9fd8ee12dc5","name":"Corncerned","flags":0,"tags":["corn","concerned"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"62c9898f9882dfa63c8225ef","username":"jinkies___","display_name":"jinkies___","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63f3d4e304e4a9fd8ee12dc5","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1440,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1530,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2488,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":3648,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3684,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":5784,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4514,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":8326,"format":"WEBP"}]}}},{"id":"63eb04a088b87ef33e5f4e2f","name":"MovieNightTime","flags":0,"timestamp":1700415071717,"actor_id":"60ae518c0e35477634c151f1","data":{"id":"63eb04a088b87ef33e5f4e2f","name":"vacation","flags":0,"tags":["vacation","banned"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"613077317e4d1ca1b80250d6","username":"jaydeelol","display_name":"Jaydeelol","avatar_url":"//cdn.7tv.app/user/613077317e4d1ca1b80250d6/av_64222b3b2b8217416a238f40/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63eb04a088b87ef33e5f4e2f","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":96,"height":32,"frame_count":1,"size":1231,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":96,"height":32,"frame_count":1,"size":1502,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":192,"height":64,"frame_count":1,"size":2681,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":192,"height":64,"frame_count":1,"size":4548,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":288,"height":96,"frame_count":1,"size":4251,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":288,"height":96,"frame_count":1,"size":9188,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":384,"height":128,"frame_count":1,"size":5929,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":384,"height":128,"frame_count":1,"size":14202,"format":"WEBP"}]}}},{"id":"6357f6c3799befeb23daee61","name":"Lootge","flags":0,"timestamp":1700493207029,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"6357f6c3799befeb23daee61","name":"Lootge","flags":0,"tags":["loot","lootge","okayge"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"61001b09767a550afb0e443c","username":"knightmar3frame","display_name":"knightmar3frame","avatar_url":"//cdn.7tv.app/pp/61001b09767a550afb0e443c/0d071f004e69470ea9ac5a2f156b51f1","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6357f6c3799befeb23daee61","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":51,"height":32,"frame_count":1,"size":1994,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":51,"height":32,"frame_count":1,"size":3020,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":102,"height":64,"frame_count":1,"size":4144,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":102,"height":64,"frame_count":1,"size":9236,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":153,"height":96,"frame_count":1,"size":6167,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":153,"height":96,"frame_count":1,"size":17380,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":204,"height":128,"frame_count":1,"size":7712,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":204,"height":128,"frame_count":1,"size":27190,"format":"WEBP"}]}}},{"id":"641e6ae02632d8d9a76e9ca8","name":"mhm","flags":0,"timestamp":1700667332354,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"641e6ae02632d8d9a76e9ca8","name":"mhm","flags":0,"tags":["yep","yeah","yes","nodders","true","agreege"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60da2c98d91a60b97c39b3e2","username":"raidindawgz","display_name":"RaidinDawgZ","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/8002607d3deb0904-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/641e6ae02632d8d9a76e9ca8","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":37,"height":32,"frame_count":9,"size":3900,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":37,"height":32,"frame_count":9,"size":6462,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":74,"height":64,"frame_count":9,"size":4800,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":74,"height":64,"frame_count":9,"size":4678,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":111,"height":96,"frame_count":9,"size":6259,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":111,"height":96,"frame_count":9,"size":13092,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":148,"height":128,"frame_count":9,"size":5662,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":148,"height":128,"frame_count":9,"size":2518,"format":"WEBP"}]}}},{"id":"60f60522e57bec0216a04191","name":"POK","flags":0,"timestamp":1700683059258,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60f60522e57bec0216a04191","name":"pokeWhat","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60f600c5f7fdd1860a1555ad","username":"maltesaa","display_name":"Maltesaa","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/d8d2f014-8160-4c21-9f54-f659ed908a2a-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60f60522e57bec0216a04191","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":34,"height":32,"frame_count":1,"size":881,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":34,"height":32,"frame_count":1,"size":612,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":68,"height":64,"frame_count":1,"size":1258,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":68,"height":64,"frame_count":1,"size":1278,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":102,"height":96,"frame_count":1,"size":1558,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":102,"height":96,"frame_count":1,"size":1868,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":136,"height":128,"frame_count":1,"size":1998,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":136,"height":128,"frame_count":1,"size":2432,"format":"WEBP"}]}}},{"id":"635020cbdbe5d048c97a7c0c","name":"Bussin","flags":0,"timestamp":1700755473820,"actor_id":"60ae518c0e35477634c151f1","data":{"id":"635020cbdbe5d048c97a7c0c","name":"Bussin","flags":0,"tags":["food","cat","kitty","kitten","tasty","eat"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"6227b409b027edd02c8beede","username":"thefrostydealer","display_name":"thefrostydealer","avatar_url":"//cdn.7tv.app/pp/6227b409b027edd02c8beede/b169caceaaaf4e9c8d91fa44135036b2","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/635020cbdbe5d048c97a7c0c","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":40,"height":32,"frame_count":270,"size":84407,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":40,"height":32,"frame_count":270,"size":127730,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":80,"height":64,"frame_count":270,"size":190488,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":80,"height":64,"frame_count":270,"size":263164,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":120,"height":96,"frame_count":270,"size":321935,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":120,"height":96,"frame_count":270,"size":419048,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":160,"height":128,"frame_count":270,"size":466056,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":160,"height":128,"frame_count":270,"size":563372,"format":"WEBP"}]}}},{"id":"61d5dbb13d52bb5c33c4e21e","name":"docRant","flags":0,"timestamp":1700852645822,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"61d5dbb13d52bb5c33c4e21e","name":"docRant","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"611d01d4c7e1fe52005c1769","username":"qullo","display_name":"Qullo","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/91832498-a8dc-4f8c-9b7c-31f419b22bf7-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61d5dbb13d52bb5c33c4e21e","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":48,"height":32,"frame_count":74,"size":36859,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":48,"height":32,"frame_count":74,"size":93692,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":96,"height":64,"frame_count":74,"size":99699,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":96,"height":64,"frame_count":74,"size":234380,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":144,"height":96,"frame_count":74,"size":176519,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":144,"height":96,"frame_count":74,"size":415316,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":192,"height":128,"frame_count":74,"size":258375,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":192,"height":128,"frame_count":74,"size":572820,"format":"WEBP"}]}}},{"id":"63b7618da18a87a489293080","name":"notok","flags":0,"timestamp":1700854181514,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"63b7618da18a87a489293080","name":"notok","flags":0,"tags":["pepe","oke","okay","okey","hmm","meme"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae409daee2aa55383ebb4b","username":"tolatos","display_name":"tolatos","avatar_url":"//cdn.7tv.app/user/60ae409daee2aa55383ebb4b/av_657b75e8b0d945ef35823739/3x.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63b7618da18a87a489293080","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1125,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1360,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2082,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":3574,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3118,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":6754,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4085,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":8762,"format":"WEBP"}]}}},{"id":"643047cad5322886daf75b01","name":"stopChatting","flags":0,"timestamp":1701007480567,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"643047cad5322886daf75b01","name":"stopChatting","flags":0,"tags":["talking","subs","guy","chatting","not","stop"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"61ce216cf644a864b441c7fb","username":"fistymart","display_name":"FistyMart","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/fistymart-profile_image-63bb6503cd5238a7-70x70.jpeg","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/643047cad5322886daf75b01","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":46,"height":32,"frame_count":116,"size":33094,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":46,"height":32,"frame_count":116,"size":57546,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":92,"height":64,"frame_count":116,"size":84244,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":92,"height":64,"frame_count":116,"size":156320,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":138,"height":96,"frame_count":116,"size":132082,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":138,"height":96,"frame_count":116,"size":253412,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":184,"height":128,"frame_count":116,"size":299929,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":184,"height":128,"frame_count":116,"size":398106,"format":"WEBP"}]}}},{"id":"60b28bb86078b7f956e7ee5f","name":"Danki","flags":0,"timestamp":1701013988330,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"60b28bb86078b7f956e7ee5f","name":"Danki","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60b28a4a4f32610f15d19e61","username":"xaeriia","display_name":"xAeriia","avatar_url":"//cdn.7tv.app/user/60b28a4a4f32610f15d19e61/av_647bb5415579ae9e28079f9e/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60b28bb86078b7f956e7ee5f","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1198,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":792,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1956,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":1804,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":2966,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":2946,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":3615,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":3564,"format":"WEBP"}]}}},{"id":"60e5bbe5a69fc8d27f4d3fe5","name":"ICANT","flags":0,"timestamp":1701172568388,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60e5bbe5a69fc8d27f4d3fe5","name":"ICANT","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60be726ef0eea79ec817e8f8","username":"dor4k","display_name":"dor4k","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/955765bd-1c71-409f-a2d0-15e1ca31997b-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60e5bbe5a69fc8d27f4d3fe5","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1589,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1174,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":3081,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2844,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4584,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4760,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":6240,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":6722,"format":"WEBP"}]}}},{"id":"63139322bdf4c4798bed8885","name":"o7","flags":0,"timestamp":1701364041094,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"63139322bdf4c4798bed8885","name":"o7","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"62c5cb16004dd4ed9b4bf89d","username":"forcevibe","display_name":"forcevibe","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/0a5c930c-6d8d-49ee-8bbf-45cb3989693d-profile_image-70x70.jpeg","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63139322bdf4c4798bed8885","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":34,"height":32,"frame_count":1,"size":1403,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":34,"height":32,"frame_count":1,"size":1788,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":68,"height":64,"frame_count":1,"size":2596,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":68,"height":64,"frame_count":1,"size":4864,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":102,"height":96,"frame_count":1,"size":3915,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":102,"height":96,"frame_count":1,"size":9008,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":136,"height":128,"frame_count":1,"size":5258,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":136,"height":128,"frame_count":1,"size":13858,"format":"WEBP"}]}}},{"id":"61b670a76906591ea6f2005b","name":"SCHIZO","flags":0,"timestamp":1701381684631,"actor_id":"60ae518c0e35477634c151f1","data":{"id":"61b670a76906591ea6f2005b","name":"SCHIZO","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"61792e90b0bfad942896803e","username":"hubbles","display_name":"hubbles","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/aa6f2257-1d63-4c36-88b8-98af152b2d4f-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61b670a76906591ea6f2005b","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":40,"size":18397,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":40,"size":31318,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":40,"size":50810,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":40,"size":79604,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":40,"size":85557,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":40,"size":134692,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":40,"size":127021,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":40,"size":115894,"format":"WEBP"}]}}},{"id":"61f884289f7bac13c42dc979","name":"docLeave","flags":0,"timestamp":1701386349272,"actor_id":"60ae518c0e35477634c151f1","data":{"id":"61f884289f7bac13c42dc979","name":"DocLeave","flags":0,"tags":["jebaited"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60fd819cfdd2c8ea2df2ee5f","username":"tichyou","display_name":"tichyou","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/cf87a61e-3556-469c-a47e-60db0858ee1d-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61f884289f7bac13c42dc979","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":153,"size":54660,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":153,"size":115926,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":153,"size":154108,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":153,"size":285322,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":153,"size":262531,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":153,"size":496188,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":153,"size":396400,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":153,"size":580456,"format":"WEBP"}]}}},{"id":"63ce753dec685e58d1731778","name":"GULP","flags":0,"timestamp":1701468774487,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"63ce753dec685e58d1731778","name":"GULP","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60d192bccb23a983f0cc6672","username":"tuneira","display_name":"tuneira","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/1c9cc985-24a9-442d-84a1-60c7a7bbf388-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63ce753dec685e58d1731778","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":21,"size":4582,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":21,"size":7070,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":21,"size":7306,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":21,"size":14326,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":21,"size":12273,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":21,"size":22242,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":21,"size":19253,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":21,"size":31368,"format":"WEBP"}]}}},{"id":"61fdd144690425de3c640404","name":"Bits-100","flags":0,"timestamp":1701518310498,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"61fdd144690425de3c640404","name":"Bits-100","flags":0,"tags":["bits","twitch","twitchbits","donate"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60bcd84e191fa96982c2bc08","username":"squeezyyyy","display_name":"sQueezyyyy","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/1d0e830c-5ae2-4573-b99e-1e858735cd1b-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61fdd144690425de3c640404","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":77,"size":27102,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":76,"size":55870,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":77,"size":54292,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":77,"size":114422,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":77,"size":95627,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":77,"size":190312,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":77,"size":136000,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":77,"size":259208,"format":"WEBP"}]}}},{"id":"6462aa765070b2cda24f368a","name":"Moo","flags":0,"timestamp":1701518339614,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"6462aa765070b2cda24f368a","name":"moo","flags":0,"tags":["meow","wantattention","pleasehelp","imscared","sadcat","cutekitty"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60f39e8bc07d1ac193652def","username":"shmovy","display_name":"Shmovy","avatar_url":"//cdn.7tv.app/user/60f39e8bc07d1ac193652def/av_63a4e84f5c2aba9b3b60bf46/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6462aa765070b2cda24f368a","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":178,"size":52631,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":178,"size":121314,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":178,"size":101841,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":178,"size":255324,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":178,"size":157603,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":178,"size":395612,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":178,"size":213403,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":178,"size":546190,"format":"WEBP"}]}}},{"id":"65639a7b95e5d35c9f23f686","name":"id","flags":0,"timestamp":1701523007237,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"65639a7b95e5d35c9f23f686","name":"iidiot","flags":0,"tags":["tudou","idiot","cat"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60aed4be4a34e31452493b7e","username":"fookstee","display_name":"Fookstee","avatar_url":"//cdn.7tv.app/user/60aed4be4a34e31452493b7e/av_6563b4f1558736959c301ab8/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/65639a7b95e5d35c9f23f686","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":58,"size":16564,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":58,"size":27136,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":58,"size":37889,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":58,"size":64078,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":58,"size":55096,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":58,"size":95568,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":58,"size":81322,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":58,"size":140552,"format":"WEBP"}]}}},{"id":"656b5ac4b818b44d6d6f3b87","name":"Nymnunculus","flags":0,"timestamp":1701534459018,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"656b5ac4b818b44d6d6f3b87","name":"Nymnunculus","flags":0,"tags":["nymn"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae3e3eb2ecb01505346ae9","username":"fawcan","display_name":"Fawcan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38051bdb-83c6-4716-95e0-731462e02b45-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/656b5ac4b818b44d6d6f3b87","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":64,"height":32,"frame_count":1,"size":1716,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":64,"height":32,"frame_count":1,"size":2414,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":128,"height":64,"frame_count":1,"size":3655,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":128,"height":64,"frame_count":1,"size":7108,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":192,"height":96,"frame_count":1,"size":5867,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":192,"height":96,"frame_count":1,"size":13934,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":256,"height":128,"frame_count":1,"size":8813,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":256,"height":128,"frame_count":1,"size":20410,"format":"WEBP"}]}}},{"id":"656b84e26faf97ff5fbc45bf","name":"PopCorncerned","flags":0,"timestamp":1701545760647,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"656b84e26faf97ff5fbc45bf","name":"PopCorncerned","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"62c6fdf7a7ffd3f6119c4735","username":"merscever","display_name":"merscever","avatar_url":"//static-cdn.jtvnw.net/user-default-pictures-uv/cdd517fe-def4-11e9-948e-784f43822e80-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/656b84e26faf97ff5fbc45bf","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":59,"height":32,"frame_count":28,"size":12496,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":59,"height":32,"frame_count":28,"size":15790,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":118,"height":64,"frame_count":28,"size":21197,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":118,"height":64,"frame_count":28,"size":32668,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":177,"height":96,"frame_count":28,"size":33342,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":177,"height":96,"frame_count":28,"size":48000,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":236,"height":128,"frame_count":28,"size":49969,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":236,"height":128,"frame_count":28,"size":59796,"format":"WEBP"}]}}},{"id":"617666d0ffc7244d797d214f","name":"Smile","flags":0,"timestamp":1701715883543,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"617666d0ffc7244d797d214f","name":"Smile","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"6114f77c446a415801b1a923","username":"tristeaf","display_name":"TristeAF","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/144ede91-46ed-462a-ac2f-58245f9b570d-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/617666d0ffc7244d797d214f","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":876,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1113,"format":"AVIF"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1836,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2014,"format":"WEBP"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":3390,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":2615,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":3428,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":5158,"format":"WEBP"}]}}},{"id":"64af93182b9b9a7b4ba03808","name":"plinkVibe","flags":0,"timestamp":1702041205202,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"64af93182b9b9a7b4ba03808","name":"plinkVibe","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"63901e3e5f5382bf533770a8","username":"kennypatron","display_name":"kennypatron","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/e7cf301a-0099-48fa-af9b-191b3bbee277-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64af93182b9b9a7b4ba03808","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":80,"height":32,"frame_count":33,"size":20390,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":80,"height":32,"frame_count":33,"size":26198,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":160,"height":64,"frame_count":33,"size":49144,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":160,"height":64,"frame_count":33,"size":50820,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":240,"height":96,"frame_count":33,"size":88483,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":240,"height":96,"frame_count":33,"size":86808,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":320,"height":128,"frame_count":33,"size":143704,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":320,"height":128,"frame_count":33,"size":116516,"format":"WEBP"}]}}},{"id":"622fa7be53c308389363aa9a","name":"nymnFriend","flags":0,"timestamp":1702060648946,"actor_id":"60ae8fc0ea50f43c9e3ae255","data":{"id":"622fa7be53c308389363aa9a","name":"NymNFriend","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae8fc0ea50f43c9e3ae255","username":"agenttud","display_name":"agenttud","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/273db808-d42f-4dab-9b39-9780ef2777b0-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/622fa7be53c308389363aa9a","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1435,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1098,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2743,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2780,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4434,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":5028,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":6403,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":8146,"format":"WEBP"}]}}},{"id":"646f5df34f0435ef0ae009a9","name":"NimeNoForsen","flags":0,"timestamp":1702129241931,"actor_id":"60ae518c0e35477634c151f1","data":{"id":"646f5df34f0435ef0ae009a9","name":"NimeNoForsen","flags":0,"tags":["nymn"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60afa8c899923bbe7f6e5a33","username":"trippycolour","display_name":"TrippyColour","avatar_url":"//cdn.7tv.app/user/60afa8c899923bbe7f6e5a33/av_6592e20b64e6ee62744a436c/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/646f5df34f0435ef0ae009a9","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1322,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1948,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":5568,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2490,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":10536,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3811,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5678,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":16874,"format":"WEBP"}]}}},{"id":"64c5a08efa9b960ee1185c83","name":"minusClean","flags":0,"timestamp":1702144169771,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"64c5a08efa9b960ee1185c83","name":"VeryClean","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"613bdc8feef63902454a4f2d","username":"maxdaxx","display_name":"maxdaxx","avatar_url":"//cdn.7tv.app/user/613bdc8feef63902454a4f2d/av_63b31dc35aa77eca61ec84b8/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64c5a08efa9b960ee1185c83","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":270,"size":72961,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":270,"size":154966,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":270,"size":149429,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":270,"size":260840,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":270,"size":240607,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":270,"size":359560,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":270,"size":326117,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":270,"size":461936,"format":"WEBP"}]}}},{"id":"63e3fb7a1d40a5212f9aeb11","name":":)))","flags":0,"timestamp":1702167791437,"actor_id":"60ae518c0e35477634c151f1","data":{"id":"63e3fb7a1d40a5212f9aeb11","name":":)))","flags":0,"tags":["smile","cute","happy","cat"],"lifecycle":3,"state":["NO_PERSONAL","LISTED"],"listed":true,"animated":true,"owner":{"id":"628e9497539b08d3d9084d98","username":"enviben","display_name":"enviben","avatar_url":"//cdn.7tv.app/user/628e9497539b08d3d9084d98/av_63ed520e7edaded517e06c95/3x_static.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63e3fb7a1d40a5212f9aeb11","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":33,"height":32,"frame_count":56,"size":16268,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":33,"height":32,"frame_count":56,"size":31352,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":66,"height":64,"frame_count":56,"size":41012,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":66,"height":64,"frame_count":56,"size":73364,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":99,"height":96,"frame_count":56,"size":76820,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":99,"height":96,"frame_count":56,"size":121236,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":132,"height":128,"frame_count":56,"size":131082,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":132,"height":128,"frame_count":56,"size":174418,"format":"WEBP"}]}}},{"id":"6574af6e0c7c0b8e18ab474a","name":"buhShakey","flags":0,"timestamp":1702230394815,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"6574af6e0c7c0b8e18ab474a","name":"buhShakey","flags":0,"tags":["buh","femboy"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"6574ad961a8b06735a3d93ea","username":"galadd","display_name":"galadd","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6574af6e0c7c0b8e18ab474a","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":33,"height":32,"frame_count":15,"size":7244,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":33,"height":32,"frame_count":15,"size":6372,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":66,"height":64,"frame_count":15,"size":14688,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":66,"height":64,"frame_count":15,"size":13576,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":99,"height":96,"frame_count":15,"size":22572,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":99,"height":96,"frame_count":15,"size":20804,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":132,"height":128,"frame_count":15,"size":44461,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":132,"height":128,"frame_count":15,"size":32802,"format":"WEBP"}]}}},{"id":"65538fe8c7791e2b4b3df928","name":"2023NymN","flags":0,"timestamp":1702304355871,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"65538fe8c7791e2b4b3df928","name":"2023NymN","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae409daee2aa55383ebb4b","username":"tolatos","display_name":"tolatos","avatar_url":"//cdn.7tv.app/user/60ae409daee2aa55383ebb4b/av_657b75e8b0d945ef35823739/3x.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/65538fe8c7791e2b4b3df928","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1177,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":2032,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2321,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":5872,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3431,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":10590,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4785,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":16820,"format":"WEBP"}]}}},{"id":"63fb91a9187076203e580359","name":"aNIME","flags":0,"timestamp":1702304439474,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"63fb91a9187076203e580359","name":"aNIME","flags":0,"tags":["weeb","exposed","forsen","nymn","anime","nime"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60b6746f64faf92496330d3c","username":"littlescampi","display_name":"LittleScampi","avatar_url":"//cdn.7tv.app/pp/60b6746f64faf92496330d3c/a1ff043123944a119d69cf9a7062a442","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63fb91a9187076203e580359","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":33,"height":32,"frame_count":1,"size":1205,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":33,"height":32,"frame_count":1,"size":2110,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":66,"height":64,"frame_count":1,"size":2403,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":66,"height":64,"frame_count":1,"size":6630,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":99,"height":96,"frame_count":1,"size":3482,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":99,"height":96,"frame_count":1,"size":12232,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":132,"height":128,"frame_count":1,"size":4905,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":132,"height":128,"frame_count":1,"size":20696,"format":"WEBP"}]}}},{"id":"613a285da2f37936d34177ff","name":"tyrissTail","flags":0,"timestamp":1702304571087,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"613a285da2f37936d34177ff","name":"tyrissTail","flags":0,"tags":["anime","tail","neko","tyriss","cute","ayaya"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60f1cac515758a7f9ad0df00","username":"ryujitakasu","display_name":"RyujiTakasu","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/9c89a3ec-05a3-4ab3-9c77-5f26cfd6dfea-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/613a285da2f37936d34177ff","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":14,"size":7586,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":14,"size":10576,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":14,"size":14330,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":14,"size":22380,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":14,"size":22995,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":14,"size":37544,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":14,"size":30656,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":14,"size":43912,"format":"WEBP"}]}}},{"id":"6157e7e793686fbfe7fbe2bd","name":"Sparkles","flags":1,"timestamp":1702304611446,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"6157e7e793686fbfe7fbe2bd","name":"Sparkles","flags":256,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60af971d12f90fadd6aa9ff8","username":"viscoito","display_name":"Viscoito","avatar_url":"//cdn.7tv.app/user/60af971d12f90fadd6aa9ff8/av_651d964e32b1db5b90eead40/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6157e7e793686fbfe7fbe2bd","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":50,"size":23029,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":50,"size":29866,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":50,"size":45822,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":50,"size":53042,"format":"WEBP"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":50,"size":83376,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":50,"size":73089,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":50,"size":82756,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":50,"size":88085,"format":"AVIF"}]}}},{"id":"62ad39d7c498e86eaf5e87af","name":"Blush","flags":1,"timestamp":1702304656225,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"62ad39d7c498e86eaf5e87af","name":"Blush","flags":256,"tags":["blush","shy","flushed","floshed"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"61ae535f15b3ff4a5bb9bdcc","username":"stereo_saiyan","display_name":"stereo_saiyan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/ab1ac98d-f38b-4e5b-8961-d0889b391160-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/62ad39d7c498e86eaf5e87af","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":924,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":742,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1393,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":1646,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":2207,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":2710,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":3014,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":4308,"format":"WEBP"}]}}},{"id":"621d1ff13808dfe5c4660745","name":"WeebsOut","flags":0,"timestamp":1702304822668,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"621d1ff13808dfe5c4660745","name":"WeebsOut","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"6042058896832ffa785800fe","username":"zhark","display_name":"Zhark","avatar_url":"//cdn.7tv.app/pp/6042058896832ffa785800fe/37ee95ffaa9846b286cb5554ff0716c5","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/621d1ff13808dfe5c4660745","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":57,"height":32,"frame_count":37,"size":24817,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":57,"height":32,"frame_count":37,"size":48318,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":114,"height":64,"frame_count":37,"size":65606,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":114,"height":64,"frame_count":37,"size":109460,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":171,"height":96,"frame_count":37,"size":117442,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":171,"height":96,"frame_count":37,"size":179454,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":228,"height":128,"frame_count":37,"size":166112,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":228,"height":128,"frame_count":37,"size":224736,"format":"WEBP"}]}}},{"id":"657777cea1c209046c08e210","name":"shakey0","flags":1,"timestamp":1702328296739,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"657777cea1c209046c08e210","name":"shakey0","flags":256,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae434b5d3fdae58382926a","username":"ayyybubu","display_name":"ayyybubu","avatar_url":"//cdn.7tv.app/user/60ae434b5d3fdae58382926a/av_636fa88735a18fd8b17e7399/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","62d86a8419fdcf401421c5ae","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/657777cea1c209046c08e210","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":33,"height":32,"frame_count":15,"size":7069,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":33,"height":32,"frame_count":15,"size":6210,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":66,"height":64,"frame_count":15,"size":13311,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":66,"height":64,"frame_count":15,"size":11136,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":99,"height":96,"frame_count":15,"size":19489,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":99,"height":96,"frame_count":15,"size":16996,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":132,"height":128,"frame_count":15,"size":27240,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":132,"height":128,"frame_count":15,"size":21484,"format":"WEBP"}]}}},{"id":"63f0212ae7b7262994ed5f38","name":"Pondering","flags":1,"timestamp":1702376113465,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"63f0212ae7b7262994ed5f38","name":"Pondering","flags":256,"tags":["life","looking","peepo"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"61e229e04f44b95f34665190","username":"f_r_o_n_g","display_name":"F_R_O_N_G","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63f0212ae7b7262994ed5f38","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":719,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":592,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":982,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":1584,"format":"WEBP"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":2374,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":1305,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":1536,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":3498,"format":"WEBP"}]}}},{"id":"630d3e623bb08262fb6c32fd","name":"catShake","flags":0,"timestamp":1702500237685,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"630d3e623bb08262fb6c32fd","name":"catShake","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60f635cbe57bec0216c302e8","username":"kamav9","display_name":"KamaV9","avatar_url":"//cdn.7tv.app/pp/60f635cbe57bec0216c302e8/f9768e1437af4c37931ad315f788306b","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/630d3e623bb08262fb6c32fd","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":14,"size":5683,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":14,"size":5860,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":14,"size":10725,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":14,"size":10572,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":14,"size":21282,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":14,"size":20956,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":14,"size":26512,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":14,"size":26134,"format":"WEBP"}]}}},{"id":"639e8ea05dca9c0bfcc29cb7","name":"SCATTER0","flags":1,"timestamp":1702560330643,"actor_id":"60ae3c29b2ecb015051f8f9a","data":{"id":"639e8ea05dca9c0bfcc29cb7","name":"SCATTER0","flags":256,"tags":["run","flee"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60eced11d3e38afa0682661b","username":"perry8782","display_name":"perry8782","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/d84e9d18-fa4c-4135-8ee5-11a13bb25250-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/639e8ea05dca9c0bfcc29cb7","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":96,"height":32,"frame_count":14,"size":18164,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":96,"height":32,"frame_count":14,"size":15500,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":192,"height":64,"frame_count":14,"size":41462,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":192,"height":64,"frame_count":14,"size":35664,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":288,"height":96,"frame_count":14,"size":71220,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":288,"height":96,"frame_count":14,"size":59374,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":384,"height":128,"frame_count":14,"size":128043,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":384,"height":128,"frame_count":14,"size":84706,"format":"WEBP"}]}}},{"id":"657b0228404cb98d28f6a031","name":"gettingjiggywithit","flags":0,"timestamp":1702560422746,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"657b0228404cb98d28f6a031","name":"gettingjiggywithit","flags":0,"tags":["dance","catjam","catvibe","chipi","chapa","chipichipi"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae434b5d3fdae58382926a","username":"ayyybubu","display_name":"ayyybubu","avatar_url":"//cdn.7tv.app/user/60ae434b5d3fdae58382926a/av_636fa88735a18fd8b17e7399/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","62d86a8419fdcf401421c5ae","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/657b0228404cb98d28f6a031","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":40,"height":32,"frame_count":24,"size":9812,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":40,"height":32,"frame_count":24,"size":13300,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":80,"height":64,"frame_count":24,"size":18566,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":80,"height":64,"frame_count":24,"size":25682,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":120,"height":96,"frame_count":24,"size":28238,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":120,"height":96,"frame_count":24,"size":38078,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":160,"height":128,"frame_count":24,"size":38861,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":160,"height":128,"frame_count":24,"size":51260,"format":"WEBP"}]}}},{"id":"656de6280e2cc09853775c70","name":"AlienParty","flags":0,"timestamp":1702562949807,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"656de6280e2cc09853775c70","name":"AlienParty","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae409daee2aa55383ebb4b","username":"tolatos","display_name":"tolatos","avatar_url":"//cdn.7tv.app/user/60ae409daee2aa55383ebb4b/av_657b75e8b0d945ef35823739/3x.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/656de6280e2cc09853775c70","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":89,"height":32,"frame_count":116,"size":301363,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":89,"height":32,"frame_count":116,"size":263938,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":178,"height":64,"frame_count":116,"size":806144,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":178,"height":64,"frame_count":116,"size":615588,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":267,"height":96,"frame_count":116,"size":1403931,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":267,"height":96,"frame_count":116,"size":1063722,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":356,"height":128,"frame_count":116,"size":2080923,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":356,"height":128,"frame_count":116,"size":1331912,"format":"WEBP"}]}}},{"id":"657ae27ed5e16dbf06d00821","name":"nymnVibe","flags":0,"timestamp":1702563432510,"actor_id":"60ae518c0e35477634c151f1","data":{"id":"657ae27ed5e16dbf06d00821","name":"nymnVibe","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60b78f631b94ba73134f0793","username":"realpiggypaps","display_name":"RealPiggyPaps","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/7f3607e7-d807-41ae-93f9-9ed3e0e8a6ea-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/657ae27ed5e16dbf06d00821","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":132,"size":34705,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":128,"size":58568,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":132,"size":77981,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":131,"size":139012,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":132,"size":129483,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":132,"size":216944,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":132,"size":182289,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":132,"size":305918,"format":"WEBP"}]}}},{"id":"6258ea1dc7b4050100611b23","name":"4House","flags":0,"timestamp":1702576797462,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"6258ea1dc7b4050100611b23","name":"4House","flags":0,"tags":["drhouse","doctor"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60aea03698f42914704af3ad","username":"mysztic","display_name":"MYSZTIC","avatar_url":"//cdn.7tv.app/user/60aea03698f42914704af3ad/av_6594935766ad24209ff2fe39/3x_static.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6258ea1dc7b4050100611b23","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1194,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1611,"format":"AVIF"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":3261,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2970,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4864,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":5080,"format":"WEBP"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":7702,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":6935,"format":"AVIF"}]}}},{"id":"657afdd4061cf22646a84228","name":"catAsk","flags":0,"timestamp":1702627740903,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"657afdd4061cf22646a84228","name":"catAsk","flags":0,"tags":["question","ask","cat","attention"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae434b5d3fdae58382926a","username":"ayyybubu","display_name":"ayyybubu","avatar_url":"//cdn.7tv.app/user/60ae434b5d3fdae58382926a/av_636fa88735a18fd8b17e7399/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","62d86a8419fdcf401421c5ae","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/657afdd4061cf22646a84228","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":21,"height":32,"frame_count":63,"size":20550,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":21,"height":32,"frame_count":63,"size":37168,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":42,"height":64,"frame_count":63,"size":47250,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":42,"height":64,"frame_count":63,"size":79512,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":63,"height":96,"frame_count":63,"size":73425,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":63,"height":96,"frame_count":63,"size":122250,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":84,"height":128,"frame_count":63,"size":102340,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":84,"height":128,"frame_count":63,"size":166624,"format":"WEBP"}]}}},{"id":"6266af7752691b69d9c624ab","name":"pepeWJAM","flags":0,"timestamp":1702658777624,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"6266af7752691b69d9c624ab","name":"pepeWJAM","flags":0,"tags":["pepew","jam"],"lifecycle":3,"state":["PERSONAL","LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae7143d8d99a9cf82e56d5","username":"gwinsen","display_name":"Gwinsen","avatar_url":"//cdn.7tv.app/pp/60ae7143d8d99a9cf82e56d5/82b4e3f32e7d44178b35bac3aa393ba5","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6266af7752691b69d9c624ab","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":18,"size":8886,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":18,"size":19832,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":18,"size":15470,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":18,"size":50226,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":18,"size":26926,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":18,"size":85978,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":18,"size":42856,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":18,"size":125304,"format":"WEBP"}]}}},{"id":"6571cc0f76838e1cababfb6b","name":"apolloDevoursYou","flags":0,"timestamp":1702658851697,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"6571cc0f76838e1cababfb6b","name":"apolloDevoursYou","flags":0,"tags":["apollo","nymn"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"643c881fd2f582316651c1ae","username":"spinynorman","display_name":"spinynorman","avatar_url":"//static-cdn.jtvnw.net/user-default-pictures-uv/294c98b5-e34d-42cd-a8f0-140b72fba9b0-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6571cc0f76838e1cababfb6b","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":32,"size":10616,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":33,"size":9026,"format":"AVIF"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":33,"size":15442,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":32,"size":20736,"format":"WEBP"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":33,"size":31876,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":33,"size":24589,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":33,"size":33738,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":33,"size":43452,"format":"WEBP"}]}}},{"id":"657cdd81fc59ded1d3164c92","name":"buhOverShakey","flags":0,"timestamp":1702682011019,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"657cdd81fc59ded1d3164c92","name":"buhOverShakey","flags":0,"tags":["buh","cokeshakey","pepsishakey","shakey","fast","overheat"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae434b5d3fdae58382926a","username":"ayyybubu","display_name":"ayyybubu","avatar_url":"//cdn.7tv.app/user/60ae434b5d3fdae58382926a/av_636fa88735a18fd8b17e7399/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","62d86a8419fdcf401421c5ae","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/657cdd81fc59ded1d3164c92","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":33,"height":32,"frame_count":3,"size":3028,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":33,"height":32,"frame_count":3,"size":1302,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":66,"height":64,"frame_count":3,"size":2738,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":66,"height":64,"frame_count":3,"size":5133,"format":"AVIF"},{"name":"3x.avif","static_name":"3x_static.avif","width":99,"height":96,"frame_count":3,"size":7611,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":99,"height":96,"frame_count":3,"size":4184,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":132,"height":128,"frame_count":3,"size":11627,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":132,"height":128,"frame_count":3,"size":5652,"format":"WEBP"}]}}},{"id":"61ecfa5acc9507d24fd4cd17","name":"+1","flags":0,"timestamp":1702736168187,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"61ecfa5acc9507d24fd4cd17","name":"1G","flags":0,"tags":["summit","one","gee"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"6114c18090ef9df34c9349fe","username":"arcchived","display_name":"Arcchived","avatar_url":"//cdn.7tv.app/","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61ecfa5acc9507d24fd4cd17","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1002,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":728,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1651,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":1562,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":2362,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":2696,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":2927,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":3328,"format":"WEBP"}]}}},{"id":"623506b6b88633b42c0c3532","name":"-1","flags":0,"timestamp":1702736215010,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"623506b6b88633b42c0c3532","name":"WTF","flags":0,"tags":["hewillnever","forsen","nymn"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60ec55f50f592e4d9d9a065e","username":"calamita","display_name":"カラミタ","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/1902ea18-b14e-4112-8260-98240e09d605-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/623506b6b88633b42c0c3532","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":940,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":654,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1775,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":1688,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":2731,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":2818,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":3807,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":3402,"format":"WEBP"}]}}},{"id":"6488e6ac11ffb819a6e41895","name":"BBidiot","flags":0,"timestamp":1702744004619,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"6488e6ac11ffb819a6e41895","name":"BBidiot","flags":0,"tags":["idiot","heartbrokendog","peppah","forsen","dog"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"63cbf23675acdcde61643eb9","username":"chrisscreams","display_name":"ChrisScreams","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/59818c70-68ca-4e12-9911-428b60f94629-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6488e6ac11ffb819a6e41895","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1780,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1926,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":3978,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":5866,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":6397,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":11426,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":10290,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":18482,"format":"WEBP"}]}}},{"id":"65299d8b582b6c8624866982","name":"ads","flags":0,"timestamp":1702832472090,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"65299d8b582b6c8624866982","name":"ads","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"6410e5edb716bba64280a17c","username":"rubenvatle","display_name":"RubenVatle","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/d8a41c8c-8f31-4748-9d95-6cc5858f7092-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/65299d8b582b6c8624866982","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":91,"height":32,"frame_count":1,"size":1248,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":91,"height":32,"frame_count":1,"size":1452,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":182,"height":64,"frame_count":1,"size":2384,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":182,"height":64,"frame_count":1,"size":3330,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":273,"height":96,"frame_count":1,"size":3076,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":273,"height":96,"frame_count":1,"size":5308,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":364,"height":128,"frame_count":1,"size":3944,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":364,"height":128,"frame_count":1,"size":8310,"format":"WEBP"}]}}},{"id":"65786bcff33524668d3c59b5","name":"dogSwing","flags":0,"timestamp":1702848248781,"actor_id":"60ae518c0e35477634c151f1","data":{"id":"65786bcff33524668d3c59b5","name":"dogSwing","flags":0,"tags":["happy","swing","xdd","dog"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"62365a49b88633b42c0c4892","username":"davidlxw","display_name":"davidlxw","avatar_url":"//cdn.7tv.app/user/62365a49b88633b42c0c4892/av_656f9aa29e9e8657200ddc2e/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/65786bcff33524668d3c59b5","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":113,"size":31552,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":113,"size":52450,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":113,"size":78259,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":113,"size":121274,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":113,"size":146361,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":113,"size":196926,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":113,"size":433892,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":113,"size":430850,"format":"WEBP"}]}}},{"id":"656f83128b22c0384d1607a2","name":"LMAO","flags":0,"timestamp":1702905127375,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"656f83128b22c0384d1607a2","name":"LMAO","flags":0,"tags":["pepe","funne","moonmoon","lmao"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"62f3cac2fb073fdaab609cfa","username":"femboyrell","display_name":"FemboyRell","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/5f4557fb-882b-4baf-abe0-f6b86b81235e-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/656f83128b22c0384d1607a2","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1370,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":2018,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2391,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":5922,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3452,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":10812,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4271,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":15794,"format":"WEBP"}]}}},{"id":"658055b2b34466cf0bafc4b8","name":"superjj","flags":0,"timestamp":1702924626457,"actor_id":"60ae518c0e35477634c151f1","data":{"id":"658055b2b34466cf0bafc4b8","name":"sjjKatze","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"6192ce26b1eb03daac7decc3","username":"matus_k6","display_name":"Matus_K6","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/9408e0ec-b87c-4b49-ac9d-80fb6e2d3026-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/658055b2b34466cf0bafc4b8","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":123,"size":27303,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":123,"size":67780,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":123,"size":69760,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":123,"size":146868,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":123,"size":134731,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":123,"size":222510,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":123,"size":211755,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":123,"size":294752,"format":"WEBP"}]}}},{"id":"637aa9624bc455c4ba37e936","name":"Banned","flags":0,"timestamp":1702927402810,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"637aa9624bc455c4ba37e936","name":"RIPVOD","flags":0,"tags":["vod","band","banned"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"6280f6826e007e074cb8a42f","username":"pilex96","display_name":"Pilex96","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/b8730635-40fb-4f5e-baff-a670d6508b88-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/637aa9624bc455c4ba37e936","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":64,"height":32,"frame_count":1,"size":986,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":64,"height":32,"frame_count":1,"size":946,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":128,"height":64,"frame_count":1,"size":1998,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":128,"height":64,"frame_count":1,"size":2486,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":192,"height":96,"frame_count":1,"size":3387,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":192,"height":96,"frame_count":1,"size":4258,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":256,"height":128,"frame_count":1,"size":4786,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":256,"height":128,"frame_count":1,"size":6426,"format":"WEBP"}]}}},{"id":"6582da34dbf474d8368c6e6e","name":"Nyoshi","flags":0,"timestamp":1703249244720,"actor_id":"60ae518c0e35477634c151f1","data":{"id":"6582da34dbf474d8368c6e6e","name":"Nyoshi","flags":0,"tags":["nymn","forsen","nam","yoshi","mario"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"610c3c6ed53540d5aad10a18","username":"kojikon","display_name":"kojikon","avatar_url":"//cdn.7tv.app/user/610c3c6ed53540d5aad10a18/av_656a5edae1df7ad680396513/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6582da34dbf474d8368c6e6e","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":44,"height":32,"frame_count":1,"size":1516,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":44,"height":32,"frame_count":1,"size":1500,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":88,"height":64,"frame_count":1,"size":2962,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":88,"height":64,"frame_count":1,"size":4216,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":132,"height":96,"frame_count":1,"size":4856,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":132,"height":96,"frame_count":1,"size":8018,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":176,"height":128,"frame_count":1,"size":6298,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":176,"height":128,"frame_count":1,"size":12694,"format":"WEBP"}]}}},{"id":"6256dc30b0dfc5aeb040f19a","name":"forsenOverlevel","flags":0,"timestamp":1703271630404,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"6256dc30b0dfc5aeb040f19a","name":"forsenOverlevel","flags":0,"tags":["forsen","level","nymn"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60ae3e3eb2ecb01505346ae9","username":"fawcan","display_name":"Fawcan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38051bdb-83c6-4716-95e0-731462e02b45-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6256dc30b0dfc5aeb040f19a","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":33,"height":32,"frame_count":1,"size":1163,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":33,"height":32,"frame_count":1,"size":944,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":66,"height":64,"frame_count":1,"size":2242,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":66,"height":64,"frame_count":1,"size":2354,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":99,"height":96,"frame_count":1,"size":3467,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":99,"height":96,"frame_count":1,"size":4226,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":132,"height":128,"frame_count":1,"size":4838,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":132,"height":128,"frame_count":1,"size":6486,"format":"WEBP"}]}}},{"id":"620425c25ccb247397667d59","name":"NOkey","flags":0,"timestamp":1703623389838,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"620425c25ccb247397667d59","name":"NOkey","flags":0,"tags":["notokay","xqcl"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae3d75aee2aa55382883c2","username":"victorbaya","display_name":"victorbaya","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/4f4c5649-c2f3-4837-a46f-486df3dde891-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/620425c25ccb247397667d59","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1693,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1356,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":3678,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":3466,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":5877,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":6052,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":8605,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":9294,"format":"WEBP"}]}}},{"id":"64358071b5534f4485d0d5f1","name":"FrankJam","flags":0,"timestamp":1703625239843,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"64358071b5534f4485d0d5f1","name":"FrankJam","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"64357faf461ddfc91d9807fa","username":"pepepge","display_name":"PepePge","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/eeff1b67-c4b6-410a-8451-1741cc64dc78-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64358071b5534f4485d0d5f1","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":2,"size":3007,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":2,"size":1624,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":2,"size":5048,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":2,"size":3364,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":2,"size":7527,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":2,"size":5108,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":2,"size":9802,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":2,"size":7026,"format":"WEBP"}]}}},{"id":"657c7bb282b9f8f92c7e8b49","name":"Frank","flags":0,"timestamp":1703625241637,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"657c7bb282b9f8f92c7e8b49","name":"Frank","flags":0,"tags":["feelsdonkman","donk"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ff054ffbd646ea3b221dc9","username":"tunari__","display_name":"Tunari__","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/bc530a7a-e04d-4765-a662-bb3efde482e2-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/657c7bb282b9f8f92c7e8b49","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1449,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1750,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2772,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":4678,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4436,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":8750,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":6084,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":13186,"format":"WEBP"}]}}},{"id":"64674a7358d599a0419f49d7","name":"CAUGHT","flags":0,"timestamp":1703633139185,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"64674a7358d599a0419f49d7","name":"CAUGHT","flags":0,"tags":["xyligun","reallymad","shiza","hands","emotiguy","monkah"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"61541c3c20eaf897465ad48b","username":"andreimonty","display_name":"AndreiMonty","avatar_url":"//cdn.7tv.app/user/61541c3c20eaf897465ad48b/av_6458e293d3b4256e12d830a9/3x.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64674a7358d599a0419f49d7","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":61,"height":32,"frame_count":1,"size":1799,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":61,"height":32,"frame_count":1,"size":2838,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":122,"height":64,"frame_count":1,"size":3423,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":122,"height":64,"frame_count":1,"size":8144,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":183,"height":96,"frame_count":1,"size":5544,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":183,"height":96,"frame_count":1,"size":16144,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":244,"height":128,"frame_count":1,"size":8139,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":244,"height":128,"frame_count":1,"size":12656,"format":"WEBP"}]}}},{"id":"614b201f0f25350dc5d7a3f5","name":"BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAT","flags":0,"timestamp":1703684030206,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"614b201f0f25350dc5d7a3f5","name":"BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAT","flags":0,"tags":["batchest","batpls","batdisco","hyper","forseninsane"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae81ff0bf2ee96aea05247","username":"snortexx","display_name":"snortexx","avatar_url":"//cdn.7tv.app/pp/60ae81ff0bf2ee96aea05247/183b9b6ab7624a53966fb782ec0963e0","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/614b201f0f25350dc5d7a3f5","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":8,"size":7073,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":8,"size":6652,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":8,"size":13877,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":8,"size":15020,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":8,"size":20637,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":8,"size":24652,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":8,"size":29208,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":8,"size":25858,"format":"WEBP"}]}}},{"id":"60bbf9d8585f017b2352b35e","name":"zyzzPls","flags":0,"timestamp":1703714472119,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60bbf9d8585f017b2352b35e","name":"zyzzPls","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60b1428c213e3888f9638acf","username":"senderak","display_name":"senderak","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/9d953c4e-3f61-48b8-8e45-08071276d03d-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60bbf9d8585f017b2352b35e","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":173,"size":124066,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":173,"size":165550,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":173,"size":283785,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":173,"size":368430,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":173,"size":446116,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":173,"size":616726,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":173,"size":618727,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":173,"size":703976,"format":"WEBP"}]}}},{"id":"657296ebb994cdd3baf21184","name":"st","flags":0,"timestamp":1703714489958,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"657296ebb994cdd3baf21184","name":"stupid","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"6318b1e329a5627b71e308e7","username":"flekyu","display_name":"flekyu","avatar_url":"//cdn.7tv.app/user/6318b1e329a5627b71e308e7/av_651e126332b1db5b90eedcc3/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/657296ebb994cdd3baf21184","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":42,"height":32,"frame_count":138,"size":39647,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":42,"height":32,"frame_count":138,"size":77014,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":84,"height":64,"frame_count":138,"size":82852,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":84,"height":64,"frame_count":138,"size":162924,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":126,"height":96,"frame_count":138,"size":137005,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":126,"height":96,"frame_count":138,"size":256108,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":168,"height":128,"frame_count":138,"size":190513,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":168,"height":128,"frame_count":138,"size":365898,"format":"WEBP"}]}}},{"id":"63187986537cb8092b6dbf8b","name":"sh","flags":0,"timestamp":1703765249027,"actor_id":"60ae518c0e35477634c151f1","data":{"id":"63187986537cb8092b6dbf8b","name":"shithead","flags":0,"tags":["kitten","cat","meme"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"619ebad96467596b1d63540a","username":"cbk_x","display_name":"CBK_x","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/31e2530f-dcd7-446b-8ed7-d9dc75b8a8c7-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63187986537cb8092b6dbf8b","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":72,"size":15214,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":72,"size":20734,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":72,"size":26956,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":72,"size":38728,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":72,"size":42584,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":72,"size":59984,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":72,"size":58043,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":72,"size":84046,"format":"WEBP"}]}}},{"id":"64c7b403d5a34030d08ee48a","name":"fu","flags":0,"timestamp":1703765765278,"actor_id":"60ae518c0e35477634c151f1","data":{"id":"64c7b403d5a34030d08ee48a","name":"lurk","flags":0,"tags":["lurking","meow","cat","kitty","kitten"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"6169d4c6474b9b7b59a37f56","username":"eropbl4_","display_name":"Eropbl4_","avatar_url":"//cdn.7tv.app/user/6169d4c6474b9b7b59a37f56/av_64305076b0b346d623214893/3x.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64c7b403d5a34030d08ee48a","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":122,"size":40610,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":122,"size":63718,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":122,"size":96196,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":122,"size":145220,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":122,"size":161258,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":122,"size":224090,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":122,"size":352359,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":122,"size":348360,"format":"WEBP"}]}}},{"id":"64156a097bb548a1f3de08c9","name":"nimeArrive","flags":0,"timestamp":1703766065881,"actor_id":"60ae518c0e35477634c151f1","data":{"id":"64156a097bb548a1f3de08c9","name":"nimeArrive","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60af8846a3648f409a124ee4","username":"kniteort","display_name":"Kniteort","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/05070f7c-ec6a-47cf-a274-54e062b11bf7-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64156a097bb548a1f3de08c9","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":23,"size":6509,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":23,"size":6550,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":23,"size":10341,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":23,"size":11262,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":23,"size":15681,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":23,"size":17092,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":23,"size":21508,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":23,"size":22126,"format":"WEBP"}]}}},{"id":"645fd62220a7827537905411","name":"coupleofleeches","flags":0,"timestamp":1703778794428,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"645fd62220a7827537905411","name":"coupleofleeches","flags":0,"tags":["vcu","leech","eddiehd","velcuz"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"62dfad28e2f69efc6a2c84b7","username":"esperdg","display_name":"EsperDG","avatar_url":"//cdn.7tv.app/user/62dfad28e2f69efc6a2c84b7/av_6515a414e66ad3b2e8846aab/3x.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/645fd62220a7827537905411","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":45,"height":32,"frame_count":1,"size":1858,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":45,"height":32,"frame_count":1,"size":2644,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":90,"height":64,"frame_count":1,"size":3999,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":90,"height":64,"frame_count":1,"size":8016,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":135,"height":96,"frame_count":1,"size":6230,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":135,"height":96,"frame_count":1,"size":15312,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":180,"height":128,"frame_count":1,"size":8470,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":180,"height":128,"frame_count":1,"size":24016,"format":"WEBP"}]}}},{"id":"634ff9f11d98780318b417cd","name":"peepoGiggles","flags":0,"timestamp":1703778940399,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"634ff9f11d98780318b417cd","name":"peepoGiggles","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"61ab1fed15b3ff4a5bb95735","username":"myzlivko","display_name":"Myzlivko","avatar_url":"//cdn.7tv.app/user/61ab1fed15b3ff4a5bb95735/av_63b742ed5f07129c6333abf4/3x_static.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/634ff9f11d98780318b417cd","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":6,"size":4155,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":6,"size":4592,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":6,"size":6137,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":6,"size":10080,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":6,"size":8987,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":6,"size":15662,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":6,"size":11857,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":6,"size":20414,"format":"WEBP"}]}}},{"id":"641f80c0fdd6b1a12218ff0f","name":"ASSEMBLE","flags":0,"timestamp":1703805722000,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"641f80c0fdd6b1a12218ff0f","name":"ASSEMBLE","flags":0,"tags":["assemble","arrive","streameroffline","gathering","scatter","peeposit"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"6150b6ea6251d7e000dab48c","username":"prototypezedd","display_name":"PrototypeZedd","avatar_url":"//cdn.7tv.app/user/6150b6ea6251d7e000dab48c/av_6518484d3e8ab458481945f5/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/641f80c0fdd6b1a12218ff0f","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":96,"height":24,"frame_count":28,"size":30671,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":96,"height":24,"frame_count":28,"size":31392,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":192,"height":48,"frame_count":28,"size":77580,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":192,"height":48,"frame_count":28,"size":64774,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":288,"height":72,"frame_count":28,"size":125346,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":288,"height":72,"frame_count":28,"size":106656,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":384,"height":96,"frame_count":28,"size":223096,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":384,"height":96,"frame_count":28,"size":145676,"format":"WEBP"}]}}},{"id":"61a457da15b3ff4a5bb83c6d","name":"Tomfoolery","flags":0,"timestamp":1703805791228,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"61a457da15b3ff4a5bb83c6d","name":"Tomfoolery","flags":0,"tags":["104x112"],"lifecycle":3,"state":["PERSONAL","LISTED"],"listed":true,"animated":false,"owner":{"id":"619b08dc70bd995987959bf9","username":"bttv_handshake_7tv","display_name":"bttv_handshake_7tv","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/0f547378-a3a7-400f-bb1e-c22bd41f131b-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61a457da15b3ff4a5bb83c6d","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":26,"height":32,"frame_count":1,"size":1090,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":26,"height":32,"frame_count":1,"size":898,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":52,"height":64,"frame_count":1,"size":1873,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":52,"height":64,"frame_count":1,"size":1948,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":78,"height":96,"frame_count":1,"size":2931,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":78,"height":96,"frame_count":1,"size":3398,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":104,"height":128,"frame_count":1,"size":3814,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":104,"height":128,"frame_count":1,"size":4610,"format":"WEBP"}]}}},{"id":"63779741da38f5d7f3d6d3db","name":"catErm","flags":0,"timestamp":1703805801102,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"63779741da38f5d7f3d6d3db","name":"catErm","flags":0,"tags":["meow","uhh","cat","stare","awkward","erm"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"629994a247051898ec04cad2","username":"clarkpls","display_name":"clarkpls","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/880ec631-b5da-441d-9528-5902d39a5846-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63779741da38f5d7f3d6d3db","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":123,"size":16043,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":122,"size":49198,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":123,"size":32962,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":123,"size":121696,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":123,"size":53207,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":123,"size":193398,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":123,"size":76818,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":123,"size":282340,"format":"WEBP"}]}}},{"id":"6460dd9f240cbc62de5f19f6","name":"meow","flags":0,"timestamp":1703805842759,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"6460dd9f240cbc62de5f19f6","name":"meow","flags":0,"tags":["meowing","catmeow","cat","kitty","kitten","gato"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"62efedc8fdc408d6e79c3fe5","username":"serapollyon","display_name":"SerApollyon","avatar_url":"//cdn.7tv.app/user/62efedc8fdc408d6e79c3fe5/av_63a87d07abdd8d304da43805/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6460dd9f240cbc62de5f19f6","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":200,"size":25486,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":169,"size":58750,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":200,"size":72760,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":200,"size":261454,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":200,"size":127138,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":200,"size":441814,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":200,"size":302716,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":200,"size":811002,"format":"WEBP"}]}}},{"id":"65442b2798e33b64b0468846","name":"rar","flags":0,"timestamp":1703805856280,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"65442b2798e33b64b0468846","name":"rar","flags":0,"tags":["cat","meow","wink","rawr"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"6196c5d8eecae7a725bbdfcd","username":"m4x0nn","display_name":"m4x0nn","avatar_url":"//cdn.7tv.app/pp/6196c5d8eecae7a725bbdfcd/3ba8c2fdd7f44f85a62fd353d8a24e25","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/65442b2798e33b64b0468846","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":146,"size":21269,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":146,"size":63140,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":146,"size":49822,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":146,"size":152252,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":146,"size":81334,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":146,"size":241236,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":146,"size":305498,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":146,"size":419944,"format":"WEBP"}]}}},{"id":"63acb7b5acef59270e7cf4b0","name":"TheVoices","flags":0,"timestamp":1703805869623,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"63acb7b5acef59270e7cf4b0","name":"TheVoices","flags":0,"tags":["cat","schizo","possessed","crazy"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae434b5d3fdae58382926a","username":"ayyybubu","display_name":"ayyybubu","avatar_url":"//cdn.7tv.app/user/60ae434b5d3fdae58382926a/av_636fa88735a18fd8b17e7399/3x.webp","style":{"color":-12171521},"roles":["6102002eab1aa12bf648cfcd","60724f65e93d828bf8858789","62d86a8419fdcf401421c5ae","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63acb7b5acef59270e7cf4b0","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":202,"size":53443,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":202,"size":114078,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":202,"size":102865,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":202,"size":205308,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":202,"size":165042,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":202,"size":284332,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":202,"size":228738,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":202,"size":369564,"format":"WEBP"}]}}},{"id":"6427e8a5c529cbb0bb3b18b4","name":"Pointless","flags":0,"timestamp":1703805888728,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"6427e8a5c529cbb0bb3b18b4","name":"Pointless","flags":0,"tags":["clueless","pain","despair","aware"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"6042058896832ffa785800fe","username":"zhark","display_name":"Zhark","avatar_url":"//cdn.7tv.app/pp/6042058896832ffa785800fe/37ee95ffaa9846b286cb5554ff0716c5","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6427e8a5c529cbb0bb3b18b4","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":35,"height":32,"frame_count":1,"size":1488,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":35,"height":32,"frame_count":1,"size":1614,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":70,"height":64,"frame_count":1,"size":3039,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":70,"height":64,"frame_count":1,"size":4138,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":105,"height":96,"frame_count":1,"size":4947,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":105,"height":96,"frame_count":1,"size":7766,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":140,"height":128,"frame_count":1,"size":7325,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":140,"height":128,"frame_count":1,"size":11330,"format":"WEBP"}]}}},{"id":"61e66081095be332e347e5a4","name":"kok","flags":0,"timestamp":1703806260027,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"61e66081095be332e347e5a4","name":"kok","flags":0,"tags":["cute","yep","cock","cat"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60b8e47b06e1b0897450b580","username":"mallairr","display_name":"MALLAIRR","avatar_url":"//cdn.7tv.app/user/60b8e47b06e1b0897450b580/av_646a7a0b6989b9b0d46b79b7/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61e66081095be332e347e5a4","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":33,"height":32,"frame_count":70,"size":12315,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":33,"height":32,"frame_count":70,"size":45208,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":66,"height":64,"frame_count":70,"size":31639,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":66,"height":64,"frame_count":70,"size":97588,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":99,"height":96,"frame_count":70,"size":61792,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":99,"height":96,"frame_count":70,"size":154750,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":132,"height":128,"frame_count":70,"size":98359,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":132,"height":128,"frame_count":70,"size":166080,"format":"WEBP"}]}}},{"id":"630db7e07b84e74996da9552","name":"Classic","flags":0,"timestamp":1703850113502,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"630db7e07b84e74996da9552","name":"classic","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ac827cc7188f3be2120450","username":"7jeo","display_name":"7JEO","avatar_url":"//cdn.7tv.app/pp/60ac827cc7188f3be2120450/60fc0761d7bb46a89a860da11114b9fc","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/630db7e07b84e74996da9552","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":41,"height":32,"frame_count":51,"size":20410,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":41,"height":32,"frame_count":51,"size":48216,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":82,"height":64,"frame_count":51,"size":55236,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":82,"height":64,"frame_count":51,"size":98966,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":123,"height":96,"frame_count":51,"size":102335,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":123,"height":96,"frame_count":51,"size":162674,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":164,"height":128,"frame_count":51,"size":186264,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":164,"height":128,"frame_count":51,"size":199974,"format":"WEBP"}]}}},{"id":"63ae8e510689991218cdcfee","name":"nnysAward","flags":0,"timestamp":1703875725544,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"63ae8e510689991218cdcfee","name":"nnysAward","flags":0,"tags":["nnys"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae3cb1b2ecb0150521fa1f","username":"waterboiledpizza","display_name":"WaterBoiledPizza","avatar_url":"//cdn.7tv.app/user/60ae3cb1b2ecb0150521fa1f/av_652806843e9323c51e05082e/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63ae8e510689991218cdcfee","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1154,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1648,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1925,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":4402,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":2934,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":8144,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":3678,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":12582,"format":"WEBP"}]}}},{"id":"63b201568730fddbe14d62be","name":"happiParty","flags":0,"timestamp":1703875795411,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"63b201568730fddbe14d62be","name":"happiParty","flags":0,"tags":["celebrate","happynewyear","plsdance","fireworks","celebration"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60bd5159a8c00a202bf7425f","username":"a_t_m_0_s","display_name":"A_T_M_0_S","avatar_url":"//cdn.7tv.app/user/60bd5159a8c00a202bf7425f/av_6482d40390f619d9af6922f1/3x.webp","style":{"color":849892095},"roles":["60724f65e93d828bf8858789","6076a86b09a4c63a38ebe801","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63b201568730fddbe14d62be","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":98,"size":62544,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":98,"size":81682,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":98,"size":154742,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":98,"size":177296,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":98,"size":262110,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":98,"size":279278,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":98,"size":364078,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":98,"size":377828,"format":"WEBP"}]}}},{"id":"60aeec1712d7701491f89cf5","name":"peepoHey","flags":0,"timestamp":1703877223114,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"60aeec1712d7701491f89cf5","name":"peepoHey","flags":0,"lifecycle":3,"state":["PERSONAL","LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae49350e35477634486602","username":"justrogan","display_name":"JustRogan","avatar_url":"//cdn.7tv.app/pp/60ae49350e35477634486602/88d6e3c4265f4be0a452c812c146da50","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60aeec1712d7701491f89cf5","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":6,"size":6460,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":6,"size":4185,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":6,"size":15734,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":6,"size":8030,"format":"AVIF"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":6,"size":12574,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":6,"size":26980,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":6,"size":19349,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":6,"size":31712,"format":"WEBP"}]}}},{"id":"658e4bd06ac2c09e4d867d20","name":"BASED","flags":0,"timestamp":1703885235134,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"658e4bd06ac2c09e4d867d20","name":"BASED","flags":0,"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"6053853cb4d31e459fdaa2dc","username":"laden","display_name":"Laden","avatar_url":"//cdn.7tv.app/pp/6053853cb4d31e459fdaa2dc/a94c67d7736940feb543e42024b740ef","style":{"color":849892095},"roles":["60724f65e93d828bf8858789","6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/658e4bd06ac2c09e4d867d20","files":[{"name":"1x.webp","static_name":"1x_static.webp","width":34,"height":32,"frame_count":1,"size":1366,"format":"WEBP"},{"name":"1x.avif","static_name":"1x_static.avif","width":34,"height":32,"frame_count":1,"size":1270,"format":"AVIF"},{"name":"2x.avif","static_name":"2x_static.avif","width":68,"height":64,"frame_count":1,"size":2087,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":68,"height":64,"frame_count":1,"size":3580,"format":"WEBP"},{"name":"3x.webp","static_name":"3x_static.webp","width":102,"height":96,"frame_count":1,"size":6368,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":102,"height":96,"frame_count":1,"size":2954,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":136,"height":128,"frame_count":1,"size":3741,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":136,"height":128,"frame_count":1,"size":8640,"format":"WEBP"}]}}},{"id":"60aeb6cce90f445e43c89540","name":"Based","flags":0,"timestamp":1703886055673,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60aeb6cce90f445e43c89540","name":"Based","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60aeb501955615deef869415","username":"froglin_","display_name":"Froglin_","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/5a020ff1-3768-4f68-8d75-d56f2dee8403-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60aeb6cce90f445e43c89540","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1093,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":786,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":1712,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1795,"format":"AVIF"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":2760,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":2728,"format":"WEBP"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":3468,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":3470,"format":"AVIF"}]}}},{"id":"63c2fcb567221ee072b9c47c","name":"docYell","flags":0,"timestamp":1703936047266,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"63c2fcb567221ee072b9c47c","name":"docYell","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"6319e48e29a5627b71e323f0","username":"slippindanny","display_name":"slippindanny","avatar_url":"//cdn.7tv.app/user/6319e48e29a5627b71e323f0/av_63eae041eb1af0564608ee0f/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63c2fcb567221ee072b9c47c","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":43,"height":32,"frame_count":38,"size":14425,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":43,"height":32,"frame_count":38,"size":27358,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":86,"height":64,"frame_count":38,"size":30676,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":86,"height":64,"frame_count":38,"size":53052,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":129,"height":96,"frame_count":38,"size":52209,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":129,"height":96,"frame_count":38,"size":75112,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":172,"height":128,"frame_count":38,"size":75442,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":172,"height":128,"frame_count":38,"size":95824,"format":"WEBP"}]}}},{"id":"63915e53209bcb04cf0aa45d","name":"(7TV)","flags":0,"timestamp":1703936472668,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"63915e53209bcb04cf0aa45d","name":"7tvM","flags":0,"tags":["seventv"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60f5e290e57bec021618c4a4","username":"ansonx10","display_name":"AnsonX10","avatar_url":"//cdn.7tv.app/user/60f5e290e57bec021618c4a4/av_63617cc39018da6429bc0298/3x_static.webp","style":{"color":401323775},"roles":["60b3f1ea886e63449c5263b1","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63915e53209bcb04cf0aa45d","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":947,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":514,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1168,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":1044,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":1717,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":1610,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":2057,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":2412,"format":"WEBP"}]}}},{"id":"612a60c9fef79a90b279b428","name":"GoslingClap","flags":0,"timestamp":1703956600951,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"612a60c9fef79a90b279b428","name":"GoslingClap","flags":0,"tags":["gosling","clap","ryan","drive","blade","runner"],"lifecycle":3,"state":["PERSONAL","LISTED"],"listed":true,"animated":true,"owner":{"id":"61193cfa8efc177ec41f09f9","username":"mentalsway","display_name":"MentalSway","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/20dff717-12cd-4e16-b124-10d56033b425-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/612a60c9fef79a90b279b428","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":9,"size":4174,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":9,"size":6514,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":9,"size":16134,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":9,"size":7852,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":9,"size":26614,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":9,"size":13541,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":9,"size":18866,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":9,"size":34948,"format":"WEBP"}]}}},{"id":"63e79f39743d199fec640849","name":"!join","flags":0,"timestamp":1703971587763,"actor_id":"60ae434b5d3fdae58382926a","data":{"id":"63e79f39743d199fec640849","name":"!join","flags":0,"tags":["xijinping","china"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae3e98b2ecb0150535c6b7","username":"gempir","display_name":"gempir","avatar_url":"//cdn.7tv.app/pp/60ae3e98b2ecb0150535c6b7/4aa1786cec024098be20d7b0683bae72","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63e79f39743d199fec640849","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":60,"height":32,"frame_count":104,"size":30917,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":60,"height":32,"frame_count":104,"size":67704,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":120,"height":64,"frame_count":104,"size":71561,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":120,"height":64,"frame_count":104,"size":118408,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":180,"height":96,"frame_count":104,"size":109221,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":180,"height":96,"frame_count":104,"size":164782,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":240,"height":128,"frame_count":104,"size":135455,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":240,"height":128,"frame_count":104,"size":206924,"format":"WEBP"}]}}},{"id":"655913a651da2a96e6f1d410","name":"2024NymN","flags":0,"timestamp":1704028196129,"actor_id":"60ae518c0e35477634c151f1","data":{"id":"655913a651da2a96e6f1d410","name":"NymNgal","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae409daee2aa55383ebb4b","username":"tolatos","display_name":"tolatos","avatar_url":"//cdn.7tv.app/user/60ae409daee2aa55383ebb4b/av_657b75e8b0d945ef35823739/3x.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/655913a651da2a96e6f1d410","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1121,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1700,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2157,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":4892,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3206,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":8772,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4513,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":13372,"format":"WEBP"}]}}},{"id":"6591c434489e26710946d33c","name":"yawN","flags":0,"timestamp":1704051918056,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"6591c434489e26710946d33c","name":"yawN","flags":0,"tags":["apollo","yawn","cat","nymn"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae3e3eb2ecb01505346ae9","username":"fawcan","display_name":"Fawcan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38051bdb-83c6-4716-95e0-731462e02b45-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6591c434489e26710946d33c","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":36,"height":32,"frame_count":141,"size":24104,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":36,"height":32,"frame_count":140,"size":48816,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":72,"height":64,"frame_count":141,"size":47728,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":72,"height":64,"frame_count":141,"size":107236,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":108,"height":96,"frame_count":141,"size":80939,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":108,"height":96,"frame_count":141,"size":169578,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":144,"height":128,"frame_count":141,"size":113979,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":144,"height":128,"frame_count":141,"size":231668,"format":"WEBP"}]}}},{"id":"6592018adbf474d8368def08","name":"FirstTimeDrama","flags":0,"timestamp":1704067635418,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"6592018adbf474d8368def08","name":"FirstTimeDrama","flags":0,"tags":["firsttime","pokimane","drama"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"60ae3e3eb2ecb01505346ae9","username":"fawcan","display_name":"Fawcan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38051bdb-83c6-4716-95e0-731462e02b45-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6592018adbf474d8368def08","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":64,"height":32,"frame_count":64,"size":16696,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":64,"height":32,"frame_count":64,"size":30200,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":128,"height":64,"frame_count":64,"size":52943,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":128,"height":64,"frame_count":64,"size":69196,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":192,"height":96,"frame_count":64,"size":103033,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":192,"height":96,"frame_count":64,"size":107740,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":256,"height":128,"frame_count":64,"size":190853,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":256,"height":128,"frame_count":64,"size":151800,"format":"WEBP"}]}}},{"id":"643215d2ab2e0a86b89ab9c3","name":"cumBo","flags":0,"timestamp":1704069622660,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"643215d2ab2e0a86b89ab9c3","name":"cumBo","flags":0,"tags":["moonmoon","cumbo","wisdom"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"643215bf8f3d9aa185267e1d","username":"patrickhaze","display_name":"PatrickHaze","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/patrickhaze-profile_image-ffdb8f022159bdc5-70x70.jpeg","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/643215d2ab2e0a86b89ab9c3","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1050,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1930,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2260,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":5668,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4058,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":11498,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5786,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":18740,"format":"WEBP"}]}}},{"id":"6477b0f70c7cd505faf2c1c9","name":"pu","flags":0,"timestamp":1704239586927,"actor_id":"60ae518c0e35477634c151f1","data":{"id":"6477b0f70c7cd505faf2c1c9","name":"CuteKitten","flags":0,"tags":["meow","myah","socute","kitty","cutecat","catwalk"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"614cb75a6251d7e000da4ce7","username":"eljugay","display_name":"eljuGay","avatar_url":"//cdn.7tv.app/user/614cb75a6251d7e000da4ce7/av_648f957bb3fdb6379f1e9b9b/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6477b0f70c7cd505faf2c1c9","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":322,"size":93436,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":322,"size":161824,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":322,"size":224834,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":322,"size":385032,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":322,"size":346351,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":322,"size":598638,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":322,"size":553038,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":322,"size":938848,"format":"WEBP"}]}}},{"id":"61c340c444cb589796afbb3a","name":"THIS","flags":0,"timestamp":1704282379417,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"61c340c444cb589796afbb3a","name":"THIS","flags":0,"tags":["weeb","lewd","booba"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"61ac447affa9aba101bcd05c","username":"leinad_osnola","display_name":"leinad_osnola","avatar_url":"//cdn.7tv.app/user/61ac447affa9aba101bcd05c/av_65939f8e52e38f90f334c199/3x_static.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61c340c444cb589796afbb3a","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":57,"height":32,"frame_count":16,"size":8293,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":57,"height":32,"frame_count":16,"size":18096,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":114,"height":64,"frame_count":16,"size":19273,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":114,"height":64,"frame_count":16,"size":40476,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":171,"height":96,"frame_count":16,"size":34005,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":171,"height":96,"frame_count":16,"size":65308,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":228,"height":128,"frame_count":16,"size":57238,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":228,"height":128,"frame_count":16,"size":60052,"format":"WEBP"}]}}},{"id":"62af8dd26f979a8714748dd2","name":"veryFors","flags":0,"timestamp":1704300301975,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"62af8dd26f979a8714748dd2","name":"veryFors","flags":0,"tags":["forsen","dvd","bad361","filmfestival","artifact","dlc"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"61e508463441abfa431cf11a","username":"mlgquikscp420","display_name":"mlgquikscp420","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/470234fe-4a9b-4f72-aed9-c68acdb83724-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/62af8dd26f979a8714748dd2","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":166,"size":32816,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":166,"size":143096,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":166,"size":69122,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":166,"size":305940,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":166,"size":118528,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":166,"size":496470,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":166,"size":271195,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":166,"size":757692,"format":"WEBP"}]}}},{"id":"6158902693686fbfe7fbf5ad","name":"Brazil","flags":0,"timestamp":1704300309976,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"6158902693686fbfe7fbf5ad","name":"Brazil","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae99afac03cad6076c6cf1","username":"sushiguh","display_name":"sushiguh","avatar_url":"//cdn.7tv.app/user/60ae99afac03cad6076c6cf1/av_656b9d423d10142edc61c2eb/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6158902693686fbfe7fbf5ad","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":86,"size":14286,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":86,"size":50008,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":86,"size":58577,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":86,"size":110318,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":86,"size":101709,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":86,"size":172522,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":86,"size":195248,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":86,"size":201206,"format":"WEBP"}]}}},{"id":"61e454f03441abfa431cd1c9","name":"7TV","flags":0,"timestamp":1704300485288,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"61e454f03441abfa431cd1c9","name":"dumpsterFire","flags":0,"tags":["dumpster","garbage","fire","crazy"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"61075734128aa13dd5eaf41a","username":"ader","display_name":"ader","avatar_url":"//cdn.7tv.app/user/61075734128aa13dd5eaf41a/av_63f59532f2915b442ca85cbd/3x_static.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61e454f03441abfa431cd1c9","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":16,"size":11008,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":16,"size":16286,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":16,"size":26780,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":16,"size":41706,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":16,"size":45291,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":16,"size":74700,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":16,"size":79638,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":16,"size":94470,"format":"WEBP"}]}}},{"id":"62530ebfb0dfc5aeb040acc2","name":"Shrugeg","flags":0,"timestamp":1704300838695,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"62530ebfb0dfc5aeb040acc2","name":"Shrugeg","flags":0,"tags":["shrug","okayeg"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"60aebfce6cfcffe15f119c18","username":"namtheweebs","display_name":"NaMTheWeebs","avatar_url":"//cdn.7tv.app/user/60aebfce6cfcffe15f119c18/av_6579da7eac03816ec8c078b9/3x.webp","style":{"color":849892095},"roles":["60724f65e93d828bf8858789","612c888812a39cc5cdd82ae0","6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/62530ebfb0dfc5aeb040acc2","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":39,"height":32,"frame_count":1,"size":1681,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":39,"height":32,"frame_count":1,"size":1310,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":78,"height":64,"frame_count":1,"size":3356,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":78,"height":64,"frame_count":1,"size":3162,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":117,"height":96,"frame_count":1,"size":4927,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":117,"height":96,"frame_count":1,"size":5226,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":156,"height":128,"frame_count":1,"size":6827,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":156,"height":128,"frame_count":1,"size":7814,"format":"WEBP"}]}}},{"id":"6042290277137b000de9e68d","name":"WideHardo","flags":0,"timestamp":1704301185325,"actor_id":"60ae759bdf5735e04acb69d9","data":{"id":"6042290277137b000de9e68d","name":"WideHardo","flags":0,"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"000000000000000000000000","username":"","display_name":"","style":{}},"host":{"url":"//cdn.7tv.app/emote/6042290277137b000de9e68d","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":96,"height":25,"frame_count":1,"size":1890,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":96,"height":25,"frame_count":1,"size":1750,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":192,"height":50,"frame_count":1,"size":3503,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":192,"height":50,"frame_count":1,"size":3952,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":288,"height":75,"frame_count":1,"size":5259,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":288,"height":75,"frame_count":1,"size":6426,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":384,"height":100,"frame_count":1,"size":7138,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":384,"height":100,"frame_count":1,"size":9450,"format":"WEBP"}]}}},{"id":"6595e92283833b99670ffb8d","name":"!nympts","flags":0,"timestamp":1704323380006,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"6595e92283833b99670ffb8d","name":"nymnCoin","flags":0,"tags":["donk","feelsdonkman","nymn","coin","donkcoin"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae3e3eb2ecb01505346ae9","username":"fawcan","display_name":"Fawcan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/38051bdb-83c6-4716-95e0-731462e02b45-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6595e92283833b99670ffb8d","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1486,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1876,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2723,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":4648,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4226,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":8572,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5514,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":13336,"format":"WEBP"}]}}},{"id":"62e25bfcd0dac916414d25cb","name":"angrE","flags":0,"timestamp":1704385162382,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"62e25bfcd0dac916414d25cb","name":"angrE","flags":0,"tags":["lule","angry","rage","mad","anger","forsen"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":false,"owner":{"id":"62dfad28e2f69efc6a2c84b7","username":"esperdg","display_name":"EsperDG","avatar_url":"//cdn.7tv.app/user/62dfad28e2f69efc6a2c84b7/av_6515a414e66ad3b2e8846aab/3x.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/62e25bfcd0dac916414d25cb","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1224,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1002,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2500,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2540,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4096,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4484,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":5428,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":6558,"format":"WEBP"}]}}},{"id":"658a6f8c1d1f4b980881c762","name":"LacariWide","flags":0,"timestamp":1704386084804,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"658a6f8c1d1f4b980881c762","name":"LacariWide","flags":0,"tags":["lacari","drakewide"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"6259b0ce85873c051292d20a","username":"croodini","display_name":"Croodini","avatar_url":"//cdn.7tv.app/user/6259b0ce85873c051292d20a/av_648ec2d5b3fdb6379f1e6a21/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/658a6f8c1d1f4b980881c762","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":59,"height":32,"frame_count":1,"size":1473,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":59,"height":32,"frame_count":1,"size":3160,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":118,"height":64,"frame_count":1,"size":2896,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":118,"height":64,"frame_count":1,"size":10166,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":177,"height":96,"frame_count":1,"size":4592,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":177,"height":96,"frame_count":1,"size":20284,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":236,"height":128,"frame_count":1,"size":6335,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":236,"height":128,"frame_count":1,"size":32738,"format":"WEBP"}]}}},{"id":"65413498dc0468e8c1fbcdc6","name":"hiii","flags":0,"timestamp":1704391602159,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"65413498dc0468e8c1fbcdc6","name":"hiii","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"63032593f7bf6645d6463dd7","username":"ockbephutejlb","display_name":"OcKBePHuTeJlb","avatar_url":"//cdn.7tv.app/user/63032593f7bf6645d6463dd7/av_6475966061d5da625fd8bb56/3x_static.webp","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/65413498dc0468e8c1fbcdc6","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":96,"height":32,"frame_count":8,"size":5892,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":96,"height":32,"frame_count":8,"size":3384,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":192,"height":64,"frame_count":8,"size":13357,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":192,"height":64,"frame_count":8,"size":7458,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":288,"height":96,"frame_count":8,"size":21392,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":288,"height":96,"frame_count":8,"size":11280,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":384,"height":128,"frame_count":8,"size":50942,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":384,"height":128,"frame_count":8,"size":18516,"format":"WEBP"}]}}},{"id":"6595df3083833b99670ffa40","name":"AlienApprove","flags":0,"timestamp":1704393714241,"actor_id":"60ae518c0e35477634c151f1","data":{"id":"6595df3083833b99670ffa40","name":"AlienPleased","flags":0,"tags":["please","pleased","thumbsup","alien","alienplease","alienpleasing"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"6319044a8e1ba8ff6ec1887d","username":"corgicam","display_name":"CorgiCam","avatar_url":"//cdn.7tv.app/user/6319044a8e1ba8ff6ec1887d/av_657b5bca4b9a7c23f770714b/3x.webp","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6595df3083833b99670ffa40","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1379,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1994,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2662,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":5878,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":4264,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":11736,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":6037,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":19554,"format":"WEBP"}]}}},{"id":"6404af5985fbda46564f38c4","name":"plenk","flags":0,"timestamp":1704457395216,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"6404af5985fbda46564f38c4","name":"plenk","flags":0,"tags":["basedgebot","plink","plonk","blink","cat","kot"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"62ca13e6a7ffd3f6119c7f6a","username":"howeverbot","display_name":"HoweverBot","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/47dbcfe4-08cf-459d-b90d-e33f10c767d7-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6404af5985fbda46564f38c4","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":67,"height":32,"frame_count":181,"size":33012,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":67,"height":32,"frame_count":181,"size":119098,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":134,"height":64,"frame_count":181,"size":98138,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":134,"height":64,"frame_count":181,"size":276494,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":201,"height":96,"frame_count":181,"size":280358,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":201,"height":96,"frame_count":181,"size":474814,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":268,"height":128,"frame_count":181,"size":623470,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":268,"height":128,"frame_count":181,"size":732886,"format":"WEBP"}]}}},{"id":"6597e7cabf3a5b70a4b2c5bb","name":"catBite","flags":0,"timestamp":1704471409849,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"6597e7cabf3a5b70a4b2c5bb","name":"catBite","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":true,"owner":{"id":"6187ed1e8d50b5f26ee83400","username":"pepewpert","display_name":"pepewpert","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/aa0769ce-5a81-4f78-9fce-f0a8e9d6b033-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6597e7cabf3a5b70a4b2c5bb","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":56,"size":26364,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":55,"size":27820,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":56,"size":57232,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":56,"size":61136,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":56,"size":91709,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":56,"size":93346,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":56,"size":125156,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":56,"size":127748,"format":"WEBP"}]}}},{"id":"64f0a894367809abe2382529","name":"GAGAGA","flags":0,"timestamp":1704471422758,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"64f0a894367809abe2382529","name":"GAGAGA","flags":0,"tags":["laughingatyou","hand","pointing","cat","wajaja"],"lifecycle":3,"state":["LISTED","NO_PERSONAL"],"listed":true,"animated":false,"owner":{"id":"6373b335d222154d7b7c8c29","username":"alchemicalpower","display_name":"Alchemicalpower","avatar_url":"//static-cdn.jtvnw.net/user-default-pictures-uv/ce57700a-def9-11e9-842d-784f43822e80-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/64f0a894367809abe2382529","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":33,"height":32,"frame_count":1,"size":1610,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":33,"height":32,"frame_count":1,"size":2546,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":66,"height":64,"frame_count":1,"size":7590,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":66,"height":64,"frame_count":1,"size":3114,"format":"AVIF"},{"name":"3x.avif","static_name":"3x_static.avif","width":99,"height":96,"frame_count":1,"size":4219,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":99,"height":96,"frame_count":1,"size":13812,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":132,"height":128,"frame_count":1,"size":5663,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":132,"height":128,"frame_count":1,"size":22462,"format":"WEBP"}]}}},{"id":"63c2563bb858b11b76ab7555","name":"xddShrug","flags":0,"timestamp":1704478474491,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"63c2563bb858b11b76ab7555","name":"xddShrug","flags":0,"tags":["shrug","xdd"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"62e3724644341b225c119238","username":"ultra_mp","display_name":"ULTRA_MP","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/bbf90e67-1360-4d2d-8292-f3ca4aee605c-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63c2563bb858b11b76ab7555","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":96,"height":32,"frame_count":1,"size":1889,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":96,"height":32,"frame_count":1,"size":3746,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":192,"height":64,"frame_count":1,"size":11100,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":192,"height":64,"frame_count":1,"size":3727,"format":"AVIF"},{"name":"3x.avif","static_name":"3x_static.avif","width":288,"height":96,"frame_count":1,"size":5585,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":288,"height":96,"frame_count":1,"size":20922,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":384,"height":128,"frame_count":1,"size":7826,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":384,"height":128,"frame_count":1,"size":33600,"format":"WEBP"}]}}},{"id":"61ed6c9fcc9507d24fd4dc0e","name":"guraFukkireta","flags":0,"timestamp":1704478506291,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"61ed6c9fcc9507d24fd4dc0e","name":"guraFukkireta","flags":0,"tags":["forsen","gawrgura","hololive","fukireta","dance","pls"],"lifecycle":3,"state":["LISTED","PERSONAL"],"listed":true,"animated":true,"owner":{"id":"60ae22bcaee2aa55388ed686","username":"suntgigel","display_name":"SuntGigel","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/f47315ef-8aac-46f3-9668-37f932446f65-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/61ed6c9fcc9507d24fd4dc0e","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":56,"size":33543,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":56,"size":63102,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":56,"size":94754,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":56,"size":165384,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":56,"size":173907,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":56,"size":292326,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":56,"size":284306,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":56,"size":361884,"format":"WEBP"}]}}},{"id":"6254f968b0dfc5aeb040d215","name":"eggsdd","flags":0,"timestamp":1704478538912,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"6254f968b0dfc5aeb040d215","name":"eggsdd","flags":0,"tags":["xdd","eggs"],"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"6188881df1ae15abc7ec4e08","username":"legion2k","display_name":"Legion2k","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/144fd55f-30a1-426c-8fd1-9e10e423acf6-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/6254f968b0dfc5aeb040d215","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":57,"height":32,"frame_count":1,"size":1386,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":57,"height":32,"frame_count":1,"size":1166,"format":"WEBP"},{"name":"2x.webp","static_name":"2x_static.webp","width":114,"height":64,"frame_count":1,"size":2702,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":114,"height":64,"frame_count":1,"size":2483,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":171,"height":96,"frame_count":1,"size":4324,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":171,"height":96,"frame_count":1,"size":3421,"format":"AVIF"},{"name":"4x.avif","static_name":"4x_static.avif","width":228,"height":128,"frame_count":1,"size":4567,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":228,"height":128,"frame_count":1,"size":6014,"format":"WEBP"}]}}},{"id":"60af262d57a061bbef2a54a0","name":"pajaArch","flags":0,"timestamp":1704490819528,"actor_id":"60ae3e3eb2ecb01505346ae9","data":{"id":"60af262d57a061bbef2a54a0","name":"pajaArch","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"60ae750eb351b8d1c083f5ec","username":"znixp","display_name":"zNIXp","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/8569f469-d7ee-468b-b718-e87459b2278a-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/60af262d57a061bbef2a54a0","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":913,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":746,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":1485,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":1622,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":2066,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":2680,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":2742,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":3206,"format":"WEBP"}]}}},{"id":"63d5c78c1608839c49516333","name":"eww","flags":0,"timestamp":1704530399406,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"63d5c78c1608839c49516333","name":"eww","flags":0,"tags":["eww","plink","monkeycatluna","dansgame"],"lifecycle":3,"state":["NO_PERSONAL","LISTED"],"listed":true,"animated":false,"owner":{"id":"60ff0e7a25bb6dd0b03e40f9","username":"saffybop","display_name":"saffybop","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/fd91a409-b82f-474f-a83f-45ab6e4bc3f1-profile_image-70x70.png","style":{"color":-5635841},"roles":["6076a86b09a4c63a38ebe801","62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63d5c78c1608839c49516333","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1100,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":1578,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2063,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":4390,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3113,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":7878,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4282,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":11668,"format":"WEBP"}]}}},{"id":"63537d16ea29b6bd62ac9b08","name":"Catfoolery","flags":0,"timestamp":1704550681325,"actor_id":"6118facd8efc177ec41f0718","data":{"id":"63537d16ea29b6bd62ac9b08","name":"Catfoolery","flags":0,"lifecycle":3,"state":["LISTED"],"listed":true,"animated":false,"owner":{"id":"615eadc703c9e8ba70eb6591","username":"mokshaman","display_name":"MokshaMan","avatar_url":"//static-cdn.jtvnw.net/jtv_user_pictures/fa9c373f-4818-48b1-8f9b-6b56c3e800b9-profile_image-70x70.png","style":{},"roles":["62b48deb791a15a25c2a0354"]},"host":{"url":"//cdn.7tv.app/emote/63537d16ea29b6bd62ac9b08","files":[{"name":"1x.avif","static_name":"1x_static.avif","width":32,"height":32,"frame_count":1,"size":1123,"format":"AVIF"},{"name":"1x.webp","static_name":"1x_static.webp","width":32,"height":32,"frame_count":1,"size":904,"format":"WEBP"},{"name":"2x.avif","static_name":"2x_static.avif","width":64,"height":64,"frame_count":1,"size":2060,"format":"AVIF"},{"name":"2x.webp","static_name":"2x_static.webp","width":64,"height":64,"frame_count":1,"size":2518,"format":"WEBP"},{"name":"3x.avif","static_name":"3x_static.avif","width":96,"height":96,"frame_count":1,"size":3067,"format":"AVIF"},{"name":"3x.webp","static_name":"3x_static.webp","width":96,"height":96,"frame_count":1,"size":4976,"format":"WEBP"},{"name":"4x.avif","static_name":"4x_static.avif","width":128,"height":128,"frame_count":1,"size":4112,"format":"AVIF"},{"name":"4x.webp","static_name":"4x_static.webp","width":128,"height":128,"frame_count":1,"size":7772,"format":"WEBP"}]}}}],"emote_count":837,"capacity":42069,"owner":{"id":"60ae3c29b2ecb015051f8f9a","username":"nymn","display_name":"NymN","avatar_url":"//cdn.7tv.app/pp/60ae3c29b2ecb015051f8f9a/71f269555aeb44c29100cae8aa59b56b","style":{"color":-1857617921},"roles":["6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"]}},"user":{"id":"60ae3c29b2ecb015051f8f9a","username":"nymn","display_name":"NymN","created_at":1622031401000,"avatar_url":"//cdn.7tv.app/pp/60ae3c29b2ecb015051f8f9a/71f269555aeb44c29100cae8aa59b56b","biography":"Click on the FOLLOW button maybe you get like noclip or something","style":{"color":-1857617921},"editors":[{"id":"60ae3e98b2ecb0150535c6b7","permissions":255,"visible":true,"added_at":1657657510033},{"id":"60ae8fc0ea50f43c9e3ae255","permissions":223,"visible":true,"added_at":1657657510033},{"id":"6118facd8efc177ec41f0718","permissions":17,"visible":true,"added_at":1657657510033},{"id":"60ae434b5d3fdae58382926a","permissions":223,"visible":true,"added_at":1664132908240},{"id":"60ae3e3eb2ecb01505346ae9","permissions":255,"visible":true,"added_at":1677529142453},{"id":"60ae518c0e35477634c151f1","permissions":17,"visible":true,"added_at":1684838378100},{"id":"60ae759bdf5735e04acb69d9","permissions":17,"visible":true,"added_at":1692188057245}],"roles":["6076a99409a4c63a38ebe802","62b48deb791a15a25c2a0354"],"connections":[{"id":"62300805","platform":"TWITCH","username":"nymn","display_name":"NymN","linked_at":1622031401000,"emote_capacity":42069,"emote_set_id":null,"emote_set":{"id":"63b02874ad025a672cb4969f","name":"","flags":0,"tags":[],"immutable":false,"privileged":false,"capacity":0,"owner":null}}]}} \ No newline at end of file diff --git a/benchmarks/src/Emojis.cpp b/benchmarks/src/Emojis.cpp index 7eb5106e3..aa64efc6a 100644 --- a/benchmarks/src/Emojis.cpp +++ b/benchmarks/src/Emojis.cpp @@ -55,3 +55,128 @@ static void BM_ShortcodeParsing(benchmark::State &state) } BENCHMARK(BM_ShortcodeParsing); + +static void BM_EmojiParsing(benchmark::State &state) +{ + Emojis emojis; + + emojis.load(); + + struct TestCase { + QString input; + std::vector> expectedOutput; + }; + + const auto &emojiMap = emojis.getEmojis(); + auto getEmoji = [&](auto code) { + std::shared_ptr emoji; + for (const auto &e : emojis.getEmojis()) + { + if (e->unifiedCode == code) + { + emoji = e; + break; + } + } + return emoji->emote; + }; + auto penguinEmoji = getEmoji("1F427"); + assert(penguinEmoji.get() != nullptr); + + std::vector tests{ + { + // 1 emoji + "foo 🐧 bar", + // expected output + { + "foo ", + penguinEmoji, + " bar", + }, + }, + { + // no emoji + "foo bar", + // expected output + { + "foo bar", + }, + }, + { + // many emoji + "foo 🐧 bar 🐧🐧🐧🐧🐧", + // expected output + { + "foo ", + penguinEmoji, + " bar ", + penguinEmoji, + penguinEmoji, + penguinEmoji, + penguinEmoji, + penguinEmoji, + }, + }, + }; + + for (auto _ : state) + { + for (const auto &test : tests) + { + auto output = emojis.parse(test.input); + + bool areEqual = std::equal(output.begin(), output.end(), + test.expectedOutput.begin()); + + if (!areEqual) + { + qDebug() << "BAD BENCH"; + for (const auto &v : output) + { + if (v.type() == typeid(QString)) + { + qDebug() << "output:" << boost::get(v); + } + } + } + } + } +} + +BENCHMARK(BM_EmojiParsing); + +static void BM_EmojiParsing2(benchmark::State &state, const QString &input, + int expectedNumEmojis) +{ + Emojis emojis; + + emojis.load(); + + for (auto _ : state) + { + auto output = emojis.parse(input); + int actualNumEmojis = 0; + for (const auto &part : output) + { + if (part.type() == typeid(EmotePtr)) + { + ++actualNumEmojis; + } + } + + if (actualNumEmojis != expectedNumEmojis) + { + qDebug() << "BAD BENCH, EXPECTED NUM EMOJIS IS WRONG" + << actualNumEmojis; + } + } +} + +BENCHMARK_CAPTURE(BM_EmojiParsing2, one_emoji, "foo 🐧 bar", 1); +BENCHMARK_CAPTURE(BM_EmojiParsing2, two_emoji, "foo 🐧 bar 🐧", 2); +BENCHMARK_CAPTURE( + BM_EmojiParsing2, many_emoji, + "😂 😂 😂 😂 😂 😂 😂 😂 😂 😂 😂 😂 😂 😂 😂 😂 😂 😂 😂 😂 😂 😂 😂 😂 " + "😂 😂 😂 😂 😂 😂 😂 😂 😂 😂 😂 😂 😂 😂 😂 😂 😂 😂 😂 😂 😂 😂 😂 😂 " + "😂 😂 😂 😂 😂 😂 😂 😂 😂 😂 😂 😂 😂 ", + 61); diff --git a/benchmarks/src/FormatTime.cpp b/benchmarks/src/FormatTime.cpp index cf63e4cad..d50418290 100644 --- a/benchmarks/src/FormatTime.cpp +++ b/benchmarks/src/FormatTime.cpp @@ -4,35 +4,41 @@ using namespace chatterino; -template -void BM_TimeFormatting(benchmark::State &state, Args &&...args) +void BM_TimeFormattingQString(benchmark::State &state, const QString &v) { - auto args_tuple = std::make_tuple(std::move(args)...); for (auto _ : state) { - formatTime(std::get<0>(args_tuple)); + formatTime(v); } } -BENCHMARK_CAPTURE(BM_TimeFormatting, 0, 0); -BENCHMARK_CAPTURE(BM_TimeFormatting, qs0, "0"); -BENCHMARK_CAPTURE(BM_TimeFormatting, 1337, 1337); -BENCHMARK_CAPTURE(BM_TimeFormatting, qs1337, "1337"); -BENCHMARK_CAPTURE(BM_TimeFormatting, 623452, 623452); -BENCHMARK_CAPTURE(BM_TimeFormatting, qs623452, "623452"); -BENCHMARK_CAPTURE(BM_TimeFormatting, 8345, 8345); -BENCHMARK_CAPTURE(BM_TimeFormatting, qs8345, "8345"); -BENCHMARK_CAPTURE(BM_TimeFormatting, 314034, 314034); -BENCHMARK_CAPTURE(BM_TimeFormatting, qs314034, "314034"); -BENCHMARK_CAPTURE(BM_TimeFormatting, 27, 27); -BENCHMARK_CAPTURE(BM_TimeFormatting, qs27, "27"); -BENCHMARK_CAPTURE(BM_TimeFormatting, 34589, 34589); -BENCHMARK_CAPTURE(BM_TimeFormatting, qs34589, "34589"); -BENCHMARK_CAPTURE(BM_TimeFormatting, 3659, 3659); -BENCHMARK_CAPTURE(BM_TimeFormatting, qs3659, "3659"); -BENCHMARK_CAPTURE(BM_TimeFormatting, 1045345, 1045345); -BENCHMARK_CAPTURE(BM_TimeFormatting, qs1045345, "1045345"); -BENCHMARK_CAPTURE(BM_TimeFormatting, 86432, 86432); -BENCHMARK_CAPTURE(BM_TimeFormatting, qs86432, "86432"); -BENCHMARK_CAPTURE(BM_TimeFormatting, qsempty, ""); -BENCHMARK_CAPTURE(BM_TimeFormatting, qsinvalid, "asd"); +void BM_TimeFormattingInt(benchmark::State &state, int v) +{ + for (auto _ : state) + { + formatTime(v); + } +} + +BENCHMARK_CAPTURE(BM_TimeFormattingInt, 0, 0); +BENCHMARK_CAPTURE(BM_TimeFormattingInt, 1045345, 1045345); +BENCHMARK_CAPTURE(BM_TimeFormattingInt, 1337, 1337); +BENCHMARK_CAPTURE(BM_TimeFormattingInt, 27, 27); +BENCHMARK_CAPTURE(BM_TimeFormattingInt, 314034, 314034); +BENCHMARK_CAPTURE(BM_TimeFormattingInt, 34589, 34589); +BENCHMARK_CAPTURE(BM_TimeFormattingInt, 3659, 3659); +BENCHMARK_CAPTURE(BM_TimeFormattingInt, 623452, 623452); +BENCHMARK_CAPTURE(BM_TimeFormattingInt, 8345, 8345); +BENCHMARK_CAPTURE(BM_TimeFormattingInt, 86432, 86432); +BENCHMARK_CAPTURE(BM_TimeFormattingQString, qs0, "0"); +BENCHMARK_CAPTURE(BM_TimeFormattingQString, qs1045345, "1045345"); +BENCHMARK_CAPTURE(BM_TimeFormattingQString, qs1337, "1337"); +BENCHMARK_CAPTURE(BM_TimeFormattingQString, qs27, "27"); +BENCHMARK_CAPTURE(BM_TimeFormattingQString, qs314034, "314034"); +BENCHMARK_CAPTURE(BM_TimeFormattingQString, qs34589, "34589"); +BENCHMARK_CAPTURE(BM_TimeFormattingQString, qs3659, "3659"); +BENCHMARK_CAPTURE(BM_TimeFormattingQString, qs623452, "623452"); +BENCHMARK_CAPTURE(BM_TimeFormattingQString, qs8345, "8345"); +BENCHMARK_CAPTURE(BM_TimeFormattingQString, qs86432, "86432"); +BENCHMARK_CAPTURE(BM_TimeFormattingQString, qsempty, ""); +BENCHMARK_CAPTURE(BM_TimeFormattingQString, qsinvalid, "asd"); diff --git a/benchmarks/src/Highlights.cpp b/benchmarks/src/Highlights.cpp index e87a38bac..69c69db49 100644 --- a/benchmarks/src/Highlights.cpp +++ b/benchmarks/src/Highlights.cpp @@ -1,28 +1,31 @@ #include "Application.hpp" -#include "BaseSettings.hpp" #include "common/Channel.hpp" #include "controllers/accounts/AccountController.hpp" #include "controllers/highlights/HighlightController.hpp" #include "controllers/highlights/HighlightPhrase.hpp" #include "messages/Message.hpp" -#include "messages/SharedMessageBuilder.hpp" +#include "messages/MessageBuilder.hpp" +#include "mocks/BaseApplication.hpp" +#include "mocks/UserData.hpp" #include "util/Helpers.hpp" #include #include #include +#include using namespace chatterino; -class BenchmarkMessageBuilder : public SharedMessageBuilder +class BenchmarkMessageBuilder : public MessageBuilder { public: explicit BenchmarkMessageBuilder( Channel *_channel, const Communi::IrcPrivateMessage *_ircMessage, const MessageParseArgs &_args) - : SharedMessageBuilder(_channel, _ircMessage, _args) + : MessageBuilder(_channel, _ircMessage, _args) { } + virtual MessagePtr build() { // PARSE @@ -45,71 +48,36 @@ public: } }; -class MockApplication : IApplication +class MockApplication : public mock::BaseApplication { public: - Theme *getThemes() override + MockApplication() + : highlights(this->settings, &this->accounts) { - return nullptr; - } - Fonts *getFonts() override - { - return nullptr; - } - Emotes *getEmotes() override - { - return nullptr; } + AccountController *getAccounts() override { return &this->accounts; } - HotkeyController *getHotkeys() override - { - return nullptr; - } - WindowManager *getWindows() override - { - return nullptr; - } - Toasts *getToasts() override - { - return nullptr; - } - CommandController *getCommands() override - { - return nullptr; - } - NotificationController *getNotifications() override - { - return nullptr; - } HighlightController *getHighlights() override { return &this->highlights; } - TwitchIrcServer *getTwitch() override + + IUserDataController *getUserData() override { - return nullptr; - } - ChatterinoBadges *getChatterinoBadges() override - { - return nullptr; - } - FfzBadges *getFfzBadges() override - { - return nullptr; + return &this->userData; } AccountController accounts; HighlightController highlights; - // TODO: Figure this out + mock::UserDataController userData; }; static void BM_HighlightTest(benchmark::State &state) { MockApplication mockApplication; - Settings settings("/tmp/c2-mock"); std::string message = R"(@badge-info=subscriber/34;badges=moderator/1,subscriber/24;color=#FF0000;display-name=테스트계정420;emotes=41:6-13,15-22;flags=;id=a3196c7e-be4c-4b49-9c5a-8b8302b50c2a;mod=1;room-id=11148817;subscriber=1;tmi-sent-ts=1590922213730;turbo=0;user-id=117166826;user-type=mod :testaccount_420!testaccount_420@testaccount_420.tmi.twitch.tv PRIVMSG #pajlada :-tags Kreygasm,Kreygasm (no space))"; diff --git a/benchmarks/src/LinkParser.cpp b/benchmarks/src/LinkParser.cpp new file mode 100644 index 000000000..b38e15de3 --- /dev/null +++ b/benchmarks/src/LinkParser.cpp @@ -0,0 +1,39 @@ +#include "common/LinkParser.hpp" + +#include +#include +#include +#include + +using namespace chatterino; + +const QString INPUT = QStringLiteral( + "If your Chatterino isn't loading FFZ emotes, update to the latest nightly " + "(or 2.4.2 if its out) " + "https://github.com/Chatterino/chatterino2/releases/tag/nightly-build " + "AlienPls https://www.youtube.com/watch?v=ELBBiBDcWc0 " + "127.0.3 aaaa xd 256.256.256.256 AsdQwe xd 127.0.0.1 https://. " + "*https://.be " + "https://a: http://a.b (https://a.be) ftp://xdd.com " + "this is a text lol . ://foo.com //aa.de :/foo.de xd.XDDDDDD "); + +static void BM_LinkParsing(benchmark::State &state) +{ + QStringList words = INPUT.split(' '); + + // Make sure the TLDs are loaded + { + benchmark::DoNotOptimize(linkparser::parse("xd.com")); + } + + for (auto _ : state) + { + for (const auto &word : words) + { + auto parsed = linkparser::parse(word); + benchmark::DoNotOptimize(parsed); + } + } +} + +BENCHMARK(BM_LinkParsing); diff --git a/benchmarks/src/RecentMessages.cpp b/benchmarks/src/RecentMessages.cpp new file mode 100644 index 000000000..24ebd127f --- /dev/null +++ b/benchmarks/src/RecentMessages.cpp @@ -0,0 +1,267 @@ +#include "common/Literals.hpp" +#include "controllers/accounts/AccountController.hpp" +#include "controllers/highlights/HighlightController.hpp" +#include "messages/Emote.hpp" +#include "mocks/BaseApplication.hpp" +#include "mocks/DisabledStreamerMode.hpp" +#include "mocks/LinkResolver.hpp" +#include "mocks/TwitchIrcServer.hpp" +#include "mocks/UserData.hpp" +#include "providers/bttv/BttvEmotes.hpp" +#include "providers/chatterino/ChatterinoBadges.hpp" +#include "providers/ffz/FfzBadges.hpp" +#include "providers/ffz/FfzEmotes.hpp" +#include "providers/recentmessages/Impl.hpp" +#include "providers/seventv/SeventvBadges.hpp" +#include "providers/seventv/SeventvEmotes.hpp" +#include "providers/twitch/TwitchBadges.hpp" +#include "providers/twitch/TwitchChannel.hpp" +#include "singletons/Emotes.hpp" +#include "singletons/Resources.hpp" + +#include +#include +#include +#include +#include + +#include + +using namespace chatterino; +using namespace literals; + +namespace { + +class MockApplication : public mock::BaseApplication +{ +public: + MockApplication() + : highlights(this->settings, &this->accounts) + { + } + + IEmotes *getEmotes() override + { + return &this->emotes; + } + + IUserDataController *getUserData() override + { + return &this->userData; + } + + AccountController *getAccounts() override + { + return &this->accounts; + } + + ITwitchIrcServer *getTwitch() override + { + return &this->twitch; + } + + ChatterinoBadges *getChatterinoBadges() override + { + return &this->chatterinoBadges; + } + + FfzBadges *getFfzBadges() override + { + return &this->ffzBadges; + } + + SeventvBadges *getSeventvBadges() override + { + return &this->seventvBadges; + } + + HighlightController *getHighlights() override + { + return &this->highlights; + } + + TwitchBadges *getTwitchBadges() override + { + return &this->twitchBadges; + } + + BttvEmotes *getBttvEmotes() override + { + return &this->bttvEmotes; + } + + FfzEmotes *getFfzEmotes() override + { + return &this->ffzEmotes; + } + + SeventvEmotes *getSeventvEmotes() override + { + return &this->seventvEmotes; + } + + IStreamerMode *getStreamerMode() override + { + return &this->streamerMode; + } + + ILinkResolver *getLinkResolver() override + { + return &this->linkResolver; + } + + AccountController accounts; + Emotes emotes; + mock::UserDataController userData; + mock::MockTwitchIrcServer twitch; + mock::EmptyLinkResolver linkResolver; + ChatterinoBadges chatterinoBadges; + FfzBadges ffzBadges; + SeventvBadges seventvBadges; + HighlightController highlights; + TwitchBadges twitchBadges; + BttvEmotes bttvEmotes; + FfzEmotes ffzEmotes; + SeventvEmotes seventvEmotes; + DisabledStreamerMode streamerMode; +}; + +std::optional tryReadJsonFile(const QString &path) +{ + QFile file(path); + if (!file.open(QFile::ReadOnly)) + { + return std::nullopt; + } + + QJsonParseError e; + auto doc = QJsonDocument::fromJson(file.readAll(), &e); + if (e.error != QJsonParseError::NoError) + { + return std::nullopt; + } + + return doc; +} + +QJsonDocument readJsonFile(const QString &path) +{ + auto opt = tryReadJsonFile(path); + if (!opt) + { + _exit(1); + } + return *opt; +} + +class RecentMessages +{ +public: + explicit RecentMessages(const QString &name_) + : name(name_) + , chan(this->name) + { + const auto seventvEmotes = + tryReadJsonFile(u":/bench/seventvemotes-%1.json"_s.arg(this->name)); + const auto bttvEmotes = + tryReadJsonFile(u":/bench/bttvemotes-%1.json"_s.arg(this->name)); + const auto ffzEmotes = + tryReadJsonFile(u":/bench/ffzemotes-%1.json"_s.arg(this->name)); + + if (seventvEmotes) + { + this->chan.setSeventvEmotes( + std::make_shared(seventv::detail::parseEmotes( + seventvEmotes->object()["emote_set"_L1] + .toObject()["emotes"_L1] + .toArray(), + false))); + } + + if (bttvEmotes) + { + this->chan.setBttvEmotes(std::make_shared( + bttv::detail::parseChannelEmotes(bttvEmotes->object(), + this->name))); + } + + if (ffzEmotes) + { + this->chan.setFfzEmotes(std::make_shared( + ffz::detail::parseChannelEmotes(ffzEmotes->object()))); + } + + this->messages = + readJsonFile(u":/bench/recentmessages-%1.json"_s.arg(this->name)); + } + + ~RecentMessages() + { + QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + } + + virtual void run(benchmark::State &state) = 0; + +protected: + QString name; + MockApplication app; + TwitchChannel chan; + QJsonDocument messages; +}; + +class ParseRecentMessages : public RecentMessages +{ +public: + explicit ParseRecentMessages(const QString &name_) + : RecentMessages(name_) + { + } + + void run(benchmark::State &state) + { + for (auto _ : state) + { + auto parsed = recentmessages::detail::parseRecentMessages( + this->messages.object()); + benchmark::DoNotOptimize(parsed); + } + } +}; + +class BuildRecentMessages : public RecentMessages +{ +public: + explicit BuildRecentMessages(const QString &name_) + : RecentMessages(name_) + { + } + + void run(benchmark::State &state) + { + auto parsed = recentmessages::detail::parseRecentMessages( + this->messages.object()); + for (auto _ : state) + { + auto built = recentmessages::detail::buildRecentMessages( + parsed, &this->chan); + benchmark::DoNotOptimize(built); + } + } +}; + +void BM_ParseRecentMessages(benchmark::State &state, const QString &name) +{ + ParseRecentMessages bench(name); + bench.run(state); +} + +void BM_BuildRecentMessages(benchmark::State &state, const QString &name) +{ + BuildRecentMessages bench(name); + bench.run(state); +} + +} // namespace + +BENCHMARK_CAPTURE(BM_ParseRecentMessages, nymn, u"nymn"_s); +BENCHMARK_CAPTURE(BM_BuildRecentMessages, nymn, u"nymn"_s); diff --git a/benchmarks/src/main.cpp b/benchmarks/src/main.cpp index 501b3aa51..9c7895a83 100644 --- a/benchmarks/src/main.cpp +++ b/benchmarks/src/main.cpp @@ -1,18 +1,44 @@ +#include "common/Args.hpp" +#include "singletons/Resources.hpp" +#include "singletons/Settings.hpp" + #include #include #include +#include + +using namespace chatterino; int main(int argc, char **argv) { QApplication app(argc, argv); + initResources(); + ::benchmark::Initialize(&argc, argv); - QtConcurrent::run([&app] { + Args args; + + // Ensure settings are initialized before any benchmarks are run + QTemporaryDir settingsDir; + settingsDir.setAutoRemove(false); // we'll remove it manually + chatterino::Settings settings(args, settingsDir.path()); + + QTimer::singleShot(0, [&]() { ::benchmark::RunSpecifiedBenchmarks(); - app.exit(0); + settingsDir.remove(); + + // Pick up the last events from the eventloop + // Using a loop to catch events queueing other events (e.g. deletions) + for (size_t i = 0; i < 32; i++) + { + QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents); + QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + } + + QApplication::exit(0); }); - return app.exec(); + return QApplication::exec(); } diff --git a/cmake/CodeCoverage.cmake b/cmake/CodeCoverage.cmake new file mode 100644 index 000000000..965337095 --- /dev/null +++ b/cmake/CodeCoverage.cmake @@ -0,0 +1,719 @@ +# Copyright (c) 2012 - 2017, Lars Bilke +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# CHANGES: +# +# 2012-01-31, Lars Bilke +# - Enable Code Coverage +# +# 2013-09-17, Joakim Söderberg +# - Added support for Clang. +# - Some additional usage instructions. +# +# 2016-02-03, Lars Bilke +# - Refactored functions to use named parameters +# +# 2017-06-02, Lars Bilke +# - Merged with modified version from github.com/ufz/ogs +# +# 2019-05-06, Anatolii Kurotych +# - Remove unnecessary --coverage flag +# +# 2019-12-13, FeRD (Frank Dana) +# - Deprecate COVERAGE_LCOVR_EXCLUDES and COVERAGE_GCOVR_EXCLUDES lists in favor +# of tool-agnostic COVERAGE_EXCLUDES variable, or EXCLUDE setup arguments. +# - CMake 3.4+: All excludes can be specified relative to BASE_DIRECTORY +# - All setup functions: accept BASE_DIRECTORY, EXCLUDE list +# - Set lcov basedir with -b argument +# - Add automatic --demangle-cpp in lcovr, if 'c++filt' is available (can be +# overridden with NO_DEMANGLE option in setup_target_for_coverage_lcovr().) +# - Delete output dir, .info file on 'make clean' +# - Remove Python detection, since version mismatches will break gcovr +# - Minor cleanup (lowercase function names, update examples...) +# +# 2019-12-19, FeRD (Frank Dana) +# - Rename Lcov outputs, make filtered file canonical, fix cleanup for targets +# +# 2020-01-19, Bob Apthorpe +# - Added gfortran support +# +# 2020-02-17, FeRD (Frank Dana) +# - Make all add_custom_target()s VERBATIM to auto-escape wildcard characters +# in EXCLUDEs, and remove manual escaping from gcovr targets +# +# 2021-01-19, Robin Mueller +# - Add CODE_COVERAGE_VERBOSE option which will allow to print out commands which are run +# - Added the option for users to set the GCOVR_ADDITIONAL_ARGS variable to supply additional +# flags to the gcovr command +# +# 2020-05-04, Mihchael Davis +# - Add -fprofile-abs-path to make gcno files contain absolute paths +# - Fix BASE_DIRECTORY not working when defined +# - Change BYPRODUCT from folder to index.html to stop ninja from complaining about double defines +# +# 2021-05-10, Martin Stump +# - Check if the generator is multi-config before warning about non-Debug builds +# +# 2022-02-22, Marko Wehle +# - Change gcovr output from -o for --xml and --html output respectively. +# This will allow for Multiple Output Formats at the same time by making use of GCOVR_ADDITIONAL_ARGS, e.g. GCOVR_ADDITIONAL_ARGS "--txt". +# +# USAGE: +# +# 1. Copy this file into your cmake modules path. +# +# 2. Add the following line to your CMakeLists.txt (best inside an if-condition +# using a CMake option() to enable it just optionally): +# include(CodeCoverage) +# +# 3. Append necessary compiler flags for all supported source files: +# append_coverage_compiler_flags() +# Or for specific target: +# append_coverage_compiler_flags_to_target(YOUR_TARGET_NAME) +# +# 3.a (OPTIONAL) Set appropriate optimization flags, e.g. -O0, -O1 or -Og +# +# 4. If you need to exclude additional directories from the report, specify them +# using full paths in the COVERAGE_EXCLUDES variable before calling +# setup_target_for_coverage_*(). +# Example: +# set(COVERAGE_EXCLUDES +# '${PROJECT_SOURCE_DIR}/src/dir1/*' +# '/path/to/my/src/dir2/*') +# Or, use the EXCLUDE argument to setup_target_for_coverage_*(). +# Example: +# setup_target_for_coverage_lcov( +# NAME coverage +# EXECUTABLE testrunner +# EXCLUDE "${PROJECT_SOURCE_DIR}/src/dir1/*" "/path/to/my/src/dir2/*") +# +# 4.a NOTE: With CMake 3.4+, COVERAGE_EXCLUDES or EXCLUDE can also be set +# relative to the BASE_DIRECTORY (default: PROJECT_SOURCE_DIR) +# Example: +# set(COVERAGE_EXCLUDES "dir1/*") +# setup_target_for_coverage_gcovr_html( +# NAME coverage +# EXECUTABLE testrunner +# BASE_DIRECTORY "${PROJECT_SOURCE_DIR}/src" +# EXCLUDE "dir2/*") +# +# 5. Use the functions described below to create a custom make target which +# runs your test executable and produces a code coverage report. +# +# 6. Build a Debug build: +# cmake -DCMAKE_BUILD_TYPE=Debug .. +# make +# make my_coverage_target +# + +include(CMakeParseArguments) + +option(CODE_COVERAGE_VERBOSE "Verbose information" FALSE) + +# Check prereqs +find_program( GCOV_PATH gcov ) +find_program( LCOV_PATH NAMES lcov lcov.bat lcov.exe lcov.perl) +find_program( FASTCOV_PATH NAMES fastcov fastcov.py ) +find_program( GENHTML_PATH NAMES genhtml genhtml.perl genhtml.bat ) +find_program( GCOVR_PATH gcovr PATHS ${CMAKE_SOURCE_DIR}/scripts/test) +find_program( CPPFILT_PATH NAMES c++filt ) + +if(NOT GCOV_PATH) + message(FATAL_ERROR "gcov not found! Aborting...") +endif() # NOT GCOV_PATH + +get_property(LANGUAGES GLOBAL PROPERTY ENABLED_LANGUAGES) +list(GET LANGUAGES 0 LANG) + +if("${CMAKE_${LANG}_COMPILER_ID}" MATCHES "(Apple)?[Cc]lang") + if("${CMAKE_${LANG}_COMPILER_VERSION}" VERSION_LESS 3) + message(FATAL_ERROR "Clang version must be 3.0.0 or greater! Aborting...") + endif() +elseif(NOT CMAKE_COMPILER_IS_GNUCXX) + if("${CMAKE_Fortran_COMPILER_ID}" MATCHES "[Ff]lang") + # Do nothing; exit conditional without error if true + elseif("${CMAKE_Fortran_COMPILER_ID}" MATCHES "GNU") + # Do nothing; exit conditional without error if true + else() + message(FATAL_ERROR "Compiler is not GNU gcc! Aborting...") + endif() +endif() + +set(COVERAGE_COMPILER_FLAGS "-g -fprofile-arcs -ftest-coverage" + CACHE INTERNAL "") +if(CMAKE_CXX_COMPILER_ID MATCHES "(GNU|Clang)") + include(CheckCXXCompilerFlag) + check_cxx_compiler_flag(-fprofile-abs-path HAVE_fprofile_abs_path) + if(HAVE_fprofile_abs_path) + set(COVERAGE_COMPILER_FLAGS "${COVERAGE_COMPILER_FLAGS} -fprofile-abs-path") + endif() +endif() + +set(CMAKE_Fortran_FLAGS_COVERAGE + ${COVERAGE_COMPILER_FLAGS} + CACHE STRING "Flags used by the Fortran compiler during coverage builds." + FORCE ) +set(CMAKE_CXX_FLAGS_COVERAGE + ${COVERAGE_COMPILER_FLAGS} + CACHE STRING "Flags used by the C++ compiler during coverage builds." + FORCE ) +set(CMAKE_C_FLAGS_COVERAGE + ${COVERAGE_COMPILER_FLAGS} + CACHE STRING "Flags used by the C compiler during coverage builds." + FORCE ) +set(CMAKE_EXE_LINKER_FLAGS_COVERAGE + "" + CACHE STRING "Flags used for linking binaries during coverage builds." + FORCE ) +set(CMAKE_SHARED_LINKER_FLAGS_COVERAGE + "" + CACHE STRING "Flags used by the shared libraries linker during coverage builds." + FORCE ) +mark_as_advanced( + CMAKE_Fortran_FLAGS_COVERAGE + CMAKE_CXX_FLAGS_COVERAGE + CMAKE_C_FLAGS_COVERAGE + CMAKE_EXE_LINKER_FLAGS_COVERAGE + CMAKE_SHARED_LINKER_FLAGS_COVERAGE ) + +get_property(GENERATOR_IS_MULTI_CONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(NOT (CMAKE_BUILD_TYPE STREQUAL "Debug" OR GENERATOR_IS_MULTI_CONFIG)) + message(WARNING "Code coverage results with an optimised (non-Debug) build may be misleading") +endif() # NOT (CMAKE_BUILD_TYPE STREQUAL "Debug" OR GENERATOR_IS_MULTI_CONFIG) + +if(CMAKE_C_COMPILER_ID STREQUAL "GNU" OR CMAKE_Fortran_COMPILER_ID STREQUAL "GNU") + link_libraries(gcov) +endif() + +# Defines a target for running and collection code coverage information +# Builds dependencies, runs the given executable and outputs reports. +# NOTE! The executable should always have a ZERO as exit code otherwise +# the coverage generation will not complete. +# +# setup_target_for_coverage_lcov( +# NAME testrunner_coverage # New target name +# EXECUTABLE testrunner -j ${PROCESSOR_COUNT} # Executable in PROJECT_BINARY_DIR +# DEPENDENCIES testrunner # Dependencies to build first +# BASE_DIRECTORY "../" # Base directory for report +# # (defaults to PROJECT_SOURCE_DIR) +# EXCLUDE "src/dir1/*" "src/dir2/*" # Patterns to exclude (can be relative +# # to BASE_DIRECTORY, with CMake 3.4+) +# NO_DEMANGLE # Don't demangle C++ symbols +# # even if c++filt is found +# ) +function(setup_target_for_coverage_lcov) + + set(options NO_DEMANGLE) + set(oneValueArgs BASE_DIRECTORY NAME) + set(multiValueArgs EXCLUDE EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES LCOV_ARGS GENHTML_ARGS) + cmake_parse_arguments(Coverage "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + if(NOT LCOV_PATH) + message(FATAL_ERROR "lcov not found! Aborting...") + endif() # NOT LCOV_PATH + + if(NOT GENHTML_PATH) + message(FATAL_ERROR "genhtml not found! Aborting...") + endif() # NOT GENHTML_PATH + + # Set base directory (as absolute path), or default to PROJECT_SOURCE_DIR + if(DEFINED Coverage_BASE_DIRECTORY) + get_filename_component(BASEDIR ${Coverage_BASE_DIRECTORY} ABSOLUTE) + else() + set(BASEDIR ${PROJECT_SOURCE_DIR}) + endif() + + # Collect excludes (CMake 3.4+: Also compute absolute paths) + set(LCOV_EXCLUDES "") + foreach(EXCLUDE ${Coverage_EXCLUDE} ${COVERAGE_EXCLUDES} ${COVERAGE_LCOV_EXCLUDES}) + if(CMAKE_VERSION VERSION_GREATER 3.4) + get_filename_component(EXCLUDE ${EXCLUDE} ABSOLUTE BASE_DIR ${BASEDIR}) + endif() + list(APPEND LCOV_EXCLUDES "${EXCLUDE}") + endforeach() + list(REMOVE_DUPLICATES LCOV_EXCLUDES) + + # Conditional arguments + if(CPPFILT_PATH AND NOT ${Coverage_NO_DEMANGLE}) + set(GENHTML_EXTRA_ARGS "--demangle-cpp") + endif() + + # Setting up commands which will be run to generate coverage data. + # Cleanup lcov + set(LCOV_CLEAN_CMD + ${LCOV_PATH} ${Coverage_LCOV_ARGS} --gcov-tool ${GCOV_PATH} -directory . + -b ${BASEDIR} --zerocounters + ) + # Create baseline to make sure untouched files show up in the report + set(LCOV_BASELINE_CMD + ${LCOV_PATH} ${Coverage_LCOV_ARGS} --gcov-tool ${GCOV_PATH} -c -i -d . -b + ${BASEDIR} -o ${Coverage_NAME}.base + ) + # Run tests + set(LCOV_EXEC_TESTS_CMD + ${Coverage_EXECUTABLE} ${Coverage_EXECUTABLE_ARGS} + ) + # Capturing lcov counters and generating report + set(LCOV_CAPTURE_CMD + ${LCOV_PATH} ${Coverage_LCOV_ARGS} --gcov-tool ${GCOV_PATH} --directory . -b + ${BASEDIR} --capture --output-file ${Coverage_NAME}.capture + ) + # add baseline counters + set(LCOV_BASELINE_COUNT_CMD + ${LCOV_PATH} ${Coverage_LCOV_ARGS} --gcov-tool ${GCOV_PATH} -a ${Coverage_NAME}.base + -a ${Coverage_NAME}.capture --output-file ${Coverage_NAME}.total + ) + # filter collected data to final coverage report + set(LCOV_FILTER_CMD + ${LCOV_PATH} ${Coverage_LCOV_ARGS} --gcov-tool ${GCOV_PATH} --remove + ${Coverage_NAME}.total ${LCOV_EXCLUDES} --output-file ${Coverage_NAME}.info + ) + # Generate HTML output + set(LCOV_GEN_HTML_CMD + ${GENHTML_PATH} ${GENHTML_EXTRA_ARGS} ${Coverage_GENHTML_ARGS} -o + ${Coverage_NAME} ${Coverage_NAME}.info + ) + + + if(CODE_COVERAGE_VERBOSE) + message(STATUS "Executed command report") + message(STATUS "Command to clean up lcov: ") + string(REPLACE ";" " " LCOV_CLEAN_CMD_SPACED "${LCOV_CLEAN_CMD}") + message(STATUS "${LCOV_CLEAN_CMD_SPACED}") + + message(STATUS "Command to create baseline: ") + string(REPLACE ";" " " LCOV_BASELINE_CMD_SPACED "${LCOV_BASELINE_CMD}") + message(STATUS "${LCOV_BASELINE_CMD_SPACED}") + + message(STATUS "Command to run the tests: ") + string(REPLACE ";" " " LCOV_EXEC_TESTS_CMD_SPACED "${LCOV_EXEC_TESTS_CMD}") + message(STATUS "${LCOV_EXEC_TESTS_CMD_SPACED}") + + message(STATUS "Command to capture counters and generate report: ") + string(REPLACE ";" " " LCOV_CAPTURE_CMD_SPACED "${LCOV_CAPTURE_CMD}") + message(STATUS "${LCOV_CAPTURE_CMD_SPACED}") + + message(STATUS "Command to add baseline counters: ") + string(REPLACE ";" " " LCOV_BASELINE_COUNT_CMD_SPACED "${LCOV_BASELINE_COUNT_CMD}") + message(STATUS "${LCOV_BASELINE_COUNT_CMD_SPACED}") + + message(STATUS "Command to filter collected data: ") + string(REPLACE ";" " " LCOV_FILTER_CMD_SPACED "${LCOV_FILTER_CMD}") + message(STATUS "${LCOV_FILTER_CMD_SPACED}") + + message(STATUS "Command to generate lcov HTML output: ") + string(REPLACE ";" " " LCOV_GEN_HTML_CMD_SPACED "${LCOV_GEN_HTML_CMD}") + message(STATUS "${LCOV_GEN_HTML_CMD_SPACED}") + endif() + + # Setup target + add_custom_target(${Coverage_NAME} + COMMAND ${LCOV_CLEAN_CMD} + COMMAND ${LCOV_BASELINE_CMD} + COMMAND ${LCOV_EXEC_TESTS_CMD} + COMMAND ${LCOV_CAPTURE_CMD} + COMMAND ${LCOV_BASELINE_COUNT_CMD} + COMMAND ${LCOV_FILTER_CMD} + COMMAND ${LCOV_GEN_HTML_CMD} + + # Set output files as GENERATED (will be removed on 'make clean') + BYPRODUCTS + ${Coverage_NAME}.base + ${Coverage_NAME}.capture + ${Coverage_NAME}.total + ${Coverage_NAME}.info + ${Coverage_NAME}/index.html + WORKING_DIRECTORY ${PROJECT_BINARY_DIR} + DEPENDS ${Coverage_DEPENDENCIES} + VERBATIM # Protect arguments to commands + COMMENT "Resetting code coverage counters to zero.\nProcessing code coverage counters and generating report." + ) + + # Show where to find the lcov info report + add_custom_command(TARGET ${Coverage_NAME} POST_BUILD + COMMAND ; + COMMENT "Lcov code coverage info report saved in ${Coverage_NAME}.info." + ) + + # Show info where to find the report + add_custom_command(TARGET ${Coverage_NAME} POST_BUILD + COMMAND ; + COMMENT "Open ./${Coverage_NAME}/index.html in your browser to view the coverage report." + ) + +endfunction() # setup_target_for_coverage_lcov + +# Defines a target for running and collection code coverage information +# Builds dependencies, runs the given executable and outputs reports. +# NOTE! The executable should always have a ZERO as exit code otherwise +# the coverage generation will not complete. +# +# setup_target_for_coverage_gcovr_xml( +# NAME ctest_coverage # New target name +# EXECUTABLE ctest -j ${PROCESSOR_COUNT} # Executable in PROJECT_BINARY_DIR +# DEPENDENCIES executable_target # Dependencies to build first +# BASE_DIRECTORY "../" # Base directory for report +# # (defaults to PROJECT_SOURCE_DIR) +# EXCLUDE "src/dir1/*" "src/dir2/*" # Patterns to exclude (can be relative +# # to BASE_DIRECTORY, with CMake 3.4+) +# ) +# The user can set the variable GCOVR_ADDITIONAL_ARGS to supply additional flags to the +# GCVOR command. +function(setup_target_for_coverage_gcovr_xml) + + set(options NONE) + set(oneValueArgs BASE_DIRECTORY NAME) + set(multiValueArgs EXCLUDE EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES) + cmake_parse_arguments(Coverage "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + if(NOT GCOVR_PATH) + message(FATAL_ERROR "gcovr not found! Aborting...") + endif() # NOT GCOVR_PATH + + # Set base directory (as absolute path), or default to PROJECT_SOURCE_DIR + if(DEFINED Coverage_BASE_DIRECTORY) + get_filename_component(BASEDIR ${Coverage_BASE_DIRECTORY} ABSOLUTE) + else() + set(BASEDIR ${PROJECT_SOURCE_DIR}) + endif() + + # Collect excludes (CMake 3.4+: Also compute absolute paths) + set(GCOVR_EXCLUDES "") + foreach(EXCLUDE ${Coverage_EXCLUDE} ${COVERAGE_EXCLUDES} ${COVERAGE_GCOVR_EXCLUDES}) + if(CMAKE_VERSION VERSION_GREATER 3.4) + get_filename_component(EXCLUDE ${EXCLUDE} ABSOLUTE BASE_DIR ${BASEDIR}) + endif() + list(APPEND GCOVR_EXCLUDES "${EXCLUDE}") + endforeach() + list(REMOVE_DUPLICATES GCOVR_EXCLUDES) + + # Combine excludes to several -e arguments + set(GCOVR_EXCLUDE_ARGS "") + foreach(EXCLUDE ${GCOVR_EXCLUDES}) + list(APPEND GCOVR_EXCLUDE_ARGS "-e") + list(APPEND GCOVR_EXCLUDE_ARGS "${EXCLUDE}") + endforeach() + + # Set up commands which will be run to generate coverage data + # Run tests + set(GCOVR_XML_EXEC_TESTS_CMD + ${Coverage_EXECUTABLE} ${Coverage_EXECUTABLE_ARGS} + ) + # Running gcovr + set(GCOVR_XML_CMD + ${GCOVR_PATH} --xml ${Coverage_NAME}.xml -r ${BASEDIR} ${GCOVR_ADDITIONAL_ARGS} + ${GCOVR_EXCLUDE_ARGS} --object-directory=${PROJECT_BINARY_DIR} + ) + + if(CODE_COVERAGE_VERBOSE) + message(STATUS "Executed command report") + + message(STATUS "Command to run tests: ") + string(REPLACE ";" " " GCOVR_XML_EXEC_TESTS_CMD_SPACED "${GCOVR_XML_EXEC_TESTS_CMD}") + message(STATUS "${GCOVR_XML_EXEC_TESTS_CMD_SPACED}") + + message(STATUS "Command to generate gcovr XML coverage data: ") + string(REPLACE ";" " " GCOVR_XML_CMD_SPACED "${GCOVR_XML_CMD}") + message(STATUS "${GCOVR_XML_CMD_SPACED}") + endif() + + add_custom_target(${Coverage_NAME} + COMMAND ${GCOVR_XML_EXEC_TESTS_CMD} + COMMAND ${GCOVR_XML_CMD} + + BYPRODUCTS ${Coverage_NAME}.xml + WORKING_DIRECTORY ${PROJECT_BINARY_DIR} + DEPENDS ${Coverage_DEPENDENCIES} + VERBATIM # Protect arguments to commands + COMMENT "Running gcovr to produce Cobertura code coverage report." + ) + + # Show info where to find the report + add_custom_command(TARGET ${Coverage_NAME} POST_BUILD + COMMAND ; + COMMENT "Cobertura code coverage report saved in ${Coverage_NAME}.xml." + ) +endfunction() # setup_target_for_coverage_gcovr_xml + +# Defines a target for running and collection code coverage information +# Builds dependencies, runs the given executable and outputs reports. +# NOTE! The executable should always have a ZERO as exit code otherwise +# the coverage generation will not complete. +# +# setup_target_for_coverage_gcovr_html( +# NAME ctest_coverage # New target name +# EXECUTABLE ctest -j ${PROCESSOR_COUNT} # Executable in PROJECT_BINARY_DIR +# DEPENDENCIES executable_target # Dependencies to build first +# BASE_DIRECTORY "../" # Base directory for report +# # (defaults to PROJECT_SOURCE_DIR) +# EXCLUDE "src/dir1/*" "src/dir2/*" # Patterns to exclude (can be relative +# # to BASE_DIRECTORY, with CMake 3.4+) +# ) +# The user can set the variable GCOVR_ADDITIONAL_ARGS to supply additional flags to the +# GCVOR command. +function(setup_target_for_coverage_gcovr_html) + + set(options NONE) + set(oneValueArgs BASE_DIRECTORY NAME) + set(multiValueArgs EXCLUDE EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES) + cmake_parse_arguments(Coverage "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + if(NOT GCOVR_PATH) + message(FATAL_ERROR "gcovr not found! Aborting...") + endif() # NOT GCOVR_PATH + + # Set base directory (as absolute path), or default to PROJECT_SOURCE_DIR + if(DEFINED Coverage_BASE_DIRECTORY) + get_filename_component(BASEDIR ${Coverage_BASE_DIRECTORY} ABSOLUTE) + else() + set(BASEDIR ${PROJECT_SOURCE_DIR}) + endif() + + # Collect excludes (CMake 3.4+: Also compute absolute paths) + set(GCOVR_EXCLUDES "") + foreach(EXCLUDE ${Coverage_EXCLUDE} ${COVERAGE_EXCLUDES} ${COVERAGE_GCOVR_EXCLUDES}) + if(CMAKE_VERSION VERSION_GREATER 3.4) + get_filename_component(EXCLUDE ${EXCLUDE} ABSOLUTE BASE_DIR ${BASEDIR}) + endif() + list(APPEND GCOVR_EXCLUDES "${EXCLUDE}") + endforeach() + list(REMOVE_DUPLICATES GCOVR_EXCLUDES) + + # Combine excludes to several -e arguments + set(GCOVR_EXCLUDE_ARGS "") + foreach(EXCLUDE ${GCOVR_EXCLUDES}) + list(APPEND GCOVR_EXCLUDE_ARGS "-e") + list(APPEND GCOVR_EXCLUDE_ARGS "${EXCLUDE}") + endforeach() + + # Set up commands which will be run to generate coverage data + # Run tests + set(GCOVR_HTML_EXEC_TESTS_CMD + ${Coverage_EXECUTABLE} ${Coverage_EXECUTABLE_ARGS} + ) + # Create folder + set(GCOVR_HTML_FOLDER_CMD + ${CMAKE_COMMAND} -E make_directory ${PROJECT_BINARY_DIR}/${Coverage_NAME} + ) + # Running gcovr + set(GCOVR_HTML_CMD + ${GCOVR_PATH} --html ${Coverage_NAME}/index.html --html-details -r ${BASEDIR} ${GCOVR_ADDITIONAL_ARGS} + ${GCOVR_EXCLUDE_ARGS} --object-directory=${PROJECT_BINARY_DIR} + ) + + if(CODE_COVERAGE_VERBOSE) + message(STATUS "Executed command report") + + message(STATUS "Command to run tests: ") + string(REPLACE ";" " " GCOVR_HTML_EXEC_TESTS_CMD_SPACED "${GCOVR_HTML_EXEC_TESTS_CMD}") + message(STATUS "${GCOVR_HTML_EXEC_TESTS_CMD_SPACED}") + + message(STATUS "Command to create a folder: ") + string(REPLACE ";" " " GCOVR_HTML_FOLDER_CMD_SPACED "${GCOVR_HTML_FOLDER_CMD}") + message(STATUS "${GCOVR_HTML_FOLDER_CMD_SPACED}") + + message(STATUS "Command to generate gcovr HTML coverage data: ") + string(REPLACE ";" " " GCOVR_HTML_CMD_SPACED "${GCOVR_HTML_CMD}") + message(STATUS "${GCOVR_HTML_CMD_SPACED}") + endif() + + add_custom_target(${Coverage_NAME} + COMMAND ${GCOVR_HTML_EXEC_TESTS_CMD} + COMMAND ${GCOVR_HTML_FOLDER_CMD} + COMMAND ${GCOVR_HTML_CMD} + + BYPRODUCTS ${PROJECT_BINARY_DIR}/${Coverage_NAME}/index.html # report directory + WORKING_DIRECTORY ${PROJECT_BINARY_DIR} + DEPENDS ${Coverage_DEPENDENCIES} + VERBATIM # Protect arguments to commands + COMMENT "Running gcovr to produce HTML code coverage report." + ) + + # Show info where to find the report + add_custom_command(TARGET ${Coverage_NAME} POST_BUILD + COMMAND ; + COMMENT "Open ./${Coverage_NAME}/index.html in your browser to view the coverage report." + ) + +endfunction() # setup_target_for_coverage_gcovr_html + +# Defines a target for running and collection code coverage information +# Builds dependencies, runs the given executable and outputs reports. +# NOTE! The executable should always have a ZERO as exit code otherwise +# the coverage generation will not complete. +# +# setup_target_for_coverage_fastcov( +# NAME testrunner_coverage # New target name +# EXECUTABLE testrunner -j ${PROCESSOR_COUNT} # Executable in PROJECT_BINARY_DIR +# DEPENDENCIES testrunner # Dependencies to build first +# BASE_DIRECTORY "../" # Base directory for report +# # (defaults to PROJECT_SOURCE_DIR) +# EXCLUDE "src/dir1/" "src/dir2/" # Patterns to exclude. +# NO_DEMANGLE # Don't demangle C++ symbols +# # even if c++filt is found +# SKIP_HTML # Don't create html report +# POST_CMD perl -i -pe s!${PROJECT_SOURCE_DIR}/!!g ctest_coverage.json # E.g. for stripping source dir from file paths +# ) +function(setup_target_for_coverage_fastcov) + + set(options NO_DEMANGLE SKIP_HTML) + set(oneValueArgs BASE_DIRECTORY NAME) + set(multiValueArgs EXCLUDE EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES FASTCOV_ARGS GENHTML_ARGS POST_CMD) + cmake_parse_arguments(Coverage "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + if(NOT FASTCOV_PATH) + message(FATAL_ERROR "fastcov not found! Aborting...") + endif() + + if(NOT Coverage_SKIP_HTML AND NOT GENHTML_PATH) + message(FATAL_ERROR "genhtml not found! Aborting...") + endif() + + # Set base directory (as absolute path), or default to PROJECT_SOURCE_DIR + if(Coverage_BASE_DIRECTORY) + get_filename_component(BASEDIR ${Coverage_BASE_DIRECTORY} ABSOLUTE) + else() + set(BASEDIR ${PROJECT_SOURCE_DIR}) + endif() + + # Collect excludes (Patterns, not paths, for fastcov) + set(FASTCOV_EXCLUDES "") + foreach(EXCLUDE ${Coverage_EXCLUDE} ${COVERAGE_EXCLUDES} ${COVERAGE_FASTCOV_EXCLUDES}) + list(APPEND FASTCOV_EXCLUDES "${EXCLUDE}") + endforeach() + list(REMOVE_DUPLICATES FASTCOV_EXCLUDES) + + # Conditional arguments + if(CPPFILT_PATH AND NOT ${Coverage_NO_DEMANGLE}) + set(GENHTML_EXTRA_ARGS "--demangle-cpp") + endif() + + # Set up commands which will be run to generate coverage data + set(FASTCOV_EXEC_TESTS_CMD ${Coverage_EXECUTABLE} ${Coverage_EXECUTABLE_ARGS}) + + set(FASTCOV_CAPTURE_CMD ${FASTCOV_PATH} ${Coverage_FASTCOV_ARGS} --gcov ${GCOV_PATH} + --search-directory ${BASEDIR} + --process-gcno + --output ${Coverage_NAME}.json + --exclude ${FASTCOV_EXCLUDES} + --exclude ${FASTCOV_EXCLUDES} + ) + + set(FASTCOV_CONVERT_CMD ${FASTCOV_PATH} + -C ${Coverage_NAME}.json --lcov --output ${Coverage_NAME}.info + ) + + if(Coverage_SKIP_HTML) + set(FASTCOV_HTML_CMD ";") + else() + set(FASTCOV_HTML_CMD ${GENHTML_PATH} ${GENHTML_EXTRA_ARGS} ${Coverage_GENHTML_ARGS} + -o ${Coverage_NAME} ${Coverage_NAME}.info + ) + endif() + + set(FASTCOV_POST_CMD ";") + if(Coverage_POST_CMD) + set(FASTCOV_POST_CMD ${Coverage_POST_CMD}) + endif() + + if(CODE_COVERAGE_VERBOSE) + message(STATUS "Code coverage commands for target ${Coverage_NAME} (fastcov):") + + message(" Running tests:") + string(REPLACE ";" " " FASTCOV_EXEC_TESTS_CMD_SPACED "${FASTCOV_EXEC_TESTS_CMD}") + message(" ${FASTCOV_EXEC_TESTS_CMD_SPACED}") + + message(" Capturing fastcov counters and generating report:") + string(REPLACE ";" " " FASTCOV_CAPTURE_CMD_SPACED "${FASTCOV_CAPTURE_CMD}") + message(" ${FASTCOV_CAPTURE_CMD_SPACED}") + + message(" Converting fastcov .json to lcov .info:") + string(REPLACE ";" " " FASTCOV_CONVERT_CMD_SPACED "${FASTCOV_CONVERT_CMD}") + message(" ${FASTCOV_CONVERT_CMD_SPACED}") + + if(NOT Coverage_SKIP_HTML) + message(" Generating HTML report: ") + string(REPLACE ";" " " FASTCOV_HTML_CMD_SPACED "${FASTCOV_HTML_CMD}") + message(" ${FASTCOV_HTML_CMD_SPACED}") + endif() + if(Coverage_POST_CMD) + message(" Running post command: ") + string(REPLACE ";" " " FASTCOV_POST_CMD_SPACED "${FASTCOV_POST_CMD}") + message(" ${FASTCOV_POST_CMD_SPACED}") + endif() + endif() + + # Setup target + add_custom_target(${Coverage_NAME} + + # Cleanup fastcov + COMMAND ${FASTCOV_PATH} ${Coverage_FASTCOV_ARGS} --gcov ${GCOV_PATH} + --search-directory ${BASEDIR} + --zerocounters + + COMMAND ${FASTCOV_EXEC_TESTS_CMD} + COMMAND ${FASTCOV_CAPTURE_CMD} + COMMAND ${FASTCOV_CONVERT_CMD} + COMMAND ${FASTCOV_HTML_CMD} + COMMAND ${FASTCOV_POST_CMD} + + # Set output files as GENERATED (will be removed on 'make clean') + BYPRODUCTS + ${Coverage_NAME}.info + ${Coverage_NAME}.json + ${Coverage_NAME}/index.html # report directory + + WORKING_DIRECTORY ${PROJECT_BINARY_DIR} + DEPENDS ${Coverage_DEPENDENCIES} + VERBATIM # Protect arguments to commands + COMMENT "Resetting code coverage counters to zero. Processing code coverage counters and generating report." + ) + + set(INFO_MSG "fastcov code coverage info report saved in ${Coverage_NAME}.info and ${Coverage_NAME}.json.") + if(NOT Coverage_SKIP_HTML) + string(APPEND INFO_MSG " Open ${PROJECT_BINARY_DIR}/${Coverage_NAME}/index.html in your browser to view the coverage report.") + endif() + # Show where to find the fastcov info report + add_custom_command(TARGET ${Coverage_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E echo ${INFO_MSG} + ) + +endfunction() # setup_target_for_coverage_fastcov + +function(append_coverage_compiler_flags) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${COVERAGE_COMPILER_FLAGS}" PARENT_SCOPE) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${COVERAGE_COMPILER_FLAGS}" PARENT_SCOPE) + set(CMAKE_Fortran_FLAGS "${CMAKE_Fortran_FLAGS} ${COVERAGE_COMPILER_FLAGS}" PARENT_SCOPE) + message(STATUS "Appending code coverage compiler flags: ${COVERAGE_COMPILER_FLAGS}") +endfunction() # append_coverage_compiler_flags + +# Setup coverage for specific library +function(append_coverage_compiler_flags_to_target name) + separate_arguments(_flag_list NATIVE_COMMAND "${COVERAGE_COMPILER_FLAGS}") + target_compile_options(${name} PRIVATE ${_flag_list}) +endfunction() diff --git a/cmake/FindMagicEnum.cmake b/cmake/FindMagicEnum.cmake index 0a77bd279..b595075ca 100644 --- a/cmake/FindMagicEnum.cmake +++ b/cmake/FindMagicEnum.cmake @@ -1,6 +1,6 @@ include(FindPackageHandleStandardArgs) -find_path(MagicEnum_INCLUDE_DIR magic_enum.hpp HINTS ${CMAKE_SOURCE_DIR}/lib/magic_enum/include) +find_path(MagicEnum_INCLUDE_DIR magic_enum/magic_enum.hpp HINTS ${CMAKE_SOURCE_DIR}/lib/magic_enum/include) find_package_handle_standard_args(MagicEnum DEFAULT_MSG MagicEnum_INCLUDE_DIR) diff --git a/cmake/FindWinToast.cmake b/cmake/FindWinToast.cmake deleted file mode 100644 index 3b767fb19..000000000 --- a/cmake/FindWinToast.cmake +++ /dev/null @@ -1,12 +0,0 @@ -if (EXISTS ${CMAKE_SOURCE_DIR}/lib/WinToast/src/wintoastlib.cpp) - set(WinToast_FOUND TRUE) - add_library(WinToast ${CMAKE_SOURCE_DIR}/lib/WinToast/src/wintoastlib.cpp) - target_include_directories(WinToast PUBLIC "${CMAKE_SOURCE_DIR}/lib/WinToast/src/") -else () - set(WinToast_FOUND FALSE) - message("WinToast submodule not found!") -endif () - - - - diff --git a/cmake/GIT.cmake b/cmake/GIT.cmake index 53cd14534..9469d9420 100644 --- a/cmake/GIT.cmake +++ b/cmake/GIT.cmake @@ -9,7 +9,7 @@ # If the git binary is found and the git work tree is intact, GIT_RELEASE is worked out using the `git describe` command # The value of GIT_RELEASE can be overriden by defining the GIT_RELEASE environment variable # GIT_MODIFIED -# If the git binary is found and the git work tree is intact, GIT_MODIFIED is worked out by checking if output of `git status --porcelain -z` command is empty +# If the git binary is found and the git work tree is intact, GIT_MODIFIED is worked out by checking if output of `git status --porcelain -z` command is empty # The value of GIT_MODIFIED cannot be overriden find_package(Git) @@ -19,66 +19,75 @@ set(GIT_COMMIT "GIT-REPOSITORY-NOT-FOUND") set(GIT_RELEASE "${PROJECT_VERSION}") set(GIT_MODIFIED 0) -if (DEFINED ENV{CHATTERINO_SKIP_GIT_GEN}) +if(DEFINED ENV{CHATTERINO_SKIP_GIT_GEN}) return() -endif () +endif() -if (GIT_EXECUTABLE) +if(GIT_EXECUTABLE) execute_process( - COMMAND ${GIT_EXECUTABLE} rev-parse --is-inside-work-tree - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} - RESULT_VARIABLE GIT_REPOSITORY_NOT_FOUND - ERROR_QUIET + COMMAND ${GIT_EXECUTABLE} rev-parse --is-inside-work-tree + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + RESULT_VARIABLE GIT_REPOSITORY_NOT_FOUND + OUTPUT_QUIET + ERROR_QUIET ) - if (GIT_REPOSITORY_NOT_FOUND) + + if(GIT_REPOSITORY_NOT_FOUND) set(GIT_REPOSITORY_FOUND 0) - else () + else() set(GIT_REPOSITORY_FOUND 1) endif() - if (GIT_REPOSITORY_FOUND) + if(GIT_REPOSITORY_FOUND) execute_process( - COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} - OUTPUT_VARIABLE GIT_HASH - OUTPUT_STRIP_TRAILING_WHITESPACE + COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE GIT_HASH + OUTPUT_STRIP_TRAILING_WHITESPACE ) execute_process( - COMMAND ${GIT_EXECUTABLE} rev-parse HEAD - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} - OUTPUT_VARIABLE GIT_COMMIT - OUTPUT_STRIP_TRAILING_WHITESPACE + COMMAND ${GIT_EXECUTABLE} rev-parse HEAD + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE GIT_COMMIT + OUTPUT_STRIP_TRAILING_WHITESPACE ) execute_process( - COMMAND ${GIT_EXECUTABLE} describe - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} - OUTPUT_VARIABLE GIT_RELEASE - OUTPUT_STRIP_TRAILING_WHITESPACE + COMMAND ${GIT_EXECUTABLE} describe + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE GIT_RELEASE + OUTPUT_STRIP_TRAILING_WHITESPACE ) execute_process( - COMMAND ${GIT_EXECUTABLE} status --porcelain -z - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} - OUTPUT_VARIABLE GIT_MODIFIED_OUTPUT - OUTPUT_STRIP_TRAILING_WHITESPACE + COMMAND ${GIT_EXECUTABLE} status --porcelain -z + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE GIT_MODIFIED_OUTPUT + OUTPUT_STRIP_TRAILING_WHITESPACE ) - endif (GIT_REPOSITORY_FOUND) -endif (GIT_EXECUTABLE) + endif(GIT_REPOSITORY_FOUND) +endif(GIT_EXECUTABLE) + +if(GIT_MODIFIED_OUTPUT) + if(DEFINED ENV{CHATTERINO_REQUIRE_CLEAN_GIT}) + message(STATUS "git status --porcelain -z\n${GIT_MODIFIED_OUTPUT}") + message(FATAL_ERROR "Git repository was expected to be clean, but modifications were found!") + endif() -if (GIT_MODIFIED_OUTPUT) set(GIT_MODIFIED 1) -endif () +endif() -if (DEFINED ENV{GIT_HASH}) +if(DEFINED ENV{GIT_HASH}) set(GIT_HASH "$ENV{GIT_HASH}") -endif () -if (DEFINED ENV{GIT_COMMIT}) +endif() + +if(DEFINED ENV{GIT_COMMIT}) set(GIT_COMMIT "$ENV{GIT_COMMIT}") -endif () -if (DEFINED ENV{GIT_RELEASE}) +endif() + +if(DEFINED ENV{GIT_RELEASE}) set(GIT_RELEASE "$ENV{GIT_RELEASE}") -endif () +endif() message(STATUS "Injected git values: ${GIT_COMMIT} (${GIT_RELEASE}) modified: ${GIT_MODIFIED}") diff --git a/cmake/MacOSXBundleInfo.plist.in b/cmake/MacOSXBundleInfo.plist.in index ae08eb0d9..9077068db 100644 --- a/cmake/MacOSXBundleInfo.plist.in +++ b/cmake/MacOSXBundleInfo.plist.in @@ -6,8 +6,6 @@ English CFBundleExecutable ${MACOSX_BUNDLE_EXECUTABLE_NAME} - CFBundleGetInfoString - ${MACOSX_BUNDLE_INFO_STRING} CFBundleIconFile ${MACOSX_BUNDLE_ICON_FILE} CFBundleIdentifier diff --git a/cmake/resources/ResourcesAutogen.cpp.in b/cmake/resources/ResourcesAutogen.cpp.in new file mode 100644 index 000000000..ac5ad6b78 --- /dev/null +++ b/cmake/resources/ResourcesAutogen.cpp.in @@ -0,0 +1,13 @@ +/**************************************************************************** +** WARNING! This file is autogenerated by cmake +** WARNING! All changes made in this file will be lost! +*****************************************************************************/ +#include "ResourcesAutogen.hpp" + +namespace chatterino { + +Resources2::Resources2() +{ +@RES_SOURCE_CONTENT@ +} +} // namespace chatterino diff --git a/cmake/resources/ResourcesAutogen.hpp.in b/cmake/resources/ResourcesAutogen.hpp.in new file mode 100644 index 000000000..affafc3db --- /dev/null +++ b/cmake/resources/ResourcesAutogen.hpp.in @@ -0,0 +1,16 @@ +/**************************************************************************** +** WARNING! This file is autogenerated by cmake +** WARNING! All changes made in this file will be lost! +*****************************************************************************/ +#include + +namespace chatterino { + +class Resources2 +{ +public: + Resources2(); + +@RES_HEADER_CONTENT@ +}; +} // namespace chatterino diff --git a/cmake/resources/generate_resources.cmake b/cmake/resources/generate_resources.cmake new file mode 100644 index 000000000..0bd28a80e --- /dev/null +++ b/cmake/resources/generate_resources.cmake @@ -0,0 +1,95 @@ +set(RES_DIR "${CMAKE_SOURCE_DIR}/resources") +# Note: files in this ignorelist should be relative to ${RES_DIR} +set( + RES_IGNORED_FILES + .gitignore + qt.conf + resources.qrc + resources_autogenerated.qrc + themes/ChatterinoTheme.schema.json +) +set(RES_EXCLUDE_FILTER ^raw) +set(RES_IMAGE_EXCLUDE_FILTER "^(buttons/(update|clearSearch)|avatars|icon|settings|raw)") + +file(GLOB_RECURSE RES_ALL_FILES RELATIVE "${RES_DIR}" LIST_DIRECTORIES false CONFIGURE_DEPENDS "${RES_DIR}/*") +file(GLOB_RECURSE RES_IMAGE_FILES RELATIVE "${RES_DIR}" LIST_DIRECTORIES false CONFIGURE_DEPENDS "${RES_DIR}/*.png") + +list(REMOVE_ITEM RES_ALL_FILES ${RES_IGNORED_FILES}) +list(FILTER RES_ALL_FILES EXCLUDE REGEX ${RES_EXCLUDE_FILTER}) +list(FILTER RES_IMAGE_FILES EXCLUDE REGEX ${RES_IMAGE_EXCLUDE_FILTER}) + +############################### +# Generate resources_autogenerated.qrc +############################### +message(STATUS "Generating resources_autogenerated.qrc") +foreach (_file ${RES_ALL_FILES}) + list(APPEND RES_RESOURCES_CONTENT " ${_file}") +endforeach () +list(JOIN RES_RESOURCES_CONTENT "\n" RES_RESOURCES_CONTENT) +configure_file(${CMAKE_CURRENT_LIST_DIR}/resources_autogenerated.qrc.in ${RES_DIR}/resources_autogenerated.qrc @ONLY) + +############################### +# Generate ResourcesAutogen.cpp +############################### +message(STATUS "Generating ResourcesAutogen.cpp") +foreach (_file ${RES_IMAGE_FILES}) + get_filename_component(_ext "${_file}" EXT) + string(REPLACE "${_ext}" "" _var_name ${_file}) + string(REPLACE "/" "." _var_name ${_var_name}) + list(APPEND RES_SOURCE_CONTENT " this->${_var_name} = QPixmap(\":/${_file}\")\;") + list(APPEND RES_VAR_NAMES "${_var_name}") +endforeach () +list(JOIN RES_SOURCE_CONTENT "\n" RES_SOURCE_CONTENT) +configure_file(${CMAKE_CURRENT_LIST_DIR}/ResourcesAutogen.cpp.in ${CMAKE_BINARY_DIR}/autogen/ResourcesAutogen.cpp @ONLY) + +############################### +# Generate ResourcesAutogen.hpp +############################### +message(STATUS "Generating ResourcesAutogen.hpp") +set(_struct_list "") + +foreach (_file ${RES_IMAGE_FILES}) + get_filename_component(_dir "${_file}" DIRECTORY) + get_filename_component(_name "${_file}" NAME_WE) + string(REPLACE "/" "_" _dir "${_dir}") + if (NOT _dir) + set(_dir "root") + endif () + list(APPEND ${_dir} "${_name}") + list(APPEND _struct_list "${_dir}") +endforeach () + +list(REMOVE_DUPLICATES _struct_list) + +foreach (_str_name ${_struct_list}) + if (NOT "${_str_name}" STREQUAL "root") + list(APPEND RES_HEADER_CONTENT " struct {") + set(_indent " ") + else () + set(_indent " ") + endif () + foreach (_name ${${_str_name}}) + list(APPEND RES_HEADER_CONTENT "${_indent}QPixmap ${_name}\;") + endforeach () + if (NOT "${_str_name}" STREQUAL "root") + list(APPEND RES_HEADER_CONTENT " } ${_str_name}\;") + endif () +endforeach () + +list(JOIN RES_HEADER_CONTENT "\n" RES_HEADER_CONTENT) +configure_file(${CMAKE_CURRENT_LIST_DIR}/ResourcesAutogen.hpp.in ${CMAKE_BINARY_DIR}/autogen/ResourcesAutogen.hpp @ONLY) + +if (WIN32) + if (NOT PROJECT_VERSION_TWEAK) + set(PROJECT_VERSION_TWEAK 0) + endif() + string(TIMESTAMP CURRENT_YEAR "%Y") + configure_file(${CMAKE_CURRENT_LIST_DIR}/windows.rc.in ${CMAKE_BINARY_DIR}/autogen/windows.rc @ONLY) + list(APPEND RES_AUTOGEN_FILES "${CMAKE_BINARY_DIR}/autogen/windows.rc") +endif () + +list(APPEND RES_AUTOGEN_FILES + "${CMAKE_SOURCE_DIR}/resources/resources_autogenerated.qrc" + "${CMAKE_BINARY_DIR}/autogen/ResourcesAutogen.cpp" + "${CMAKE_BINARY_DIR}/autogen/ResourcesAutogen.hpp" + ) diff --git a/cmake/resources/resources_autogenerated.qrc.in b/cmake/resources/resources_autogenerated.qrc.in new file mode 100644 index 000000000..cce67fae9 --- /dev/null +++ b/cmake/resources/resources_autogenerated.qrc.in @@ -0,0 +1,9 @@ + + + +@RES_RESOURCES_CONTENT@ + + \ No newline at end of file diff --git a/cmake/resources/windows.rc.in b/cmake/resources/windows.rc.in new file mode 100644 index 000000000..a43cb8810 --- /dev/null +++ b/cmake/resources/windows.rc.in @@ -0,0 +1,34 @@ +#include + +IDI_ICON1 ICON "@RES_DIR@/icon.ico" + +VS_VERSION_INFO VERSIONINFO + FILEVERSION @PROJECT_VERSION_MAJOR@,@PROJECT_VERSION_MINOR@,@PROJECT_VERSION_PATCH@,@PROJECT_VERSION_TWEAK@ + PRODUCTVERSION @PROJECT_VERSION_MAJOR@,@PROJECT_VERSION_MINOR@,@PROJECT_VERSION_PATCH@,@PROJECT_VERSION_TWEAK@ + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK + FILEFLAGS VS_FF_SPECIALBUILD + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" + BEGIN + VALUE "ProductName", "Chatterino" + VALUE "ProductVersion", "@PROJECT_VERSION@" + VALUE "CompanyName", "Chatterino, @PROJECT_HOMEPAGE_URL@" + VALUE "FileDescription", "Chatterino" + VALUE "FileVersion", "@PROJECT_VERSION@" + VALUE "SpecialBuild", "@GIT_COMMIT@" + VALUE "InternalName", "Chatterino" + VALUE "OriginalFilename", "Chatterino" + VALUE "LegalCopyright", "Project contributors 2016-@CURRENT_YEAR@" + VALUE "Licence", "MIT" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 + END +END \ No newline at end of file diff --git a/cmake/sanitizers-cmake b/cmake/sanitizers-cmake index 99e159ec9..3f0542e4e 160000 --- a/cmake/sanitizers-cmake +++ b/cmake/sanitizers-cmake @@ -1 +1 @@ -Subproject commit 99e159ec9bc8dd362b08d18436bd40ff0648417b +Subproject commit 3f0542e4e034aab417c51b2b22c94f83355dee15 diff --git a/conanfile.py b/conanfile.py new file mode 100644 index 000000000..a1d498695 --- /dev/null +++ b/conanfile.py @@ -0,0 +1,55 @@ +from conan import ConanFile +from conan.tools.files import copy +from os import path + + +class Chatterino(ConanFile): + name = "Chatterino" + requires = "boost/1.83.0" + settings = "os", "compiler", "build_type", "arch" + default_options = { + "with_benchmark": False, + "with_openssl3": True, + "openssl*:shared": True, + "boost*:header_only": True, + } + options = { + "with_benchmark": [True, False], + # Qt is built with OpenSSL 3 from version 6.5.0 onwards + "with_openssl3": [True, False], + } + generators = "CMakeDeps", "CMakeToolchain" + + def requirements(self): + if self.options.get_safe("with_benchmark", False): + self.requires("benchmark/1.7.1") + + if self.options.get_safe("with_openssl3", False): + self.requires("openssl/3.2.0") + else: + self.requires("openssl/1.1.1t") + + def generate(self): + def copy_bin(dep, selector, subdir): + src = path.realpath(dep.cpp_info.bindirs[0]) + dst = path.realpath(path.join(self.build_folder, subdir)) + + if src == dst: + return + + copy(self, selector, src, dst, keep_path=False) + + for dep in self.dependencies.values(): + # macOS + copy_bin(dep, "*.dylib", "bin") + # Windows + copy_bin(dep, "*.dll", "bin") + copy_bin(dep, "*.dll", "Chatterino2") # used in CI + # Linux + copy( + self, + "*.so*", + dep.cpp_info.libdirs[0], + path.join(self.build_folder, "bin"), + keep_path=False, + ) diff --git a/conanfile.txt b/conanfile.txt deleted file mode 100644 index 3c3ff3169..000000000 --- a/conanfile.txt +++ /dev/null @@ -1,14 +0,0 @@ -[requires] -openssl/1.1.1m -boost/1.78.0 - -[generators] -cmake - -[options] -openssl:shared=True - -[imports] -bin, *.dll -> ./bin @ keep_path=False -bin, *.dll -> ./Chatterino2 @ keep_path=False -lib, *.so* -> ./bin @ keep_path=False diff --git a/docs/ChatterinoTheme.schema.json b/docs/ChatterinoTheme.schema.json new file mode 100644 index 000000000..972d7598a --- /dev/null +++ b/docs/ChatterinoTheme.schema.json @@ -0,0 +1,405 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "title": "Chatterino Theme", + "description": "Colors and metadata for a Chatterino 2 theme", + "definitions": { + "qt-color": { + "type": "string", + "$comment": "https://doc.qt.io/qt-5/qcolor.html#setNamedColor", + "anyOf": [ + { + "title": "#RGB", + "pattern": "^#[a-fA-F0-9]{3}$" + }, + { + "title": "#RRGGBB", + "pattern": "^#[a-fA-F0-9]{6}$" + }, + { + "title": "#AARRGGBB", + "$comment": "Note that this isn't identical to the CSS Color Moudle Level 4 where the alpha value is at the end.", + "pattern": "^#[a-fA-F0-9]{8}$" + }, + { + "title": "#RRRGGGBBB", + "pattern": "^#[a-fA-F0-9]{9}$" + }, + { + "title": "#RRRRGGGGBBBB", + "pattern": "^#[a-fA-F0-9]{12}$" + }, + { + "title": "SVG Color", + "description": "This enum is stricter than Qt. You could theoretically put tabs and spaces between characters in a named color and capitalize the color.", + "$comment": "https://www.w3.org/TR/SVG11/types.html#ColorKeywords", + "enum": [ + "aliceblue", + "antiquewhite", + "aqua", + "aquamarine", + "azure", + "beige", + "bisque", + "black", + "blanchedalmond", + "blue", + "blueviolet", + "brown", + "burlywood", + "cadetblue", + "chartreuse", + "chocolate", + "coral", + "cornflowerblue", + "cornsilk", + "crimson", + "cyan", + "darkblue", + "darkcyan", + "darkgoldenrod", + "darkgray", + "darkgreen", + "darkgrey", + "darkkhaki", + "darkmagenta", + "darkolivegreen", + "darkorange", + "darkorchid", + "darkred", + "darksalmon", + "darkseagreen", + "darkslateblue", + "darkslategray", + "darkslategrey", + "darkturquoise", + "darkviolet", + "deeppink", + "deepskyblue", + "dimgray", + "dimgrey", + "dodgerblue", + "firebrick", + "floralwhite", + "forestgreen", + "fuchsia", + "gainsboro", + "ghostwhite", + "gold", + "goldenrod", + "gray", + "grey", + "green", + "greenyellow", + "honeydew", + "hotpink", + "indianred", + "indigo", + "ivory", + "khaki", + "lavender", + "lavenderblush", + "lawngreen", + "lemonchiffon", + "lightblue", + "lightcoral", + "lightcyan", + "lightgoldenrodyellow", + "lightgray", + "lightgreen", + "lightgrey", + "lightpink", + "lightsalmon", + "lightseagreen", + "lightskyblue", + "lightslategray", + "lightslategrey", + "lightsteelblue", + "lightyellow", + "lime", + "limegreen", + "linen", + "magenta", + "maroon", + "mediumaquamarine", + "mediumblue", + "mediumorchid", + "mediumpurple", + "mediumseagreen", + "mediumslateblue", + "mediumspringgreen", + "mediumturquoise", + "mediumvioletred", + "midnightblue", + "mintcream", + "mistyrose", + "moccasin", + "navajowhite", + "navy", + "oldlace", + "olive", + "olivedrab", + "orange", + "orangered", + "orchid", + "palegoldenrod", + "palegreen", + "paleturquoise", + "palevioletred", + "papayawhip", + "peachpuff", + "peru", + "pink", + "plum", + "powderblue", + "purple", + "red", + "rosybrown", + "royalblue", + "saddlebrown", + "salmon", + "sandybrown", + "seagreen", + "seashell", + "sienna", + "silver", + "skyblue", + "slateblue", + "slategray", + "slategrey", + "snow", + "springgreen", + "steelblue", + "tan", + "teal", + "thistle", + "tomato", + "turquoise", + "violet", + "wheat", + "white", + "whitesmoke", + "yellow", + "yellowgreen" + ] + }, + { + "title": "transparent", + "enum": ["transparent"] + } + ] + }, + "tab-colors": { + "type": "object", + "additionalProperties": false, + "properties": { + "backgrounds": { + "type": "object", + "additionalProperties": false, + "properties": { + "hover": { "$ref": "#/definitions/qt-color" }, + "regular": { "$ref": "#/definitions/qt-color" }, + "unfocused": { "$ref": "#/definitions/qt-color" } + }, + "required": ["hover", "regular", "unfocused"] + }, + "line": { + "type": "object", + "additionalProperties": false, + "properties": { + "hover": { "$ref": "#/definitions/qt-color" }, + "regular": { "$ref": "#/definitions/qt-color" }, + "unfocused": { "$ref": "#/definitions/qt-color" } + }, + "required": ["hover", "regular", "unfocused"] + }, + "text": { "$ref": "#/definitions/qt-color" } + }, + "required": ["backgrounds", "line", "text"] + } + }, + "type": "object", + "additionalProperties": false, + "properties": { + "colors": { + "type": "object", + "additionalProperties": false, + "properties": { + "accent": { "$ref": "#/definitions/qt-color" }, + "messages": { + "type": "object", + "additionalProperties": false, + "properties": { + "backgrounds": { + "type": "object", + "additionalProperties": false, + "properties": { + "alternate": { "$ref": "#/definitions/qt-color" }, + "regular": { "$ref": "#/definitions/qt-color" } + }, + "required": ["alternate", "regular"] + }, + "disabled": { "$ref": "#/definitions/qt-color" }, + "highlightAnimationEnd": { "$ref": "#/definitions/qt-color" }, + "highlightAnimationStart": { "$ref": "#/definitions/qt-color" }, + "selection": { "$ref": "#/definitions/qt-color" }, + "textColors": { + "type": "object", + "additionalProperties": false, + "properties": { + "caret": { "$ref": "#/definitions/qt-color" }, + "chatPlaceholder": { "$ref": "#/definitions/qt-color" }, + "link": { "$ref": "#/definitions/qt-color" }, + "regular": { "$ref": "#/definitions/qt-color" }, + "system": { "$ref": "#/definitions/qt-color" } + }, + "required": [ + "caret", + "chatPlaceholder", + "link", + "regular", + "system" + ] + } + }, + "required": [ + "backgrounds", + "disabled", + "highlightAnimationEnd", + "highlightAnimationStart", + "selection", + "textColors" + ] + }, + "scrollbars": { + "type": "object", + "additionalProperties": false, + "properties": { + "background": { "$ref": "#/definitions/qt-color" }, + "thumb": { "$ref": "#/definitions/qt-color" }, + "thumbSelected": { "$ref": "#/definitions/qt-color" } + }, + "required": ["background", "thumb", "thumbSelected"] + }, + "splits": { + "type": "object", + "additionalProperties": false, + "properties": { + "background": { "$ref": "#/definitions/qt-color" }, + "dropPreview": { "$ref": "#/definitions/qt-color" }, + "dropPreviewBorder": { "$ref": "#/definitions/qt-color" }, + "dropTargetRect": { "$ref": "#/definitions/qt-color" }, + "dropTargetRectBorder": { "$ref": "#/definitions/qt-color" }, + "header": { + "type": "object", + "additionalProperties": false, + "properties": { + "background": { "$ref": "#/definitions/qt-color" }, + "border": { "$ref": "#/definitions/qt-color" }, + "focusedBackground": { "$ref": "#/definitions/qt-color" }, + "focusedBorder": { "$ref": "#/definitions/qt-color" }, + "focusedText": { "$ref": "#/definitions/qt-color" }, + "text": { "$ref": "#/definitions/qt-color" } + }, + "required": [ + "background", + "border", + "focusedBackground", + "focusedBorder", + "focusedText", + "text" + ] + }, + "input": { + "type": "object", + "additionalProperties": false, + "properties": { + "background": { "$ref": "#/definitions/qt-color" }, + "text": { "$ref": "#/definitions/qt-color" } + }, + "required": ["background", "text"] + }, + "messageSeperator": { "$ref": "#/definitions/qt-color" }, + "resizeHandle": { "$ref": "#/definitions/qt-color" }, + "resizeHandleBackground": { "$ref": "#/definitions/qt-color" } + }, + "required": [ + "background", + "dropPreview", + "dropPreviewBorder", + "dropTargetRect", + "dropTargetRectBorder", + "header", + "input", + "messageSeperator", + "resizeHandle", + "resizeHandleBackground" + ] + }, + "tabs": { + "type": "object", + "additionalProperties": false, + "properties": { + "liveIndicator": { "$ref": "#/definitions/qt-color" }, + "rerunIndicator": { "$ref": "#/definitions/qt-color" }, + "dividerLine": { "$ref": "#/definitions/qt-color" }, + "highlighted": { + "$ref": "#/definitions/tab-colors" + }, + "newMessage": { + "$ref": "#/definitions/tab-colors" + }, + "regular": { + "$ref": "#/definitions/tab-colors" + }, + "selected": { + "$ref": "#/definitions/tab-colors" + } + }, + "required": [ + "dividerLine", + "highlighted", + "newMessage", + "regular", + "selected" + ] + }, + "window": { + "type": "object", + "additionalProperties": false, + "properties": { + "background": { "$ref": "#/definitions/qt-color" }, + "text": { "$ref": "#/definitions/qt-color" } + }, + "required": ["background", "text"] + } + }, + "required": [ + "accent", + "messages", + "scrollbars", + "splits", + "tabs", + "window" + ] + }, + "metadata": { + "type": "object", + "additionalProperties": false, + "properties": { + "iconTheme": { + "$comment": "Determines which icons to use. 'dark' will use dark icons (best for a light theme). 'light' will use light icons.", + "enum": ["light", "dark"], + "default": "light" + }, + "fallbackTheme": { + "$comment": "Determines which built-in Chatterino theme to use as a fallback in case a color isn't configured.", + "enum": ["White", "Light", "Dark", "Black"], + "default": "Dark" + } + }, + "required": ["iconTheme"] + }, + "$schema": { "type": "string" } + }, + "required": ["colors", "metadata"] +} diff --git a/docs/chatterino.d.ts b/docs/chatterino.d.ts new file mode 100644 index 000000000..7929fbc3d --- /dev/null +++ b/docs/chatterino.d.ts @@ -0,0 +1,133 @@ +/** @noSelfInFile */ + +declare module c2 { + enum LogLevel { + Debug, + Info, + Warning, + Critical, + } + class CommandContext { + words: String[]; + channel: Channel; + } + + enum Platform { + Twitch, + } + enum ChannelType { + None, + Direct, + Twitch, + TwitchWhispers, + TwitchWatching, + TwitchMentions, + TwitchLive, + TwitchAutomod, + Irc, + Misc, + } + + interface IWeakResource { + is_valid(): boolean; + } + + interface ISharedResource {} + + class RoomModes { + unique_chat: boolean; + subscriber_only: boolean; + emotes_only: boolean; + follower_only: null | number; + slow_mode: null | number; + } + class StreamStatus { + live: boolean; + viewer_count: number; + uptime: number; + title: string; + game_name: string; + game_id: string; + } + + class Channel implements IWeakResource { + is_valid(): boolean; + get_name(): string; + get_type(): ChannelType; + get_display_name(): string; + send_message(message: string, execute_commands: boolean): void; + add_system_message(message: string): void; + + is_twitch_channel(): boolean; + + get_room_modes(): RoomModes; + get_stream_status(): StreamStatus; + get_twitch_id(): string; + is_broadcaster(): boolean; + is_mod(): boolean; + is_vip(): boolean; + + static by_name(name: string, platform: Platform): null | Channel; + static by_twitch_id(id: string): null | Channel; + } + + enum HTTPMethod { + Get, + Post, + Put, + Delete, + Patch, + } + + class HTTPResponse implements ISharedResource { + data(): string; + status(): number | null; + error(): string; + } + + type HTTPCallback = (res: HTTPResponse) => void; + class HTTPRequest implements ISharedResource { + on_success(callback: HTTPCallback): void; + on_error(callback: HTTPCallback): void; + finally(callback: () => void): void; + + set_timeout(millis: number): void; + set_payload(data: string): void; + set_header(name: string, value: string): void; + + execute(): void; + + // might error + static create(method: HTTPMethod, url: string): HTTPRequest; + } + + function log(level: LogLevel, ...data: any[]): void; + function register_command( + name: String, + handler: (ctx: CommandContext) => void + ): boolean; + + class CompletionEvent { + query: string; + full_text_content: string; + cursor_position: number; + is_first_word: boolean; + } + + class CompletionList { + values: String[]; + hide_others: boolean; + } + + enum EventType { + CompletionRequested = "CompletionRequested", + } + + type CbFuncCompletionsRequested = (ev: CompletionEvent) => CompletionList; + type CbFunc = T extends EventType.CompletionRequested + ? CbFuncCompletionsRequested + : never; + + function register_callback(type: T, func: CbFunc): void; + function later(callback: () => void, msec: number): void; +} diff --git a/docs/make-release.md b/docs/make-release.md index 27008ea5c..1509289fd 100644 --- a/docs/make-release.md +++ b/docs/make-release.md @@ -1,4 +1,25 @@ # Checklist for making a release +## In the release PR + - [ ] Updated version code in `src/common/Version.hpp` -- [ ] Updated version code in `CMakeLists.txt` +- [ ] Updated version code in `CMakeLists.txt` + This can only be "whole versions", so if you're releasing `2.4.0-beta` you'll need to condense it to `2.4.0` +- [ ] Add a new release at the top of the `releases` key in `resources/com.chatterino.chatterino.appdata.xml` + This cannot use dash to denote a pre-release identifier, you have to use a tilde instead. + +- [ ] Updated version code in `.CI/chatterino-installer.iss` + This can only be "whole versions", so if you're releasing `2.4.0-beta` you'll need to condense it to `2.4.0` + +- [ ] Update the changelog `## Unreleased` section to the new version `CHANGELOG.md` + Make sure to leave the `## Unreleased` line unchanged for easier merges + +## After the PR has been merged + +- [ ] Tag the release +- [ ] Manually run the [create-installer](https://github.com/Chatterino/chatterino2/actions/workflows/create-installer.yml) workflow. + This is only necessary if the tag was created after the CI in the main branch finished. +- [ ] Start a manual [Launchpad import](https://code.launchpad.net/~pajlada/chatterino/+git/chatterino) - scroll down & click Import Now +- [ ] Make a PPA release to [this repo](https://git.launchpad.net/~pajlada/+git/chatterino-packaging/) with the `debchange` command. + `debchange -v 2.4.0` then add the changelog entries + `debchange --release` then change the distro to be `unstable` diff --git a/docs/plugin-info.schema.json b/docs/plugin-info.schema.json new file mode 100644 index 000000000..07c1a29f9 --- /dev/null +++ b/docs/plugin-info.schema.json @@ -0,0 +1,62 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "https://raw.githubusercontent.com/Chatterino/chatterino2/master/docs/plugin-info.schema.json", + "title": "Plugin info", + "description": "Describes a Chatterino2 plugin (draft)", + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "Plugin name shown to the user." + }, + "description": { + "type": "string", + "description": "Plugin description shown to the user." + }, + "authors": { + "type": "array", + "description": "An array of authors of this Plugin.", + "items": { + "type": "string" + } + }, + "homepage": { + "type": "string", + "description": "Optional URL to your Plugin's homepage. This could be your GitHub repo for example." + }, + "tags": { + "description": "Something that could in the future be used to find your plugin.", + "type": "array", + "items": { + "type": "string", + "examples": ["moderation", "utility", "commands"] + }, + "uniqueItems": true + }, + "version": { + "type": "string", + "description": "Semver version string, for more info see https://semver.org.", + "examples": ["0.0.1", "1.0.0-rc.1"] + }, + "license": { + "type": "string", + "description": "SPDX identifier for license of this plugin. See https://spdx.org/licenses/", + "examples": ["MIT", "GPL-2.0-or-later"] + }, + "permissions": { + "type": "array", + "description": "The permissions the plugin needs to work.", + "items": { + "type": "object", + "properties": { + "type": { + "enum": ["FilesystemRead", "FilesystemWrite", "Network"] + } + } + } + }, + "$schema": { "type": "string" } + }, + "required": ["name", "description", "authors", "version", "license"] +} diff --git a/docs/plugin-meta.lua b/docs/plugin-meta.lua new file mode 100644 index 000000000..d4b1ac25b --- /dev/null +++ b/docs/plugin-meta.lua @@ -0,0 +1,263 @@ +---@meta Chatterino2 + +-- This file is automatically generated from src/controllers/plugins/LuaAPI.hpp by the scripts/make_luals_meta.py script +-- This file is intended to be used with LuaLS (https://luals.github.io/). +-- Add the folder this file is in to "Lua.workspace.library". + +c2 = {} +---@alias c2.LogLevel integer +---@type { Debug: c2.LogLevel, Info: c2.LogLevel, Warning: c2.LogLevel, Critical: c2.LogLevel } +c2.LogLevel = {} + +---@alias c2.EventType integer +---@type { CompletionRequested: c2.EventType } +c2.EventType = {} + +---@class CommandContext +---@field words string[] The words typed when executing the command. For example `/foo bar baz` will result in `{"/foo", "bar", "baz"}`. +---@field channel c2.Channel The channel the command was executed in. + +---@class CompletionList +---@field values string[] The completions +---@field hide_others boolean Whether other completions from Chatterino should be hidden/ignored. + +---@class CompletionEvent +---@field query string The word being completed +---@field full_text_content string Content of the text input +---@field cursor_position integer Position of the cursor in the text input in unicode codepoints (not bytes) +---@field is_first_word boolean True if this is the first word in the input + +-- Begin src/common/Channel.hpp + +---@alias c2.ChannelType integer +---@type { None: c2.ChannelType, Direct: c2.ChannelType, Twitch: c2.ChannelType, TwitchWhispers: c2.ChannelType, TwitchWatching: c2.ChannelType, TwitchMentions: c2.ChannelType, TwitchLive: c2.ChannelType, TwitchAutomod: c2.ChannelType, TwitchEnd: c2.ChannelType, Irc: c2.ChannelType, Misc: c2.ChannelType } +c2.ChannelType = {} + +-- End src/common/Channel.hpp + +-- Begin src/controllers/plugins/api/ChannelRef.hpp + +---@alias c2.Platform integer +--- This enum describes a platform for the purpose of searching for a channel. +--- Currently only Twitch is supported because identifying IRC channels is tricky. +---@type { Twitch: c2.Platform } +c2.Platform = {} + +---@class c2.Channel +c2.Channel = {} + +--- Returns true if the channel this object points to is valid. +--- If the object expired, returns false +--- If given a non-Channel object, it errors. +--- +---@return boolean success +function c2.Channel:is_valid() end + +--- Gets the channel's name. This is the lowercase login name. +--- +---@return string name +function c2.Channel:get_name() end + +--- Gets the channel's type +--- +---@return c2.ChannelType +function c2.Channel:get_type() end + +--- Get the channel owner's display name. This may contain non-lowercase ascii characters. +--- +---@return string name +function c2.Channel:get_display_name() end + +--- Sends a message to the target channel. +--- Note that this does not execute client-commands. +--- +---@param message string +---@param execute_commands boolean Should commands be run on the text? +function c2.Channel:send_message(message, execute_commands) end + +--- Adds a system message client-side +--- +---@param message string +function c2.Channel:add_system_message(message) end + +--- Returns true for twitch channels. +--- Compares the channel Type. Note that enum values aren't guaranteed, just +--- that they are equal to the exposed enum. +--- +---@return boolean +function c2.Channel:is_twitch_channel() end + +--- Returns a copy of the channel mode settings (subscriber only, r9k etc.) +--- +---@return RoomModes +function c2.Channel:get_room_modes() end + +--- Returns a copy of the stream status. +--- +---@return StreamStatus +function c2.Channel:get_stream_status() end + +--- Returns the Twitch user ID of the owner of the channel. +--- +---@return string +function c2.Channel:get_twitch_id() end + +--- Returns true if the channel is a Twitch channel and the user owns it +--- +---@return boolean +function c2.Channel:is_broadcaster() end + +--- Returns true if the channel is a Twitch channel and the user is a moderator in the channel +--- Returns false for broadcaster. +--- +---@return boolean +function c2.Channel:is_mod() end + +--- Returns true if the channel is a Twitch channel and the user is a VIP in the channel +--- Returns false for broadcaster. +--- +---@return boolean +function c2.Channel:is_vip() end + +---@return string +function c2.Channel:__tostring() end + +--- Finds a channel by name. +--- Misc channels are marked as Twitch: +--- - /whispers +--- - /mentions +--- - /watching +--- - /live +--- - /automod +--- +---@param name string Which channel are you looking for? +---@param platform c2.Platform Where to search for the channel? +---@return c2.Channel? +function c2.Channel.by_name(name, platform) end + +--- Finds a channel by the Twitch user ID of its owner. +--- +---@param id string ID of the owner of the channel. +---@return c2.Channel? +function c2.Channel.by_twitch_id(id) end + +---@class RoomModes +---@field unique_chat boolean You might know this as r9kbeta or robot9000. +---@field subscriber_only boolean +---@field emotes_only boolean Whether or not text is allowed in messages. Note that "emotes" here only means Twitch emotes, not Unicode emoji, nor 3rd party text-based emotes +---@field follower_only number? Time in minutes you need to follow to chat or nil. +---@field slow_mode number? Time in seconds you need to wait before sending messages or nil. + +---@class StreamStatus +---@field live boolean +---@field viewer_count number +---@field uptime number Seconds since the stream started. +---@field title string Stream title or last stream title +---@field game_name string +---@field game_id string + +-- End src/controllers/plugins/api/ChannelRef.hpp + +-- Begin src/controllers/plugins/api/HTTPResponse.hpp + +---@class HTTPResponse +HTTPResponse = {} + +--- Returns the data. This is not guaranteed to be encoded using any +--- particular encoding scheme. It's just the bytes the server returned. +--- +function HTTPResponse:data() end + +--- Returns the status code. +--- +function HTTPResponse:status() end + +--- A somewhat human readable description of an error if such happened +--- +function HTTPResponse:error() end + +-- End src/controllers/plugins/api/HTTPResponse.hpp + +-- Begin src/controllers/plugins/api/HTTPRequest.hpp + +---@alias HTTPCallback fun(result: HTTPResponse): nil +---@class HTTPRequest +HTTPRequest = {} + +--- Sets the success callback +--- +---@param callback HTTPCallback Function to call when the HTTP request succeeds +function HTTPRequest:on_success(callback) end + +--- Sets the failure callback +--- +---@param callback HTTPCallback Function to call when the HTTP request fails or returns a non-ok status +function HTTPRequest:on_error(callback) end + +--- Sets the finally callback +--- +---@param callback fun(): nil Function to call when the HTTP request finishes +function HTTPRequest:finally(callback) end + +--- Sets the timeout +--- +---@param timeout integer How long in milliseconds until the times out +function HTTPRequest:set_timeout(timeout) end + +--- Sets the request payload +--- +---@param data string +function HTTPRequest:set_payload(data) end + +--- Sets a header in the request +--- +---@param name string +---@param value string +function HTTPRequest:set_header(name, value) end + +--- Executes the HTTP request +--- +function HTTPRequest:execute() end + +--- Creates a new HTTPRequest +--- +---@param method HTTPMethod Method to use +---@param url string Where to send the request to +---@return HTTPRequest +function HTTPRequest.create(method, url) end + +-- End src/controllers/plugins/api/HTTPRequest.hpp + +-- Begin src/common/network/NetworkCommon.hpp + +---@alias HTTPMethod integer +---@type { Get: HTTPMethod, Post: HTTPMethod, Put: HTTPMethod, Delete: HTTPMethod, Patch: HTTPMethod } +HTTPMethod = {} + +-- End src/common/network/NetworkCommon.hpp + +--- Registers a new command called `name` which when executed will call `handler`. +--- +---@param name string The name of the command. +---@param handler fun(ctx: CommandContext) The handler to be invoked when the command gets executed. +---@return boolean ok Returns `true` if everything went ok, `false` if a command with this name exists. +function c2.register_command(name, handler) end + +--- Registers a callback to be invoked when completions for a term are requested. +--- +---@param type "CompletionRequested" +---@param func fun(event: CompletionEvent): CompletionList The callback to be invoked. +function c2.register_callback(type, func) end + +--- Writes a message to the Chatterino log. +--- +---@param level c2.LogLevel The desired level. +---@param ... any Values to log. Should be convertible to a string with `tostring()`. +function c2.log(level, ...) end + +--- Calls callback around msec milliseconds later. Does not freeze Chatterino. +--- +---@param callback fun() The callback that will be called. +---@param msec number How long to wait. +function c2.later(callback, msec) end + diff --git a/docs/test-and-benchmark.md b/docs/test-and-benchmark.md index e881db6be..7e42ab56b 100644 --- a/docs/test-and-benchmark.md +++ b/docs/test-and-benchmark.md @@ -1,6 +1,6 @@ # Test and Benchmark -Chatterino includes a set of unit tests and benchmarks. These can be built using cmake by adding the `-DBUILD_TESTS=On` and `-DBUILD_BENCHMARKS=On` flags respectively. +Chatterino includes a set of unit tests and benchmarks. These can be built using CMake by adding the `-DBUILD_TESTS=On` and `-DBUILD_BENCHMARKS=On` flags respectively. ## Adding your own test diff --git a/docs/wip-plugins.md b/docs/wip-plugins.md new file mode 100644 index 000000000..cd38fa18c --- /dev/null +++ b/docs/wip-plugins.md @@ -0,0 +1,661 @@ +# Plugins + +If Chatterino is compiled with the `CHATTERINO_PLUGINS` CMake option, it can +load and execute Lua files. Note that while there are attempts at making this +decently safe, we cannot guarantee safety. + +## Plugin structure + +Chatterino searches for plugins in the `Plugins` directory in the app data, right next to `Settings` and `Logs`. + +Each plugin should have its own directory. + +``` +Chatterino Plugins dir/ +└── plugin_name/ + ├── init.lua + ├── info.json + └── data/ + └── This is where your data/configs can be dumped +``` + +`init.lua` will be the file loaded when the plugin is enabled. You may load other files using [`require` global function](#requiremodname). + +`info.json` contains metadata about the plugin, like its name, description, +authors, homepage link, tags, version, license name. The version field **must** +be [semver 2.0](https://semver.org/) compliant. The general idea of `info.json` +will not change however the exact contents probably will, for example with +permission system ideas. +Example file: + +```json +{ + "$schema": "https://raw.githubusercontent.com/Chatterino/chatterino2/master/docs/plugin-info.schema.json", + "name": "Test plugin", + "description": "This plugin is for testing stuff.", + "authors": ["Mm2PL"], + "homepage": "https://github.com/Chatterino/Chatterino2", + "tags": ["test"], + "version": "0.0.0", + "license": "MIT", + "permissions": [] +} +``` + +An example plugin is available at [https://github.com/Mm2PL/Chatterino-test-plugin](https://github.com/Mm2PL/Chatterino-test-plugin) + +## Permissions + +Plugins can have permissions associated to them. Unless otherwise noted functions don't require permissions. +These are the valid permissions: + +### FilesystemRead + +Allows the plugin to read from its data directory. + +Example: + +```json +{ + ..., + "permissions": [ + { + "type": "FilesystemRead" + }, + ... + ] +} +``` + +### FilesystemWrite + +Allows the plugin to write to files and create files in its data directory. + +Example: + +```json +{ + ..., + "permissions": [ + { + "type": "FilesystemWrite" + }, + ... + ] +} +``` + +### Network + +Allows the plugin to send HTTP requests. + +Example: + +```json +{ + ..., + "permissions": [ + { + "type": "Network" + }, + ... + ] +} +``` + +## Plugins with Typescript + +If you prefer, you may use [TypescriptToLua](https://typescripttolua.github.io) +to typecheck your plugins. There is a `chatterino.d.ts` file describing the API +in this directory. However, this has several drawbacks like harder debugging at +runtime. + +## LuaLS type definitions + +Type definitions for LuaLS are available in +[the `/plugin-meta.lua` file](./plugin-meta.lua). These are generated from [the C++ +headers](../src/controllers/plugins/LuaAPI.hpp) of Chatterino using [a +script](../scripts/make_luals_meta.py). + +## API + +The following parts of the Lua standard library are loaded: + +- `_G` (most globals) +- `io` - except `stdin`, `stdout`, `stderr`. Some functions require permissions. +- `math` +- `string` +- `table` +- `utf8` + +The official manual for them is available [here](https://www.lua.org/manual/5.4/manual.html#6). + +### Chatterino API + +All Chatterino functions are exposed in a global table called `c2`. The following members are available: + +#### `log(level, args...)` + +Writes a message to the Chatterino log. The `level` argument should be a +`LogLevel` member. All `args` should be convertible to a string with +`tostring()`. + +Example: + +```lua +c2.log(c2.LogLevel.Warning, "Hello, this should show up in the Chatterino log by default") + +c2.log(c2.LogLevel.Debug, "Hello world") +-- Equivalent to doing qCDebug(chatterinoLua) << "[pluginDirectory:Plugin Name]" << "Hello, world"; from C++ +``` + +#### `LogLevel` enum + +This table describes log levels available to Lua Plugins. The values behind the names may change, do not count on them. It has the following keys: + +- `Debug` +- `Info` +- `Warning` +- `Critical` + +#### `register_command(name, handler)` + +Registers a new command called `name` which when executed will call `handler`. +Returns `true` if everything went ok, `false` if there already exists another +command with this name. + +Example: + +```lua +function cmd_words(ctx) + -- ctx contains: + -- words - table of words supplied to the command including the trigger + -- channel - the channel the command is being run in + channel:add_system_message("Words are: " .. table.concat(ctx.words, " ")) +end + +c2.register_command("/words", cmd_words) +``` + +Limitations/known issues: + +- Commands registered in functions, not in the global scope might not show up in the settings UI, + rebuilding the window content caused by reloading another plugin will solve this. +- Spaces in command names aren't handled very well (https://github.com/Chatterino/chatterino2/issues/1517). + +#### `register_callback("CompletionRequested", handler)` + +Registers a callback (`handler`) to process completions. The callback takes a single table with the following entries: + +- `query`: The queried word. +- `full_text_content`: The whole input. +- `cursor_position`: The position of the cursor in the input. +- `is_first_word`: Flag whether `query` is the first word in the input. + +Example: + +| Input | `query` | `full_text_content` | `cursor_position` | `is_first_word` | +| ---------- | ------- | ------------------- | ----------------- | --------------- | +| `foo│` | `foo` | `foo` | 3 | `true` | +| `fo│o` | `fo` | `foo` | 2 | `true` | +| `foo bar│` | `bar` | `foo bar` | 7 | `false` | +| `foo │bar` | `foo` | `foo bar` | 4 | `false` | + +```lua +function string.startswith(s, other) + return string.sub(s, 1, string.len(other)) == other +end + +c2.register_callback( + "CompletionRequested", + function(event) + if ("!join"):startswith(event.query) then + ---@type CompletionList + return { hide_others = true, values = { "!join" } } + end + ---@type CompletionList + return { hide_others = false, values = {} } + end +) +``` + +#### `Platform` enum + +This table describes platforms that can be accessed. Chatterino supports IRC +however plugins do not yet have explicit access to get IRC channels objects. +The values behind the names may change, do not count on them. It has the +following keys: + +- `Twitch` + +#### `ChannelType` enum + +This table describes channel types Chatterino supports. The values behind the +names may change, do not count on them. It has the following keys: + +- `None` +- `Direct` +- `Twitch` +- `TwitchWhispers` +- `TwitchWatching` +- `TwitchMentions` +- `TwitchLive` +- `TwitchAutomod` +- `TwitchEnd` +- `Irc` +- `Misc` + +#### `Channel` + +This is a type that represents a channel. Existence of this object doesn't +force Chatterino to hold the channel open. Should the user close the last split +holding this channel open, your Channel object will expire. You can check for +this using the `Channel:is_valid()` function. Using any other function on an +expired Channel yields an error. Using any `Channel` member function on a +non-`Channel` table also yields an error. + +Some functions make sense only for Twitch channel, these yield an error when +used on non-Twitch channels. Special channels while marked as +`is_twitch_channel() = true` do not have these functions. To check if a channel +is an actual Twitch chatroom use `Channel:get_type()` instead of +`Channel:is_twitch_channel()`. + +##### `Channel:by_name(name, platform)` + +Finds a channel given by `name` on `platform` (see [`Platform` enum](#Platform-enum)). Returns the channel or `nil` if not open. + +Some miscellaneous channels are marked as if they are specifically Twitch channels: + +- `/whispers` +- `/mentions` +- `/watching` +- `/live` +- `/automod` + +Example: + +```lua +local pajladas = c2.Channel.by_name("pajlada", c2.Platform.Twitch) +``` + +##### `Channel:by_twitch_id(id)` + +Finds a channel given by the string representation of the owner's Twitch user ID. Returns the channel or `nil` if not open. + +Example: + +```lua +local pajladas = c2.Channel.by_twitch_id("11148817") +``` + +##### `Channel:get_name()` + +On Twitch returns the lowercase login name of the channel owner. On IRC returns the normalized channel name. + +Example: + +```lua +-- Note: if the channel is not open this errors +pajladas:get_name() -- "pajlada" +``` + +##### `Channel:get_type()` + +Returns the channel's type. See [`ChannelType` enum](#ChannelType-enum). + +##### `Channel:get_display_name()` + +Returns the channel owner's display name. This can contain characters that are not lowercase and even non-ASCII. + +Example: + +```lua +local saddummys = c2.Channel.by_name("saddummy") +saddummys:get_display_name() -- "서새봄냥" +``` + + + +##### `Channel:send_message(message[, execute_commands])` + +Sends a message to the channel with the given text. If `execute_commands` is +not present or `false` commands will not be executed client-side, this affects +all user commands and all Twitch commands except `/me`. + +Examples: + +```lua +-- times out @Mm2PL +pajladas:send_message("/timeout mm2pl 1s test", true) + +-- results in a "Unknown command" error from Twitch +pajladas:send_message("/timeout mm2pl 1s test") + +-- Given a user command "hello": +-- this will execute it +pajladas:send_message("hello", true) +-- this will send "hello" literally, bypassing commands +pajladas:send_message("hello") + +function cmd_shout(ctx) + table.remove(ctx.words, 1) + local output = table.concat(ctx.words, " ") + ctx.channel:send_message(string.upper(output)) +end +c2.register_command("/shout", cmd_shout) +``` + +Limitations/Known issues: + +- It is possible to trigger your own Lua command with this causing a potentially infinite loop. + +##### `Channel:add_system_message(message)` + +Shows a system message in the channel with the given text. + +Example: + +```lua +pajladas:add_system_message("Hello, world!") +``` + +##### `Channel:is_twitch_channel()` + +Returns `true` if the channel is a Twitch channel, that is its type name has +the `Twitch` prefix. This returns `true` for special channels like Mentions. +You might want `Channel:get_type() == "Twitch"` if you want to use +Twitch-specific functions. + +##### `Channel:get_twitch_id()` + +Returns the string form of the channel owner's Twitch user ID. + +Example: + +```lua +pajladas:get_twitch_id() -- "11148817" +``` + +##### `Channel:is_broadcaster()` + +Returns `true` if the channel is owned by the current user. + +##### `Channel:is_mod()` + +Returns `true` if the channel can be moderated by the current user. + +##### `Channel:is_vip()` + +Returns `true` if the current user is a VIP in the channel. + +#### `HTTPMethod` enum + +This table describes HTTP methods available to Lua Plugins. The values behind +the names may change, do not count on them. It has the following keys: + +- `Get` +- `Post` +- `Put` +- `Delete` +- `Patch` + +#### `HTTPResponse` + +An `HTTPResponse` is a table you receive in the callback after a completed `HTTPRequest`. + +##### `HTTPResponse.data()` + +This function returns the data received from the server as a string. Usually +this will be UTF-8-encoded however that is not guaranteed, this could be any +binary data. + +##### `HTTPResponse.error()` + +If an error happened this function returns a human readable description of it. + +It returns something like: `"ConnectionRefusedError"`, `"401"`. + +##### `HTTPResponse.status()` + +This function returns the HTTP status code of the request or `nil` if there was +an error before a status code could be received. + +#### `HTTPRequest` + +Allows you to send an HTTP request to a URL. Do not create requests that you +don't want to call `execute()` on. For the time being that leaks callback +functions and all their upvalues with them. + +##### `HTTPRequest.create(method, url)` + +Creates a new `HTTPRequest`. The `method` argument is an +[`HTTPMethod`](#HTTPMethod-enum). The `url` argument must be a string +containing a valid URL (ex. `https://example.com/path/to/api`). + +```lua +local req = c2.HTTPRequest.create(c2.HTTPMethod.Get, "https://example.com") +req:on_success(function (res) + print(res:data()) +end) +req:execute() +``` + +##### `HTTPRequest:on_success(callback)` + +Sets the success callback. It accepts a function that takes a single parameter +of type `HTTPResponse`. The callback will be called on success. This function +returns nothing. + +##### `HTTPRequest:on_error(callback)` + +Sets the error callback. It accepts a function that takes a single parameter of +type `HTTPResponse`. The callback will be called if the request fails. To see why +it failed check the `error` field of the result. This function returns nothing. + +##### `HTTPRequest:finally(callback)` + +Sets the finally callback. It accepts a function that takes no parameters and +returns nothing. It will be always called after `success` or `error`. This +function returns nothing. + +##### `HTTPRequest:set_timeout(timeout)` + +Sets how long the request will take before it times out. The `timeout` +parameter is in milliseconds. This function returns nothing. + +##### `HTTPRequest:set_payload(data)` + +Sets the data that will be sent with the request. The `data` parameter must be +a string. This function returns nothing. + +##### `HTTPRequest:set_header(name, value)` + +Adds or overwrites a header in the request. Both `name` and `value` should be +strings. If they are not strings they will be converted to strings. This +function returns nothing. + +##### `HTTPRequest:execute()` + +Sends the request. This function returns nothing. + +```lua +local url = "http://localhost:8080/thing" +local request = c2.HTTPRequest.create("Post", url) +request:set_timeout(1000) +request:set_payload("TEST!") +request:set_header("X-Test", "Testing!") +request:set_header("Content-Type", "text/plain") +request:on_success(function (res) + print('Success!') + -- Data is in res.data + print(res:status()) +end) +request:on_error(function (res) + print('Error!') + print(res:status()) + print(res:error()) +end) +request:finally(function () + print('Finally') +end) +request:execute() + +-- This prints: +-- Success! +-- [content of /thing] +-- 200 +-- Finally + +-- Or: +-- Error! +-- nil +-- ConnectionRefusedError +``` + +### Input/Output API + +These functions are wrappers for Lua's I/O library. Functions on file pointer +objects (`FILE*`) are not modified or replaced. [You can read the documentation +for them here](https://www.lua.org/manual/5.4/manual.html#pdf-file:close). +Chatterino does _not_ give you stdin and stdout as default input and output +respectively. The following objects are missing from the `io` table exposed by +Chatterino compared to Lua's native library: `stdin`, `stdout`, `stderr`. + +#### `close([file])` + +Closes a file. If not given, `io.output()` is used instead. + +See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.close) + +#### `flush()` + +Flushes `io.output()`. + +See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.flush) + +#### `input([file_or_name])` + +When called with no arguments this function returns the default input file. +This variant requires no permissions. + +When called with a file object, it will set the default input file to the one +given. This one also requires no permissions. + +When called with a filename as a string, it will open that file for reading. +Equivalent to: `io.input(io.open(filename))`. This variant requires +the `FilesystemRead` permission and the given file to be within the plugin's +data directory. + +See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.input) + +#### `lines([filename, ...])` + +With no arguments this function is equivalent to `io.input():lines("l")`. See +[Lua documentation for file:flush()](https://www.lua.org/manual/5.4/manual.html#pdf-file:flush). +This variant requires no permissions. + +With `filename` given it is most like `io.open(filename):lines(...)`. This +variant requires the `FilesystemRead` permission and the given file to be +within the plugin's data directory. + +See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.lines) + +#### `open(filename [, mode])` + +This functions opens the given file with a mode. It requires `filename` to be +within the plugin's data directory. A call with no mode given is equivalent to +one with `mode="r"`. +Depending on the mode this function has slightly different behavior: + +| Mode | Permission | Read? | Write? | Truncate? | Create? | +| ----------- | ----------------- | ----- | ------ | --------- | ------- | +| `r` read | `FilesystemRead` | Yes | No | No | No | +| `w` write | `FilesystemWrite` | No | Yes | Yes | Yes | +| `a` append | `FilesystemWrite` | No | Append | No | Yes | +| `r+` update | `FilesystemWrite` | Yes | Yes | No | No | +| `w+` update | `FilesystemWrite` | Yes | Yes | Yes | Yes | +| `a+` update | `FilesystemWrite` | Yes | Append | No | Yes | + +To open a file in binary mode add a `b` at the end of the mode. + +See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.open) + +#### `output([file_or_name])` + +This is identical to [`io.input()`](#inputfile_or_name) but operates on the +default output and opens the file in write mode instead. Requires +`FilesystemWrite` instead of `FilesystemRead`. + +See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.output) + +#### `popen(exe [, mode])` + +This function is unavailable in Chatterino. Calling it results in an error +message to let you know that it's not available, no permissions needed. + +See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.popen) + +#### `read(...)` + +Equivalent to `io.input():read(...)`. See [`io.input()`](#inputfile_or_name) +and [`file:read()`](https://www.lua.org/manual/5.4/manual.html#pdf-file:read). + +See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.read) + +#### `tmpfile()` + +This function is unavailable in Chatterino. Calling it results in an error +message to let you know that it's not available, no permissions needed. + +See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.tmpfile) + +#### `type(obj)` + +This functions allows you to tell if the object is a `file`, a `closed file` or +a different bit of data. + +See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.type) + +#### `write(...)` + +Equivalent to `io.output():write(...)`. See [`io.output()`](#outputfile_or_name) +and [`file:write()`](https://www.lua.org/manual/5.4/manual.html#pdf-file:write). + +See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.write) + +### Changed globals + +#### `load(chunk [, chunkname [, mode [, env]]])` + +This function is only available if Chatterino is compiled in debug mode. It is meant for debugging with little exception. +This function behaves really similarity to Lua's `load`, however it does not allow for bytecode to be executed. +It achieves this by forcing all inputs to be encoded with `UTF-8`. + +See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-load) + +#### `require(modname)` + +This is Lua's [`require()`](https://www.lua.org/manual/5.3/manual.html#pdf-require) function. +However, the searcher and load configuration is notably different from the default: + +- Lua's built-in dynamic library searcher is removed, +- `package.path` is not used, in its place are two searchers, +- when `require()` is used, first a file relative to the currently executing + file will be checked, then a file relative to the plugin directory, +- binary chunks are never loaded, +- files inside of the plugin `data` directory are never loaded + +As in normal Lua, dots are converted to the path separators (`'/'` on Linux and Mac, `'\'` on Windows). + +Example: + +```lua +require("stuff") -- executes Plugins/name/stuff.lua or $(dirname $CURR_FILE)/stuff.lua +require("dir.name") -- executes Plugins/name/dir/name.lua or $(dirname $CURR_FILE)/dir/name.lua +require("binary") -- tried to load Plugins/name/binary.lua and errors because binary is not a text file +require("data.file") -- tried to load Plugins/name/data/file.lua and errors because that is not allowed +``` + +#### `print(Args...)` + +The `print` global function is equivalent to calling `c2.log(c2.LogLevel.Debug, Args...)` diff --git a/lib/README.md b/lib/README.md index 40d2acf17..ea37d0b63 100644 --- a/lib/README.md +++ b/lib/README.md @@ -1,3 +1,3 @@ -Third party libraries are stored here +Third party libraries are stored here. Fetched via `git submodule update --init --recursive` diff --git a/lib/WinToast b/lib/WinToast index 5e441fd03..a78ce469b 160000 --- a/lib/WinToast +++ b/lib/WinToast @@ -1 +1 @@ -Subproject commit 5e441fd03543b999edb663caf8df7be37c0d575c +Subproject commit a78ce469b456c06103b3b30d4bd37e7bb80da30c diff --git a/lib/expected-lite b/lib/expected-lite new file mode 160000 index 000000000..f339d2f73 --- /dev/null +++ b/lib/expected-lite @@ -0,0 +1 @@ +Subproject commit f339d2f73730f8fee4412f5e4938717866ecef48 diff --git a/lib/libcommuni b/lib/libcommuni index a7b32cd6f..030710ad5 160000 --- a/lib/libcommuni +++ b/lib/libcommuni @@ -1 +1 @@ -Subproject commit a7b32cd6fa0640721b6114b31d37d79ebf832411 +Subproject commit 030710ad53dda1541601ccabbad36a12a9e90c78 diff --git a/lib/lua/CMakeLists.txt b/lib/lua/CMakeLists.txt new file mode 100644 index 000000000..cf2fad9bd --- /dev/null +++ b/lib/lua/CMakeLists.txt @@ -0,0 +1,53 @@ +project(lua CXX) + +#[====[ +Updating this list: +remove all listed files +go to line below, ^y2j4j$@" and then reindent the file names +/LUA_SRC +:r!ls lib/lua/src | grep '\.c' | grep -Ev 'lua\.c|onelua\.c' | sed 's#^#src/#' + +#]====] +set(LUA_SRC + "src/lapi.c" + "src/lauxlib.c" + "src/lbaselib.c" + "src/lcode.c" + "src/lcorolib.c" + "src/lctype.c" + "src/ldblib.c" + "src/ldebug.c" + "src/ldo.c" + "src/ldump.c" + "src/lfunc.c" + "src/lgc.c" + "src/linit.c" + "src/liolib.c" + "src/llex.c" + "src/lmathlib.c" + "src/lmem.c" + "src/loadlib.c" + "src/lobject.c" + "src/lopcodes.c" + "src/loslib.c" + "src/lparser.c" + "src/lstate.c" + "src/lstring.c" + "src/lstrlib.c" + "src/ltable.c" + "src/ltablib.c" + "src/ltests.c" + "src/ltm.c" + "src/lua.c" + "src/lundump.c" + "src/lutf8lib.c" + "src/lvm.c" + "src/lzio.c" +) + +add_library(lua STATIC ${LUA_SRC}) +target_include_directories(lua + PUBLIC + ${LUA_INCLUDE_DIRS} +) +set_source_files_properties(${LUA_SRC} PROPERTIES LANGUAGE C) diff --git a/lib/lua/src b/lib/lua/src new file mode 160000 index 000000000..0897c0a42 --- /dev/null +++ b/lib/lua/src @@ -0,0 +1 @@ +Subproject commit 0897c0a4289ef3a8d45761266124613f364bef60 diff --git a/lib/magic_enum b/lib/magic_enum index f4ebb4f18..e55b9b54d 160000 --- a/lib/magic_enum +++ b/lib/magic_enum @@ -1 +1 @@ -Subproject commit f4ebb4f185ce956bf50b93acbef1516030ecdb36 +Subproject commit e55b9b54d5cf61f8e117cafb17846d7d742dd3b4 diff --git a/lib/miniaudio b/lib/miniaudio new file mode 160000 index 000000000..4a5b74bef --- /dev/null +++ b/lib/miniaudio @@ -0,0 +1 @@ +Subproject commit 4a5b74bef029b3592c54b6048650ee5f972c1a48 diff --git a/lib/qBreakpad b/lib/qBreakpad deleted file mode 160000 index a4626c12e..000000000 --- a/lib/qBreakpad +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a4626c12e9ae6f02fc1ca7a4e399bd8307424103 diff --git a/lib/qtkeychain b/lib/qtkeychain index de9546273..e5b070831 160000 --- a/lib/qtkeychain +++ b/lib/qtkeychain @@ -1 +1 @@ -Subproject commit de954627363b0b4bff9a2616f1a409b7e14d5df9 +Subproject commit e5b070831cf1ea3cb98c95f97fcb7439f8d79bd6 diff --git a/lib/semver/README.md b/lib/semver/README.md new file mode 100644 index 000000000..792300807 --- /dev/null +++ b/lib/semver/README.md @@ -0,0 +1,3 @@ +From https://github.com/Neargye/semver + +Downloaded 2023-01-25 from commit hash [eae828abf579836ba2ecc72a8604ad1b6fb10d86](https://github.com/Neargye/semver/commit/eae828abf579836ba2ecc72a8604ad1b6fb10d86) diff --git a/lib/semver/include/semver/semver.hpp b/lib/semver/include/semver/semver.hpp new file mode 100644 index 000000000..b132c52e4 --- /dev/null +++ b/lib/semver/include/semver/semver.hpp @@ -0,0 +1,858 @@ +// _____ _ _ +// / ____| | | (_) +// | (___ ___ _ __ ___ __ _ _ __ | |_ _ ___ +// \___ \ / _ \ '_ ` _ \ / _` | '_ \| __| |/ __| +// ____) | __/ | | | | | (_| | | | | |_| | (__ +// |_____/ \___|_| |_| |_|\__,_|_| |_|\__|_|\___| +// __ __ _ _ _____ +// \ \ / / (_) (_) / ____|_ _ +// \ \ / /__ _ __ ___ _ ___ _ __ _ _ __ __ _ | | _| |_ _| |_ +// \ \/ / _ \ '__/ __| |/ _ \| '_ \| | '_ \ / _` | | | |_ _|_ _| +// \ / __/ | \__ \ | (_) | | | | | | | | (_| | | |____|_| |_| +// \/ \___|_| |___/_|\___/|_| |_|_|_| |_|\__, | \_____| +// https://github.com/Neargye/semver __/ | +// version 0.3.0 |___/ +// +// Licensed under the MIT License . +// SPDX-License-Identifier: MIT +// Copyright (c) 2018 - 2021 Daniil Goncharov . +// Copyright (c) 2020 - 2021 Alexander Gorbunov . +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#ifndef NEARGYE_SEMANTIC_VERSIONING_HPP +#define NEARGYE_SEMANTIC_VERSIONING_HPP + +#define SEMVER_VERSION_MAJOR 0 +#define SEMVER_VERSION_MINOR 3 +#define SEMVER_VERSION_PATCH 0 + +#include +#include +#include +#include +#include +#include +#include +#if __has_include() +#include +#else +#include +#endif + +#if defined(SEMVER_CONFIG_FILE) +#include SEMVER_CONFIG_FILE +#endif + +#if defined(SEMVER_THROW) +// define SEMVER_THROW(msg) to override semver throw behavior. +#elif defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND) +# include +# define SEMVER_THROW(msg) (throw std::invalid_argument{msg}) +#else +# include +# include +# define SEMVER_THROW(msg) (assert(!msg), std::abort()) +#endif + +#if defined(__clang__) +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wmissing-braces" // Ignore warning: suggest braces around initialization of subobject 'return {first, std::errc::invalid_argument};'. +#endif + +namespace semver { + +enum struct prerelease : std::uint8_t { + alpha = 0, + beta = 1, + rc = 2, + none = 3 +}; + +#if __has_include() +struct from_chars_result : std::from_chars_result { + [[nodiscard]] constexpr operator bool() const noexcept { return ec == std::errc{}; } +}; + +struct to_chars_result : std::to_chars_result { + [[nodiscard]] constexpr operator bool() const noexcept { return ec == std::errc{}; } +}; +#else +struct from_chars_result { + const char* ptr; + std::errc ec; + + [[nodiscard]] constexpr operator bool() const noexcept { return ec == std::errc{}; } +}; + +struct to_chars_result { + char* ptr; + std::errc ec; + + [[nodiscard]] constexpr operator bool() const noexcept { return ec == std::errc{}; } +}; +#endif + +// Max version string length = 3() + 1(.) + 3() + 1(.) + 3() + 1(-) + 5() + 1(.) + 3() = 21. +inline constexpr auto max_version_string_length = std::size_t{21}; + +namespace detail { + +inline constexpr auto alpha = std::string_view{"alpha", 5}; +inline constexpr auto beta = std::string_view{"beta", 4}; +inline constexpr auto rc = std::string_view{"rc", 2}; + +// Min version string length = 1() + 1(.) + 1() + 1(.) + 1() = 5. +inline constexpr auto min_version_string_length = 5; + +constexpr char to_lower(char c) noexcept { + return (c >= 'A' && c <= 'Z') ? static_cast(c + ('a' - 'A')) : c; +} + +constexpr bool is_digit(char c) noexcept { + return c >= '0' && c <= '9'; +} + +constexpr bool is_space(char c) noexcept { + return c == ' '; +} + +constexpr bool is_operator(char c) noexcept { + return c == '<' || c == '>' || c == '='; +} + +constexpr bool is_dot(char c) noexcept { + return c == '.'; +} + +constexpr bool is_logical_or(char c) noexcept { + return c == '|'; +} + +constexpr bool is_hyphen(char c) noexcept { + return c == '-'; +} + +constexpr bool is_letter(char c) noexcept { + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); +} + +constexpr std::uint8_t to_digit(char c) noexcept { + return static_cast(c - '0'); +} + +constexpr std::uint8_t length(std::uint8_t x) noexcept { + return x < 10 ? 1 : (x < 100 ? 2 : 3); +} + +constexpr std::uint8_t length(prerelease t) noexcept { + if (t == prerelease::alpha) { + return static_cast(alpha.length()); + } else if (t == prerelease::beta) { + return static_cast(beta.length()); + } else if (t == prerelease::rc) { + return static_cast(rc.length()); + } + + return 0; +} + +constexpr bool equals(const char* first, const char* last, std::string_view str) noexcept { + for (std::size_t i = 0; first != last && i < str.length(); ++i, ++first) { + if (to_lower(*first) != to_lower(str[i])) { + return false; + } + } + + return true; +} + +constexpr char* to_chars(char* str, std::uint8_t x, bool dot = true) noexcept { + do { + *(--str) = static_cast('0' + (x % 10)); + x /= 10; + } while (x != 0); + + if (dot) { + *(--str) = '.'; + } + + return str; +} + +constexpr char* to_chars(char* str, prerelease t) noexcept { + const auto p = t == prerelease::alpha + ? alpha + : t == prerelease::beta + ? beta + : t == prerelease::rc + ? rc + : std::string_view{}; + + if (p.size() > 0) { + for (auto it = p.rbegin(); it != p.rend(); ++it) { + *(--str) = *it; + } + *(--str) = '-'; + } + + return str; +} + +constexpr const char* from_chars(const char* first, const char* last, std::uint8_t& d) noexcept { + if (first != last && is_digit(*first)) { + std::int32_t t = 0; + for (; first != last && is_digit(*first); ++first) { + t = t * 10 + to_digit(*first); + } + if (t <= (std::numeric_limits::max)()) { + d = static_cast(t); + return first; + } + } + + return nullptr; +} + +constexpr const char* from_chars(const char* first, const char* last, std::optional& d) noexcept { + if (first != last && is_digit(*first)) { + std::int32_t t = 0; + for (; first != last && is_digit(*first); ++first) { + t = t * 10 + to_digit(*first); + } + if (t <= (std::numeric_limits::max)()) { + d = static_cast(t); + return first; + } + } + + return nullptr; +} + +constexpr const char* from_chars(const char* first, const char* last, prerelease& p) noexcept { + if (is_hyphen(*first)) { + ++first; + } + + if (equals(first, last, alpha)) { + p = prerelease::alpha; + return first + alpha.length(); + } else if (equals(first, last, beta)) { + p = prerelease::beta; + return first + beta.length(); + } else if (equals(first, last, rc)) { + p = prerelease::rc; + return first + rc.length(); + } + + return nullptr; +} + +constexpr bool check_delimiter(const char* first, const char* last, char d) noexcept { + return first != last && first != nullptr && *first == d; +} + +template +struct resize_uninitialized { + static auto resize(T& str, std::size_t size) -> std::void_t { + str.resize(size); + } +}; + +template +struct resize_uninitialized().__resize_default_init(42))>> { + static void resize(T& str, std::size_t size) { + str.__resize_default_init(size); + } +}; + +} // namespace semver::detail + +struct version { + std::uint8_t major = 0; + std::uint8_t minor = 1; + std::uint8_t patch = 0; + prerelease prerelease_type = prerelease::none; + std::optional prerelease_number = std::nullopt; + + constexpr version(std::uint8_t mj, + std::uint8_t mn, + std::uint8_t pt, + prerelease prt = prerelease::none, + std::optional prn = std::nullopt) noexcept + : major{mj}, + minor{mn}, + patch{pt}, + prerelease_type{prt}, + prerelease_number{prt == prerelease::none ? std::nullopt : prn} { + } + + + explicit constexpr version(std::string_view str) : version(0, 0, 0, prerelease::none, 0) { + from_string(str); + } + + constexpr version() = default; // https://semver.org/#how-should-i-deal-with-revisions-in-the-0yz-initial-development-phase + + constexpr version(const version&) = default; + + constexpr version(version&&) = default; + + ~version() = default; + + version& operator=(const version&) = default; + + version& operator=(version&&) = default; + + [[nodiscard]] constexpr from_chars_result from_chars(const char* first, const char* last) noexcept { + if (first == nullptr || last == nullptr || (last - first) < detail::min_version_string_length) { + return {first, std::errc::invalid_argument}; + } + + auto next = first; + if (next = detail::from_chars(next, last, major); detail::check_delimiter(next, last, '.')) { + if (next = detail::from_chars(++next, last, minor); detail::check_delimiter(next, last, '.')) { + if (next = detail::from_chars(++next, last, patch); next == last) { + prerelease_type = prerelease::none; + prerelease_number = {}; + return {next, std::errc{}}; + } else if (detail::check_delimiter(next, last, '-')) { + if (next = detail::from_chars(next, last, prerelease_type); next == last) { + prerelease_number = {}; + return {next, std::errc{}}; + } else if (detail::check_delimiter(next, last, '.')) { + if (next = detail::from_chars(++next, last, prerelease_number); next == last) { + return {next, std::errc{}}; + } + } + } + } + } + + return {first, std::errc::invalid_argument}; + } + + [[nodiscard]] constexpr to_chars_result to_chars(char* first, char* last) const noexcept { + const auto length = string_length(); + if (first == nullptr || last == nullptr || (last - first) < length) { + return {last, std::errc::value_too_large}; + } + + auto next = first + length; + if (prerelease_type != prerelease::none) { + if (prerelease_number.has_value()) { + next = detail::to_chars(next, prerelease_number.value()); + } + next = detail::to_chars(next, prerelease_type); + } + next = detail::to_chars(next, patch); + next = detail::to_chars(next, minor); + next = detail::to_chars(next, major, false); + + return {first + length, std::errc{}}; + } + + [[nodiscard]] constexpr bool from_string_noexcept(std::string_view str) noexcept { + return from_chars(str.data(), str.data() + str.length()); + } + + constexpr version& from_string(std::string_view str) { + if (!from_string_noexcept(str)) { + SEMVER_THROW("semver::version::from_string invalid version."); + } + + return *this; + } + + [[nodiscard]] std::string to_string() const { + auto str = std::string{}; + detail::resize_uninitialized::resize(str, string_length()); + if (!to_chars(str.data(), str.data() + str.length())) { + SEMVER_THROW("semver::version::to_string invalid version."); + } + + return str; + } + + [[nodiscard]] constexpr std::uint8_t string_length() const noexcept { + // () + 1(.) + () + 1(.) + () + auto length = detail::length(major) + detail::length(minor) + detail::length(patch) + 2; + if (prerelease_type != prerelease::none) { + // + 1(-) + () + length += detail::length(prerelease_type) + 1; + if (prerelease_number.has_value()) { + // + 1(.) + () + length += detail::length(prerelease_number.value()) + 1; + } + } + + return static_cast(length); + } + + [[nodiscard]] constexpr int compare(const version& other) const noexcept { + if (major != other.major) { + return major - other.major; + } + + if (minor != other.minor) { + return minor - other.minor; + } + + if (patch != other.patch) { + return patch - other.patch; + } + + if (prerelease_type != other.prerelease_type) { + return static_cast(prerelease_type) - static_cast(other.prerelease_type); + } + + if (prerelease_number.has_value()) { + if (other.prerelease_number.has_value()) { + return prerelease_number.value() - other.prerelease_number.value(); + } + return 1; + } else if (other.prerelease_number.has_value()) { + return -1; + } + + return 0; + } +}; + +[[nodiscard]] constexpr bool operator==(const version& lhs, const version& rhs) noexcept { + return lhs.compare(rhs) == 0; +} + +[[nodiscard]] constexpr bool operator!=(const version& lhs, const version& rhs) noexcept { + return lhs.compare(rhs) != 0; +} + +[[nodiscard]] constexpr bool operator>(const version& lhs, const version& rhs) noexcept { + return lhs.compare(rhs) > 0; +} + +[[nodiscard]] constexpr bool operator>=(const version& lhs, const version& rhs) noexcept { + return lhs.compare(rhs) >= 0; +} + +[[nodiscard]] constexpr bool operator<(const version& lhs, const version& rhs) noexcept { + return lhs.compare(rhs) < 0; +} + +[[nodiscard]] constexpr bool operator<=(const version& lhs, const version& rhs) noexcept { + return lhs.compare(rhs) <= 0; +} + +[[nodiscard]] constexpr version operator""_version(const char* str, std::size_t length) { + return version{std::string_view{str, length}}; +} + +[[nodiscard]] constexpr bool valid(std::string_view str) noexcept { + return version{}.from_string_noexcept(str); +} + +[[nodiscard]] constexpr from_chars_result from_chars(const char* first, const char* last, version& v) noexcept { + return v.from_chars(first, last); +} + +[[nodiscard]] constexpr to_chars_result to_chars(char* first, char* last, const version& v) noexcept { + return v.to_chars(first, last); +} + +[[nodiscard]] constexpr std::optional from_string_noexcept(std::string_view str) noexcept { + if (version v{}; v.from_string_noexcept(str)) { + return v; + } + + return std::nullopt; +} + +[[nodiscard]] constexpr version from_string(std::string_view str) { + return version{str}; +} + +[[nodiscard]] inline std::string to_string(const version& v) { + return v.to_string(); +} + +template +inline std::basic_ostream& operator<<(std::basic_ostream& os, const version& v) { + for (const auto c : v.to_string()) { + os.put(c); + } + + return os; +} + +inline namespace comparators { + +enum struct comparators_option : std::uint8_t { + exclude_prerelease, + include_prerelease +}; + +[[nodiscard]] constexpr int compare(const version& lhs, const version& rhs, comparators_option option = comparators_option::include_prerelease) noexcept { + if (option == comparators_option::exclude_prerelease) { + return version{lhs.major, lhs.minor, lhs.patch}.compare(version{rhs.major, rhs.minor, rhs.patch}); + } + return lhs.compare(rhs); +} + +[[nodiscard]] constexpr bool equal_to(const version& lhs, const version& rhs, comparators_option option = comparators_option::include_prerelease) noexcept { + return compare(lhs, rhs, option) == 0; +} + +[[nodiscard]] constexpr bool not_equal_to(const version& lhs, const version& rhs, comparators_option option = comparators_option::include_prerelease) noexcept { + return compare(lhs, rhs, option) != 0; +} + +[[nodiscard]] constexpr bool greater(const version& lhs, const version& rhs, comparators_option option = comparators_option::include_prerelease) noexcept { + return compare(lhs, rhs, option) > 0; +} + +[[nodiscard]] constexpr bool greater_equal(const version& lhs, const version& rhs, comparators_option option = comparators_option::include_prerelease) noexcept { + return compare(lhs, rhs, option) >= 0; +} + +[[nodiscard]] constexpr bool less(const version& lhs, const version& rhs, comparators_option option = comparators_option::include_prerelease) noexcept { + return compare(lhs, rhs, option) < 0; +} + +[[nodiscard]] constexpr bool less_equal(const version& lhs, const version& rhs, comparators_option option = comparators_option::include_prerelease) noexcept { + return compare(lhs, rhs, option) <= 0; +} + +} // namespace semver::comparators + +namespace range { + +namespace detail { + +using namespace semver::detail; + +class range { + public: + constexpr explicit range(std::string_view str) noexcept : parser{str} {} + + constexpr bool satisfies(const version& ver, bool include_prerelease) { + const bool has_prerelease = ver.prerelease_type != prerelease::none; + + do { + if (is_logical_or_token()) { + parser.advance_token(range_token_type::logical_or); + } + + bool contains = true; + bool allow_compare = include_prerelease; + + while (is_operator_token() || is_number_token()) { + const auto range = parser.parse_range(); + const bool equal_without_tags = equal_to(range.ver, ver, comparators_option::exclude_prerelease); + + if (has_prerelease && equal_without_tags) { + allow_compare = true; + } + + if (!range.satisfies(ver)) { + contains = false; + break; + } + } + + if (has_prerelease) { + if (allow_compare && contains) { + return true; + } + } else if (contains) { + return true; + } + + } while (is_logical_or_token()); + + return false; + } + +private: + enum struct range_operator : std::uint8_t { + less, + less_or_equal, + greater, + greater_or_equal, + equal + }; + + struct range_comparator { + range_operator op; + version ver; + + constexpr bool satisfies(const version& version) const { + switch (op) { + case range_operator::equal: + return version == ver; + case range_operator::greater: + return version > ver; + case range_operator::greater_or_equal: + return version >= ver; + case range_operator::less: + return version < ver; + case range_operator::less_or_equal: + return version <= ver; + default: + SEMVER_THROW("semver::range unexpected operator."); + } + } + }; + + enum struct range_token_type : std::uint8_t { + none, + number, + range_operator, + dot, + logical_or, + hyphen, + prerelease, + end_of_line + }; + + struct range_token { + range_token_type type = range_token_type::none; + std::uint8_t number = 0; + range_operator op = range_operator::equal; + prerelease prerelease_type = prerelease::none; + }; + + struct range_lexer { + std::string_view text; + std::size_t pos; + + constexpr explicit range_lexer(std::string_view text) noexcept : text{text}, pos{0} {} + + constexpr range_token get_next_token() noexcept { + while (!end_of_line()) { + + if (is_space(text[pos])) { + advance(1); + continue; + } + + if (is_logical_or(text[pos])) { + advance(2); + return {range_token_type::logical_or}; + } + + if (is_operator(text[pos])) { + const auto op = get_operator(); + return {range_token_type::range_operator, 0, op}; + } + + if (is_digit(text[pos])) { + const auto number = get_number(); + return {range_token_type::number, number}; + } + + if (is_dot(text[pos])) { + advance(1); + return {range_token_type::dot}; + } + + if (is_hyphen(text[pos])) { + advance(1); + return {range_token_type::hyphen}; + } + + if (is_letter(text[pos])) { + const auto prerelease = get_prerelease(); + return {range_token_type::prerelease, 0, range_operator::equal, prerelease}; + } + } + + return {range_token_type::end_of_line}; + } + + constexpr bool end_of_line() const noexcept { return pos >= text.length(); } + + constexpr void advance(std::size_t i) noexcept { + pos += i; + } + + constexpr range_operator get_operator() noexcept { + if (text[pos] == '<') { + advance(1); + if (text[pos] == '=') { + advance(1); + return range_operator::less_or_equal; + } + return range_operator::less; + } else if (text[pos] == '>') { + advance(1); + if (text[pos] == '=') { + advance(1); + return range_operator::greater_or_equal; + } + return range_operator::greater; + } else if (text[pos] == '=') { + advance(1); + return range_operator::equal; + } + + return range_operator::equal; + } + + constexpr std::uint8_t get_number() noexcept { + const auto first = text.data() + pos; + const auto last = text.data() + text.length(); + if (std::uint8_t n{}; from_chars(first, last, n) != nullptr) { + advance(length(n)); + return n; + } + + return 0; + } + + constexpr prerelease get_prerelease() noexcept { + const auto first = text.data() + pos; + const auto last = text.data() + text.length(); + if (first > last) { + advance(1); + return prerelease::none; + } + + if (prerelease p{}; from_chars(first, last, p) != nullptr) { + advance(length(p)); + return p; + } + + advance(1); + + return prerelease::none; + } + }; + + struct range_parser { + range_lexer lexer; + range_token current_token; + + constexpr explicit range_parser(std::string_view str) : lexer{str}, current_token{range_token_type::none} { + advance_token(range_token_type::none); + } + + constexpr void advance_token(range_token_type token_type) { + if (current_token.type != token_type) { + SEMVER_THROW("semver::range unexpected token."); + } + current_token = lexer.get_next_token(); + } + + constexpr range_comparator parse_range() { + if (current_token.type == range_token_type::number) { + const auto version = parse_version(); + return {range_operator::equal, version}; + } else if (current_token.type == range_token_type::range_operator) { + const auto range_operator = current_token.op; + advance_token(range_token_type::range_operator); + const auto version = parse_version(); + return {range_operator, version}; + } + + return {range_operator::equal, version{}}; + } + + constexpr version parse_version() { + const auto major = parse_number(); + + advance_token(range_token_type::dot); + const auto minor = parse_number(); + + advance_token(range_token_type::dot); + const auto patch = parse_number(); + + prerelease prerelease = prerelease::none; + std::optional prerelease_number = std::nullopt; + + if (current_token.type == range_token_type::hyphen) { + advance_token(range_token_type::hyphen); + prerelease = parse_prerelease(); + if (current_token.type == range_token_type::dot) { + advance_token(range_token_type::dot); + prerelease_number = parse_number(); + } + } + + return {major, minor, patch, prerelease, prerelease_number}; + } + + constexpr std::uint8_t parse_number() { + const auto token = current_token; + advance_token(range_token_type::number); + + return token.number; + } + + constexpr prerelease parse_prerelease() { + const auto token = current_token; + advance_token(range_token_type::prerelease); + + return token.prerelease_type; + } + }; + + [[nodiscard]] constexpr bool is_logical_or_token() const noexcept { + return parser.current_token.type == range_token_type::logical_or; + } + [[nodiscard]] constexpr bool is_operator_token() const noexcept { + return parser.current_token.type == range_token_type::range_operator; + } + + [[nodiscard]] constexpr bool is_number_token() const noexcept { + return parser.current_token.type == range_token_type::number; + } + + range_parser parser; +}; + +} // namespace semver::range::detail + +enum struct satisfies_option : std::uint8_t { + exclude_prerelease, + include_prerelease +}; + +constexpr bool satisfies(const version& ver, std::string_view str, satisfies_option option = satisfies_option::exclude_prerelease) { + switch (option) { + case satisfies_option::exclude_prerelease: + return detail::range{str}.satisfies(ver, false); + case satisfies_option::include_prerelease: + return detail::range{str}.satisfies(ver, true); + default: + SEMVER_THROW("semver::range unexpected satisfies_option."); + } +} + +} // namespace semver::range + +// Version lib semver. +inline constexpr auto semver_version = version{SEMVER_VERSION_MAJOR, SEMVER_VERSION_MINOR, SEMVER_VERSION_PATCH}; + +} // namespace semver + +#if defined(__clang__) +# pragma clang diagnostic pop +#endif + +#endif // NEARGYE_SEMANTIC_VERSIONING_HPP diff --git a/lib/serialize b/lib/serialize index 7d37cbfd5..17946d65a 160000 --- a/lib/serialize +++ b/lib/serialize @@ -1 +1 @@ -Subproject commit 7d37cbfd5ac3bfbe046118e1cec3d32ba4696469 +Subproject commit 17946d65a41a72b447da37df6e314cded9650c32 diff --git a/lib/settings b/lib/settings index 04792d853..7011003ea 160000 --- a/lib/settings +++ b/lib/settings @@ -1 +1 @@ -Subproject commit 04792d853c7f83c9d7ab4df00279442a658d3be3 +Subproject commit 7011003eabf6ac95c19f523968a72771fc177fcd diff --git a/lib/signals b/lib/signals index 25e4ec3b8..d06770649 160000 --- a/lib/signals +++ b/lib/signals @@ -1 +1 @@ -Subproject commit 25e4ec3b8d6ea94a5e65a26e7cfcbbce3b87c5d6 +Subproject commit d06770649a7e83db780865d09c313a876bf0f4eb diff --git a/lib/websocketpp b/lib/websocketpp index 1b11fd301..b9aeec6ea 160000 --- a/lib/websocketpp +++ b/lib/websocketpp @@ -1 +1 @@ -Subproject commit 1b11fd301531e6df35a6107c1e8665b1e77a2d8e +Subproject commit b9aeec6eaf3d5610503439b4fae3581d9aff08e8 diff --git a/mocks/CMakeLists.txt b/mocks/CMakeLists.txt new file mode 100644 index 000000000..47abd0ef4 --- /dev/null +++ b/mocks/CMakeLists.txt @@ -0,0 +1,7 @@ +project(chatterino-mocks) + +add_library(chatterino-mocks INTERFACE) + +target_include_directories(chatterino-mocks INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include) + +target_link_libraries(${PROJECT_NAME} INTERFACE gmock) diff --git a/mocks/include/mocks/BaseApplication.hpp b/mocks/include/mocks/BaseApplication.hpp new file mode 100644 index 000000000..2ba9f949c --- /dev/null +++ b/mocks/include/mocks/BaseApplication.hpp @@ -0,0 +1,76 @@ +#pragma once + +#include "common/Args.hpp" +#include "mocks/DisabledStreamerMode.hpp" +#include "mocks/EmptyApplication.hpp" +#include "providers/bttv/BttvLiveUpdates.hpp" +#include "singletons/Fonts.hpp" +#include "singletons/Settings.hpp" +#include "singletons/Theme.hpp" + +#include + +namespace chatterino::mock { + +/** + * BaseApplication intends to be a mock application with a few more sane defaults, but with less configurability + */ +class BaseApplication : public EmptyApplication +{ +public: + BaseApplication() + : settings(this->args, this->settingsDir.path()) + , updates(this->paths_, this->settings) + , theme(this->paths_) + , fonts(this->settings) + { + } + + explicit BaseApplication(const QString &settingsData) + : EmptyApplication(settingsData) + , settings(this->args, this->settingsDir.path()) + , updates(this->paths_, this->settings) + , theme(this->paths_) + , fonts(this->settings) + { + } + + Updates &getUpdates() override + { + return this->updates; + } + + IStreamerMode *getStreamerMode() override + { + return &this->streamerMode; + } + + Theme *getThemes() override + { + return &this->theme; + } + + Fonts *getFonts() override + { + return &this->fonts; + } + + BttvLiveUpdates *getBttvLiveUpdates() override + { + return nullptr; + } + + SeventvEventAPI *getSeventvEventAPI() override + { + return nullptr; + } + + Args args; + Settings settings; + Updates updates; + DisabledStreamerMode streamerMode; + Theme theme; + Fonts fonts; +}; + +} // namespace chatterino::mock diff --git a/mocks/include/mocks/Channel.hpp b/mocks/include/mocks/Channel.hpp new file mode 100644 index 000000000..0af536e46 --- /dev/null +++ b/mocks/include/mocks/Channel.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include "common/Channel.hpp" + +namespace chatterino::mock { + +class MockChannel : public Channel +{ +public: + MockChannel(const QString &name) + : Channel(name, Channel::Type::Twitch) + { + } +}; + +} // namespace chatterino::mock diff --git a/mocks/include/mocks/ChatterinoBadges.hpp b/mocks/include/mocks/ChatterinoBadges.hpp new file mode 100644 index 000000000..9070a7d7e --- /dev/null +++ b/mocks/include/mocks/ChatterinoBadges.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include "providers/chatterino/ChatterinoBadges.hpp" + +namespace chatterino::mock { + +class ChatterinoBadges : public IChatterinoBadges +{ +public: + std::optional getBadge(const UserId &id) override + { + (void)id; + return std::nullopt; + } +}; + +} // namespace chatterino::mock diff --git a/mocks/include/mocks/DisabledStreamerMode.hpp b/mocks/include/mocks/DisabledStreamerMode.hpp new file mode 100644 index 000000000..747fdd0ad --- /dev/null +++ b/mocks/include/mocks/DisabledStreamerMode.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include "singletons/StreamerMode.hpp" + +class DisabledStreamerMode : public chatterino::IStreamerMode +{ +public: + bool isEnabled() const override + { + return false; + } + + void start() override + { + } +}; diff --git a/mocks/include/mocks/EmptyApplication.hpp b/mocks/include/mocks/EmptyApplication.hpp new file mode 100644 index 000000000..947a55f3d --- /dev/null +++ b/mocks/include/mocks/EmptyApplication.hpp @@ -0,0 +1,265 @@ +#pragma once + +#include "Application.hpp" +#include "common/Args.hpp" +#include "singletons/Paths.hpp" +#include "singletons/Updates.hpp" + +#include + +namespace chatterino::mock { + +class EmptyApplication : public IApplication +{ +public: + EmptyApplication() = default; + + explicit EmptyApplication(const QString &settingsData) + { + QFile settingsFile(this->settingsDir.filePath("settings.json")); + settingsFile.open(QIODevice::WriteOnly | QIODevice::Text); + settingsFile.write(settingsData.toUtf8()); + settingsFile.flush(); + settingsFile.close(); + } + + ~EmptyApplication() override = default; + + bool isTest() const override + { + return true; + } + + const Paths &getPaths() override + { + return this->paths_; + } + + const Args &getArgs() override + { + return this->args_; + } + + Theme *getThemes() override + { + assert( + false && + "EmptyApplication::getThemes was called without being initialized"); + return nullptr; + } + + Fonts *getFonts() override + { + assert( + false && + "EmptyApplication::getFonts was called without being initialized"); + return nullptr; + } + + IEmotes *getEmotes() override + { + assert( + false && + "EmptyApplication::getEmotes was called without being initialized"); + return nullptr; + } + + AccountController *getAccounts() override + { + assert(false && "EmptyApplication::getAccounts was called without " + "being initialized"); + return nullptr; + } + + HotkeyController *getHotkeys() override + { + assert(false && "EmptyApplication::getHotkeys was called without being " + "initialized"); + return nullptr; + } + + WindowManager *getWindows() override + { + assert(false && "EmptyApplication::getWindows was called without being " + "initialized"); + return nullptr; + } + + Toasts *getToasts() override + { + assert( + false && + "EmptyApplication::getToasts was called without being initialized"); + return nullptr; + } + + CrashHandler *getCrashHandler() override + { + assert(false && "EmptyApplication::getCrashHandler was called without " + "being initialized"); + return nullptr; + } + + CommandController *getCommands() override + { + assert(false && "EmptyApplication::getCommands was called without " + "being initialized"); + return nullptr; + } + + NotificationController *getNotifications() override + { + assert(false && "EmptyApplication::getNotifications was called without " + "being initialized"); + return nullptr; + } + + HighlightController *getHighlights() override + { + assert(false && "EmptyApplication::getHighlights was called without " + "being initialized"); + return nullptr; + } + + ITwitchIrcServer *getTwitch() override + { + assert( + false && + "EmptyApplication::getTwitch was called without being initialized"); + return nullptr; + } + + PubSub *getTwitchPubSub() override + { + assert(false && "getTwitchPubSub was called without being initialized"); + return nullptr; + } + + TwitchBadges *getTwitchBadges() override + { + assert(false && "getTwitchBadges was called without being initialized"); + return nullptr; + } + + ILogging *getChatLogger() override + { + assert(!"getChatLogger was called without being initialized"); + return nullptr; + } + + IChatterinoBadges *getChatterinoBadges() override + { + assert(false && "EmptyApplication::getChatterinoBadges was called " + "without being initialized"); + return nullptr; + } + + FfzBadges *getFfzBadges() override + { + assert(false && "EmptyApplication::getFfzBadges was called without " + "being initialized"); + return nullptr; + } + + SeventvBadges *getSeventvBadges() override + { + assert(!"getSeventvBadges was called without being initialized"); + return nullptr; + } + + IUserDataController *getUserData() override + { + assert(false && "EmptyApplication::getUserData was called without " + "being initialized"); + return nullptr; + } + + ISoundController *getSound() override + { + assert(!"getSound was called without being initialized"); + return nullptr; + } + + ITwitchLiveController *getTwitchLiveController() override + { + assert(false && "EmptyApplication::getTwitchLiveController was called " + "without being initialized"); + return nullptr; + } + + ImageUploader *getImageUploader() override + { + assert(false && "EmptyApplication::getImageUploader was called without " + "being initialized"); + return nullptr; + } + + SeventvAPI *getSeventvAPI() override + { + return nullptr; + } + +#ifdef CHATTERINO_HAVE_PLUGINS + PluginController *getPlugins() override + { + assert(false && "EmptyApplication::getPlugins was called without " + "being initialized"); + return nullptr; + } +#endif + + BttvEmotes *getBttvEmotes() override + { + assert(false && "EmptyApplication::getBttvEmotes was called without " + "being initialized"); + return nullptr; + } + + BttvLiveUpdates *getBttvLiveUpdates() override + { + assert(false && "EmptyApplication::getBttvLiveUpdates was called " + "without being initialized"); + return nullptr; + } + + FfzEmotes *getFfzEmotes() override + { + assert(false && "EmptyApplication::getFfzEmotes was called without " + "being initialized"); + return nullptr; + } + + SeventvEmotes *getSeventvEmotes() override + { + assert(false && "EmptyApplication::getSeventvEmotes was called without " + "being initialized"); + return nullptr; + } + + SeventvEventAPI *getSeventvEventAPI() override + { + assert(false && "EmptyApplication::getSeventvEventAPI was called " + "without being initialized"); + return nullptr; + } + + ILinkResolver *getLinkResolver() override + { + assert(false && "EmptyApplication::getLinkResolver was called without " + "being initialized"); + return nullptr; + } + + IStreamerMode *getStreamerMode() override + { + assert(false && "EmptyApplication::getStreamerMode was called without " + "being initialized"); + return nullptr; + } + + QTemporaryDir settingsDir; + Paths paths_; + Args args_; +}; + +} // namespace chatterino::mock diff --git a/mocks/include/mocks/Helix.hpp b/mocks/include/mocks/Helix.hpp new file mode 100644 index 000000000..14ec3976e --- /dev/null +++ b/mocks/include/mocks/Helix.hpp @@ -0,0 +1,427 @@ +#pragma once + +#include "providers/twitch/api/Helix.hpp" +#include "util/CancellationToken.hpp" + +#include +#include +#include + +#include + +namespace chatterino::mock { + +class Helix : public IHelix +{ +public: + virtual ~Helix() = default; + + MOCK_METHOD(void, fetchUsers, + (QStringList userIds, QStringList userLogins, + ResultCallback> successCallback, + HelixFailureCallback failureCallback), + (override)); + + MOCK_METHOD(void, getUserByName, + (QString userName, ResultCallback successCallback, + HelixFailureCallback failureCallback), + (override)); + MOCK_METHOD(void, getUserById, + (QString userId, ResultCallback successCallback, + HelixFailureCallback failureCallback), + (override)); + + MOCK_METHOD( + void, getChannelFollowers, + (QString broadcasterID, + ResultCallback successCallback, + std::function failureCallback), + (override)); + + MOCK_METHOD(void, fetchStreams, + (QStringList userIds, QStringList userLogins, + ResultCallback> successCallback, + HelixFailureCallback failureCallback, + std::function finallyCallback), + (override)); + + MOCK_METHOD(void, getStreamById, + (QString userId, + (ResultCallback successCallback), + HelixFailureCallback failureCallback, + std::function finallyCallback), + (override)); + + MOCK_METHOD(void, getStreamByName, + (QString userName, + (ResultCallback successCallback), + HelixFailureCallback failureCallback, + std::function finallyCallback), + (override)); + + MOCK_METHOD(void, fetchGames, + (QStringList gameIds, QStringList gameNames, + (ResultCallback> successCallback), + HelixFailureCallback failureCallback), + (override)); + + MOCK_METHOD(void, searchGames, + (QString gameName, + ResultCallback> successCallback, + HelixFailureCallback failureCallback), + (override)); + + MOCK_METHOD(void, getGameById, + (QString gameId, ResultCallback successCallback, + HelixFailureCallback failureCallback), + (override)); + + MOCK_METHOD(void, createClip, + (QString channelId, ResultCallback successCallback, + std::function failureCallback, + std::function finallyCallback), + (override)); + + MOCK_METHOD(void, fetchChannels, + (QStringList userIDs, + ResultCallback> successCallback, + HelixFailureCallback failureCallback), + (override)); + + MOCK_METHOD(void, getChannel, + (QString broadcasterId, + ResultCallback successCallback, + HelixFailureCallback failureCallback), + (override)); + + MOCK_METHOD(void, createStreamMarker, + (QString broadcasterId, QString description, + ResultCallback successCallback, + std::function failureCallback), + (override)); + + MOCK_METHOD(void, loadBlocks, + (QString userId, + ResultCallback> successCallback, + FailureCallback failureCallback, + CancellationToken &&token), + (override)); + + MOCK_METHOD(void, blockUser, + (QString targetUserId, const QObject *caller, + std::function successCallback, + HelixFailureCallback failureCallback), + (override)); + + MOCK_METHOD(void, unblockUser, + (QString targetUserId, const QObject *caller, + std::function successCallback, + HelixFailureCallback failureCallback), + (override)); + + MOCK_METHOD( + void, updateChannel, + (QString broadcasterId, QString gameId, QString language, QString title, + std::function successCallback, + (FailureCallback failureCallback)), + (override)); + + MOCK_METHOD(void, manageAutoModMessages, + (QString userID, QString msgID, QString action, + std::function successCallback, + std::function failureCallback), + (override)); + + MOCK_METHOD(void, getCheermotes, + (QString broadcasterId, + ResultCallback> successCallback, + HelixFailureCallback failureCallback), + (override)); + + MOCK_METHOD(void, getEmoteSetData, + (QString emoteSetId, + ResultCallback successCallback, + HelixFailureCallback failureCallback), + (override)); + + MOCK_METHOD(void, getChannelEmotes, + (QString broadcasterId, + ResultCallback> successCallback, + HelixFailureCallback failureCallback), + (override)); + + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD( + void, getGlobalBadges, + (ResultCallback successCallback, + (FailureCallback failureCallback)), + (override)); + + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD(void, getChannelBadges, + (QString broadcasterID, + ResultCallback successCallback, + (FailureCallback + failureCallback)), + (override)); + + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD(void, updateUserChatColor, + (QString userID, QString color, + ResultCallback<> successCallback, + (FailureCallback + failureCallback)), + (override)); + + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD(void, deleteChatMessages, + (QString broadcasterID, QString moderatorID, QString messageID, + ResultCallback<> successCallback, + (FailureCallback + failureCallback)), + (override)); + + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD(void, addChannelModerator, + (QString broadcasterID, QString userID, + ResultCallback<> successCallback, + (FailureCallback + failureCallback)), + (override)); + + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD(void, removeChannelModerator, + (QString broadcasterID, QString userID, + ResultCallback<> successCallback, + (FailureCallback + failureCallback)), + (override)); + + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD(void, sendChatAnnouncement, + (QString broadcasterID, QString moderatorID, QString message, + HelixAnnouncementColor color, ResultCallback<> successCallback, + (FailureCallback + failureCallback)), + (override)); + + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD( + void, addChannelVIP, + (QString broadcasterID, QString userID, + ResultCallback<> successCallback, + (FailureCallback failureCallback)), + (override)); + + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD(void, removeChannelVIP, + (QString broadcasterID, QString userID, + ResultCallback<> successCallback, + (FailureCallback + failureCallback)), + (override)); + + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD( + void, unbanUser, + (QString broadcasterID, QString moderatorID, QString userID, + ResultCallback<> successCallback, + (FailureCallback failureCallback)), + (override)); + + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD( // /raid + void, startRaid, + (QString fromBroadcasterID, QString toBroadcasterId, + ResultCallback<> successCallback, + (FailureCallback failureCallback)), + (override)); // /raid + + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD( // /unraid + void, cancelRaid, + (QString broadcasterID, ResultCallback<> successCallback, + (FailureCallback failureCallback)), + (override)); // /unraid + + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD(void, updateEmoteMode, + (QString broadcasterID, QString moderatorID, bool emoteMode, + ResultCallback successCallback, + (FailureCallback + failureCallback)), + (override)); + + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD(void, updateFollowerMode, + (QString broadcasterID, QString moderatorID, + std::optional followerModeDuration, + ResultCallback successCallback, + (FailureCallback + failureCallback)), + (override)); + + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD(void, updateNonModeratorChatDelay, + (QString broadcasterID, QString moderatorID, + std::optional nonModeratorChatDelayDuration, + ResultCallback successCallback, + (FailureCallback + failureCallback)), + (override)); + + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD(void, updateSlowMode, + (QString broadcasterID, QString moderatorID, + std::optional slowModeWaitTime, + ResultCallback successCallback, + (FailureCallback + failureCallback)), + (override)); + + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD(void, updateSubscriberMode, + (QString broadcasterID, QString moderatorID, + bool subscriberMode, + ResultCallback successCallback, + (FailureCallback + failureCallback)), + (override)); + + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD(void, updateUniqueChatMode, + (QString broadcasterID, QString moderatorID, + bool uniqueChatMode, + ResultCallback successCallback, + (FailureCallback + failureCallback)), + (override)); + // update chat settings + + // /timeout, /ban + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD(void, banUser, + (QString broadcasterID, QString moderatorID, QString userID, + std::optional duration, QString reason, + ResultCallback<> successCallback, + (FailureCallback failureCallback)), + (override)); // /timeout, /ban + + // /warn + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD( + void, warnUser, + (QString broadcasterID, QString moderatorID, QString userID, + QString reason, ResultCallback<> successCallback, + (FailureCallback failureCallback)), + (override)); // /warn + + // /w + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD(void, sendWhisper, + (QString fromUserID, QString toUserID, QString message, + ResultCallback<> successCallback, + (FailureCallback failureCallback)), + (override)); // /w + + // getChatters + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD( + void, getChatters, + (QString broadcasterID, QString moderatorID, int maxChattersToFetch, + ResultCallback successCallback, + (FailureCallback failureCallback)), + (override)); // getChatters + + // /vips + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD( + void, getChannelVIPs, + (QString broadcasterID, + ResultCallback> successCallback, + (FailureCallback failureCallback)), + (override)); // /vips + + // /commercial + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD( + void, startCommercial, + (QString broadcasterID, int length, + ResultCallback successCallback, + (FailureCallback failureCallback)), + (override)); // /commercial + + // /mods + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD( + void, getModerators, + (QString broadcasterID, int maxModeratorsToFetch, + ResultCallback> successCallback, + (FailureCallback failureCallback)), + (override)); // /mods + + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD(void, updateShieldMode, + (QString broadcasterID, QString moderatorID, bool isActive, + ResultCallback successCallback, + (FailureCallback + failureCallback)), + (override)); + + // /shoutout + MOCK_METHOD( + void, sendShoutout, + (QString fromBroadcasterID, QString toBroadcasterID, + QString moderatorID, ResultCallback<> successCallback, + (FailureCallback failureCallback)), + (override)); + + // send message + MOCK_METHOD( + void, sendChatMessage, + (HelixSendMessageArgs args, + ResultCallback successCallback, + (FailureCallback failureCallback)), + (override)); + + MOCK_METHOD(void, update, (QString clientId, QString oauthToken), + (override)); + +protected: + // The extra parenthesis around the failure callback is because its type + // contains a comma + MOCK_METHOD(void, updateChatSettings, + (QString broadcasterID, QString moderatorID, QJsonObject json, + ResultCallback successCallback, + (FailureCallback + failureCallback)), + (override)); +}; + +} // namespace chatterino::mock diff --git a/mocks/include/mocks/LinkResolver.hpp b/mocks/include/mocks/LinkResolver.hpp new file mode 100644 index 000000000..8a5682a3d --- /dev/null +++ b/mocks/include/mocks/LinkResolver.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include "providers/links/LinkResolver.hpp" + +#include +#include +#include + +namespace chatterino::mock { + +class LinkResolver : public ILinkResolver +{ +public: + LinkResolver() = default; + ~LinkResolver() override = default; + + MOCK_METHOD(void, resolve, (LinkInfo * info), (override)); +}; + +class EmptyLinkResolver : public ILinkResolver +{ +public: + EmptyLinkResolver() = default; + ~EmptyLinkResolver() override = default; + + void resolve(LinkInfo *info) override + { + // + } +}; + +} // namespace chatterino::mock diff --git a/mocks/include/mocks/Logging.hpp b/mocks/include/mocks/Logging.hpp new file mode 100644 index 000000000..0f4dba38a --- /dev/null +++ b/mocks/include/mocks/Logging.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include "singletons/Logging.hpp" + +#include +#include +#include + +namespace chatterino::mock { + +class Logging : public ILogging +{ +public: + Logging() = default; + ~Logging() override = default; + + MOCK_METHOD(void, addMessage, + (const QString &channelName, MessagePtr message, + const QString &platformName, const QString &streamID), + (override)); +}; + +class EmptyLogging : public ILogging +{ +public: + EmptyLogging() = default; + ~EmptyLogging() override = default; + + void addMessage(const QString &channelName, MessagePtr message, + const QString &platformName, + const QString &streamID) override + { + // + } +}; + +} // namespace chatterino::mock diff --git a/mocks/include/mocks/TwitchIrcServer.hpp b/mocks/include/mocks/TwitchIrcServer.hpp new file mode 100644 index 000000000..bbeba8ca4 --- /dev/null +++ b/mocks/include/mocks/TwitchIrcServer.hpp @@ -0,0 +1,128 @@ +#pragma once + +#include "mocks/Channel.hpp" +#include "providers/bttv/BttvEmotes.hpp" +#include "providers/ffz/FfzEmotes.hpp" +#include "providers/seventv/eventapi/Client.hpp" +#include "providers/seventv/eventapi/Dispatch.hpp" +#include "providers/seventv/eventapi/Message.hpp" +#include "providers/seventv/SeventvEmotes.hpp" +#include "providers/twitch/TwitchIrcServer.hpp" + +namespace chatterino::mock { + +class MockTwitchIrcServer : public ITwitchIrcServer +{ +public: + MockTwitchIrcServer() + : watchingChannelInner( + std::shared_ptr(new MockChannel("testaccount_420"))) + , watchingChannel(this->watchingChannelInner, + Channel::Type::TwitchWatching) + , whispersChannel(std::shared_ptr(new MockChannel("whispers"))) + , mentionsChannel(std::shared_ptr(new MockChannel("forsen3"))) + , liveChannel(std::shared_ptr(new MockChannel("forsen"))) + , automodChannel(std::shared_ptr(new MockChannel("forsen2"))) + { + } + + void connect() override + { + } + + void sendRawMessage(const QString &rawMessage) override + { + } + + ChannelPtr getOrAddChannel(const QString &dirtyChannelName) override + { + assert(false && "unimplemented getOrAddChannel in mock irc server"); + return {}; + } + + ChannelPtr getChannelOrEmpty(const QString &dirtyChannelName) override + { + assert(false && "unimplemented getChannelOrEmpty in mock irc server"); + return {}; + } + + void addFakeMessage(const QString &data) override + { + } + + void addGlobalSystemMessage(const QString &messageText) override + { + } + + void forEachChannel(std::function func) override + { + } + + void forEachChannelAndSpecialChannels( + std::function func) override + { + // + } + + std::shared_ptr getChannelOrEmptyByID( + const QString &channelID) override + { + return {}; + } + + void dropSeventvChannel(const QString &userID, + const QString &emoteSetID) override + { + // + } + + const IndirectChannel &getWatchingChannel() const override + { + return this->watchingChannel; + } + + void setWatchingChannel(ChannelPtr newWatchingChannel) override + { + this->watchingChannel.reset(newWatchingChannel); + } + + QString getLastUserThatWhisperedMe() const override + { + return this->lastUserThatWhisperedMe; + } + + void setLastUserThatWhisperedMe(const QString &user) override + { + this->lastUserThatWhisperedMe = user; + } + + ChannelPtr getWhispersChannel() const override + { + return this->whispersChannel; + } + + ChannelPtr getMentionsChannel() const override + { + return this->mentionsChannel; + } + + ChannelPtr getLiveChannel() const override + { + return this->liveChannel; + } + + ChannelPtr getAutomodChannel() const override + { + return this->automodChannel; + } + + ChannelPtr watchingChannelInner; + IndirectChannel watchingChannel; + ChannelPtr whispersChannel; + ChannelPtr mentionsChannel; + ChannelPtr liveChannel; + ChannelPtr automodChannel; + QString lastUserThatWhisperedMe{"forsen"}; +}; + +} // namespace chatterino::mock diff --git a/mocks/include/mocks/UserData.hpp b/mocks/include/mocks/UserData.hpp new file mode 100644 index 000000000..62159a19f --- /dev/null +++ b/mocks/include/mocks/UserData.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include "controllers/userdata/UserDataController.hpp" + +namespace chatterino::mock { + +class UserDataController : public IUserDataController +{ +public: + UserDataController() = default; + + // Get extra data about a user + // If the user does not have any extra data, return none + std::optional getUser(const QString &userID) const override + { + return std::nullopt; + } + + // Update or insert extra data for the user's color override + void setUserColor(const QString &userID, + const QString &colorString) override + { + // do nothing + } +}; + +} // namespace chatterino::mock diff --git a/resources/_generate_resources.py b/resources/_generate_resources.py deleted file mode 100644 index 7f1ee7a53..000000000 --- a/resources/_generate_resources.py +++ /dev/null @@ -1,39 +0,0 @@ -resources_header = \ -''' - ''' - -resources_footer = \ -''' -\n''' - -header_header = \ -'''#include -#include "common/Singleton.hpp" - -namespace chatterino { - -class Resources2 : public Singleton -{ -public: - Resources2(); - -''' - -header_footer = \ -'''}; - -} // namespace chatterino\n''' - -source_header = \ -'''#include "ResourcesAutogen.hpp" - -namespace chatterino { - -Resources2::Resources2() -{ -''' - -source_footer = \ -'''} - -} // namespace chatterino\n''' diff --git a/resources/avatars/_1xelerate.png b/resources/avatars/_1xelerate.png index 94843f790..17d5a344b 100644 Binary files a/resources/avatars/_1xelerate.png and b/resources/avatars/_1xelerate.png differ diff --git a/resources/avatars/alazymeme.png b/resources/avatars/alazymeme.png index 66ac412bb..190801cb7 100644 Binary files a/resources/avatars/alazymeme.png and b/resources/avatars/alazymeme.png differ diff --git a/resources/avatars/anon.png b/resources/avatars/anon.png new file mode 100644 index 000000000..b7993edcb Binary files /dev/null and b/resources/avatars/anon.png differ diff --git a/resources/avatars/brian6932.png b/resources/avatars/brian6932.png index 37e1cafe9..de9ffa491 100644 Binary files a/resources/avatars/brian6932.png and b/resources/avatars/brian6932.png differ diff --git a/resources/avatars/crazysmc.png b/resources/avatars/crazysmc.png new file mode 100644 index 000000000..f84ce14d7 Binary files /dev/null and b/resources/avatars/crazysmc.png differ diff --git a/resources/avatars/cyclone.png b/resources/avatars/cyclone.png new file mode 100644 index 000000000..6f9f9f90b Binary files /dev/null and b/resources/avatars/cyclone.png differ diff --git a/resources/avatars/explooosion_code.png b/resources/avatars/explooosion_code.png new file mode 100644 index 000000000..851d90b46 Binary files /dev/null and b/resources/avatars/explooosion_code.png differ diff --git a/resources/avatars/fourtf.png b/resources/avatars/fourtf.png index 7749458d9..43c557979 100644 Binary files a/resources/avatars/fourtf.png and b/resources/avatars/fourtf.png differ diff --git a/resources/avatars/fraxx.png b/resources/avatars/fraxx.png new file mode 100644 index 000000000..5eb3f2231 Binary files /dev/null and b/resources/avatars/fraxx.png differ diff --git a/resources/avatars/hicupalot.png b/resources/avatars/hicupalot.png index cf42f4371..425c5633a 100644 Binary files a/resources/avatars/hicupalot.png and b/resources/avatars/hicupalot.png differ diff --git a/resources/avatars/iprodigy.png b/resources/avatars/iprodigy.png index 481df599e..2fbaf3245 100644 Binary files a/resources/avatars/iprodigy.png and b/resources/avatars/iprodigy.png differ diff --git a/resources/avatars/jakeryw.png b/resources/avatars/jakeryw.png new file mode 100644 index 000000000..fd256af64 Binary files /dev/null and b/resources/avatars/jakeryw.png differ diff --git a/resources/avatars/jaxkey.png b/resources/avatars/jaxkey.png index 412ee2120..5c30fb79b 100644 Binary files a/resources/avatars/jaxkey.png and b/resources/avatars/jaxkey.png differ diff --git a/resources/avatars/kararty.png b/resources/avatars/kararty.png index dd0e1dcd1..3e937ee93 100644 Binary files a/resources/avatars/kararty.png and b/resources/avatars/kararty.png differ diff --git a/resources/avatars/karlpolice.png b/resources/avatars/karlpolice.png index 316fd6ae9..f86a3a0bf 100644 Binary files a/resources/avatars/karlpolice.png and b/resources/avatars/karlpolice.png differ diff --git a/resources/avatars/mm2pl.png b/resources/avatars/mm2pl.png index 97cb8b96b..9155d476d 100644 Binary files a/resources/avatars/mm2pl.png and b/resources/avatars/mm2pl.png differ diff --git a/resources/avatars/mohad12211.png b/resources/avatars/mohad12211.png new file mode 100644 index 000000000..54c6aece4 Binary files /dev/null and b/resources/avatars/mohad12211.png differ diff --git a/resources/avatars/nealxm.png b/resources/avatars/nealxm.png new file mode 100644 index 000000000..fcba49189 Binary files /dev/null and b/resources/avatars/nealxm.png differ diff --git a/resources/avatars/niller2005.png b/resources/avatars/niller2005.png new file mode 100644 index 000000000..1a2b5bab1 Binary files /dev/null and b/resources/avatars/niller2005.png differ diff --git a/resources/avatars/pajlada.png b/resources/avatars/pajlada.png index dd3c88ce3..e8ff6160d 100644 Binary files a/resources/avatars/pajlada.png and b/resources/avatars/pajlada.png differ diff --git a/resources/avatars/slch.png b/resources/avatars/slch.png index f5264458d..64c56e662 100644 Binary files a/resources/avatars/slch.png and b/resources/avatars/slch.png differ diff --git a/resources/avatars/techno.png b/resources/avatars/techno.png new file mode 100644 index 000000000..6f5913dc4 Binary files /dev/null and b/resources/avatars/techno.png differ diff --git a/resources/avatars/wissididom.png b/resources/avatars/wissididom.png new file mode 100644 index 000000000..26b3b67e7 Binary files /dev/null and b/resources/avatars/wissididom.png differ diff --git a/resources/avatars/xheaveny.png b/resources/avatars/xheaveny.png index 0977b6402..19c8f361e 100644 Binary files a/resources/avatars/xheaveny.png and b/resources/avatars/xheaveny.png differ diff --git a/resources/avatars/zneix.png b/resources/avatars/zneix.png index 814cd573d..5823ee988 100644 Binary files a/resources/avatars/zneix.png and b/resources/avatars/zneix.png differ diff --git a/resources/avatars/zonianmidian.png b/resources/avatars/zonianmidian.png new file mode 100644 index 000000000..1b9568292 Binary files /dev/null and b/resources/avatars/zonianmidian.png differ diff --git a/resources/buttons/addSplit.png b/resources/buttons/addSplit.png index 54714d573..13a8891d2 100644 Binary files a/resources/buttons/addSplit.png and b/resources/buttons/addSplit.png differ diff --git a/resources/buttons/addSplitDark.png b/resources/buttons/addSplitDark.png index c6cd6707a..814aa9f16 100644 Binary files a/resources/buttons/addSplitDark.png and b/resources/buttons/addSplitDark.png differ diff --git a/resources/buttons/ban.png b/resources/buttons/ban.png index a983d1c87..e128454af 100644 Binary files a/resources/buttons/ban.png and b/resources/buttons/ban.png differ diff --git a/resources/buttons/banRed.png b/resources/buttons/banRed.png deleted file mode 100644 index af6b1ac6a..000000000 Binary files a/resources/buttons/banRed.png and /dev/null differ diff --git a/resources/buttons/cancel.svg b/resources/buttons/cancel.svg index 7bc012dae..9a353121a 100644 --- a/resources/buttons/cancel.svg +++ b/resources/buttons/cancel.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/resources/buttons/cancelDark.svg b/resources/buttons/cancelDark.svg index 5c231ffdb..4e8e3ce33 100644 --- a/resources/buttons/cancelDark.svg +++ b/resources/buttons/cancelDark.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/resources/buttons/chattersDark.png b/resources/buttons/chattersDark.png new file mode 100644 index 000000000..5cf95d931 Binary files /dev/null and b/resources/buttons/chattersDark.png differ diff --git a/resources/buttons/chattersLight.png b/resources/buttons/chattersLight.png new file mode 100644 index 000000000..9bd209f35 Binary files /dev/null and b/resources/buttons/chattersLight.png differ diff --git a/resources/buttons/clearSearch.png b/resources/buttons/clearSearch.png index 881a0dd7b..b607322f9 100644 Binary files a/resources/buttons/clearSearch.png and b/resources/buttons/clearSearch.png differ diff --git a/resources/buttons/copyDark.png b/resources/buttons/copyDark.png index a0b633eec..9e87f15de 100644 Binary files a/resources/buttons/copyDark.png and b/resources/buttons/copyDark.png differ diff --git a/resources/buttons/copyLight.png b/resources/buttons/copyLight.png index 2a663bfd6..0e0de7804 100644 Binary files a/resources/buttons/copyLight.png and b/resources/buttons/copyLight.png differ diff --git a/resources/buttons/emote.svg b/resources/buttons/emote.svg index 10e25c9f5..feabe9771 100644 --- a/resources/buttons/emote.svg +++ b/resources/buttons/emote.svg @@ -1,12 +1 @@ - - - - - - + \ No newline at end of file diff --git a/resources/buttons/emoteDark.svg b/resources/buttons/emoteDark.svg index 10f2ad9bd..317b36517 100644 --- a/resources/buttons/emoteDark.svg +++ b/resources/buttons/emoteDark.svg @@ -1,12 +1 @@ - - - - - - + \ No newline at end of file diff --git a/resources/buttons/menuDark.png b/resources/buttons/menuDark.png index 307fbb9d5..bca55f262 100644 Binary files a/resources/buttons/menuDark.png and b/resources/buttons/menuDark.png differ diff --git a/resources/buttons/mod.png b/resources/buttons/mod.png index 6d92034b7..1cabf1197 100644 Binary files a/resources/buttons/mod.png and b/resources/buttons/mod.png differ diff --git a/resources/buttons/modModeDisabled.png b/resources/buttons/modModeDisabled.png index 9b53282b0..2ebbf44e6 100644 Binary files a/resources/buttons/modModeDisabled.png and b/resources/buttons/modModeDisabled.png differ diff --git a/resources/buttons/modModeDisabled2.png b/resources/buttons/modModeDisabled2.png deleted file mode 100644 index b0a063072..000000000 Binary files a/resources/buttons/modModeDisabled2.png and /dev/null differ diff --git a/resources/buttons/modModeEnabled.png b/resources/buttons/modModeEnabled.png index 0ea511188..f7bfec4b9 100644 Binary files a/resources/buttons/modModeEnabled.png and b/resources/buttons/modModeEnabled.png differ diff --git a/resources/buttons/modModeEnabled2.png b/resources/buttons/modModeEnabled2.png deleted file mode 100644 index a563c4ba4..000000000 Binary files a/resources/buttons/modModeEnabled2.png and /dev/null differ diff --git a/resources/buttons/pinDisabledDark.png b/resources/buttons/pinDisabledDark.png new file mode 100644 index 000000000..87e912c5d Binary files /dev/null and b/resources/buttons/pinDisabledDark.png differ diff --git a/resources/buttons/pinDisabledLight.png b/resources/buttons/pinDisabledLight.png new file mode 100644 index 000000000..5eeac17b2 Binary files /dev/null and b/resources/buttons/pinDisabledLight.png differ diff --git a/resources/buttons/pinEnabled.png b/resources/buttons/pinEnabled.png new file mode 100644 index 000000000..2fe5944d4 Binary files /dev/null and b/resources/buttons/pinEnabled.png differ diff --git a/resources/buttons/replyDark.png b/resources/buttons/replyDark.png index 0b47b7b92..1b6ef884e 100644 Binary files a/resources/buttons/replyDark.png and b/resources/buttons/replyDark.png differ diff --git a/resources/buttons/replyThreadDark.png b/resources/buttons/replyThreadDark.png index 3d6c0b437..131691949 100644 Binary files a/resources/buttons/replyThreadDark.png and b/resources/buttons/replyThreadDark.png differ diff --git a/resources/buttons/search.png b/resources/buttons/search.png deleted file mode 100644 index 8671ce644..000000000 Binary files a/resources/buttons/search.png and /dev/null differ diff --git a/resources/buttons/streamerModeEnabledDark.png b/resources/buttons/streamerModeEnabledDark.png new file mode 100644 index 000000000..43bd6d5b2 Binary files /dev/null and b/resources/buttons/streamerModeEnabledDark.png differ diff --git a/resources/buttons/streamerModeEnabledLight.png b/resources/buttons/streamerModeEnabledLight.png new file mode 100644 index 000000000..a56aa63e0 Binary files /dev/null and b/resources/buttons/streamerModeEnabledLight.png differ diff --git a/resources/buttons/timeout.png b/resources/buttons/timeout.png deleted file mode 100644 index a087ed123..000000000 Binary files a/resources/buttons/timeout.png and /dev/null differ diff --git a/resources/buttons/trashCan.png b/resources/buttons/trashCan.png index fd1bce3a0..3b23ae0ef 100644 Binary files a/resources/buttons/trashCan.png and b/resources/buttons/trashCan.png differ diff --git a/resources/buttons/unban.png b/resources/buttons/unban.png index 6ef6453e6..66a6da01d 100644 Binary files a/resources/buttons/unban.png and b/resources/buttons/unban.png differ diff --git a/resources/buttons/unmod.png b/resources/buttons/unmod.png index a45ee6f26..0e5d15766 100644 Binary files a/resources/buttons/unmod.png and b/resources/buttons/unmod.png differ diff --git a/resources/buttons/unvip.png b/resources/buttons/unvip.png index 61b3476d9..f53e097f5 100644 Binary files a/resources/buttons/unvip.png and b/resources/buttons/unvip.png differ diff --git a/resources/buttons/update.png b/resources/buttons/update.png index eca3c5044..f808cc097 100644 Binary files a/resources/buttons/update.png and b/resources/buttons/update.png differ diff --git a/resources/buttons/updateError.png b/resources/buttons/updateError.png index a1e30e22c..5d25444ca 100644 Binary files a/resources/buttons/updateError.png and b/resources/buttons/updateError.png differ diff --git a/resources/buttons/viewersDark.png b/resources/buttons/viewersDark.png deleted file mode 100644 index 399b942e2..000000000 Binary files a/resources/buttons/viewersDark.png and /dev/null differ diff --git a/resources/buttons/viewersLight.png b/resources/buttons/viewersLight.png deleted file mode 100644 index a55aae5c4..000000000 Binary files a/resources/buttons/viewersLight.png and /dev/null differ diff --git a/resources/buttons/vip.png b/resources/buttons/vip.png index 43b18b1e6..0ce707c96 100644 Binary files a/resources/buttons/vip.png and b/resources/buttons/vip.png differ diff --git a/resources/chatterino.icns b/resources/chatterino.icns index 8b6482ee7..dd41e63e5 100644 Binary files a/resources/chatterino.icns and b/resources/chatterino.icns differ diff --git a/resources/com.chatterino.chatterino.appdata.xml b/resources/com.chatterino.chatterino.appdata.xml index 9641a0843..a2d09fecb 100644 --- a/resources/com.chatterino.chatterino.appdata.xml +++ b/resources/com.chatterino.chatterino.appdata.xml @@ -2,12 +2,14 @@ com.chatterino.chatterino.desktop + com.chatterino.chatterino.desktop CC0-1.0 MIT intense Chatterino + Chatterino Developers Chat client for twitch.tv @@ -18,7 +20,7 @@ - https://i.imgur.com/CFLDARZ.png + https://chatterino.com/img/example_01.png @@ -32,6 +34,29 @@ chatterino - + + https://github.com/Chatterino/chatterino2/releases/tag/v2.5.1 + + + https://github.com/Chatterino/chatterino2/releases/tag/v2.5.0 + + + https://github.com/Chatterino/chatterino2/releases/tag/v2.5.0-beta.1 + + + https://github.com/Chatterino/chatterino2/releases/tag/v2.4.6 + + + https://github.com/Chatterino/chatterino2/releases/tag/v2.4.5 + + + https://github.com/Chatterino/chatterino2/releases/tag/v2.4.4 + + + https://github.com/Chatterino/chatterino2/releases/tag/v2.4.3 + + + https://github.com/Chatterino/chatterino2/releases/tag/v2.4.2 + diff --git a/resources/com.chatterino.chatterino.desktop b/resources/com.chatterino.chatterino.desktop index 6ee453543..ece81f5c8 100644 --- a/resources/com.chatterino.chatterino.desktop +++ b/resources/com.chatterino.chatterino.desktop @@ -4,7 +4,7 @@ Version=1.0 Name=Chatterino Comment=Chat client for Twitch Exec=chatterino -Icon=chatterino +Icon=com.chatterino.chatterino Terminal=false Categories=Network;InstantMessaging; StartupWMClass=chatterino diff --git a/resources/contributors.txt b/resources/contributors.txt index 3148d4252..6267887e3 100644 --- a/resources/contributors.txt +++ b/resources/contributors.txt @@ -3,63 +3,92 @@ # TODO: Parse this into a CONTRIBUTORS.md too # Adding yourself? Copy and paste this template at the bottom of this file and fill in the fields in a PR! -# Name | Link | Avatar (Loaded as a resource) | Title (description of work done). +# Name | Link | Avatar (Loaded as a resource, avatars are not required). # Avatar should be located in avatars/ directory. Its size should be 128x128 (get it from https://github.com/username.png?size=128). -# Make sure to reduce avatar's size as much as possible with tool like pngcrush or optipng (if the file is in png format) and run ./generate_resources.py +# Make sure to reduce avatar's size as much as possible with tool like pngcrush or optipng (if the file is in png format). # Contributor is what we use for someone who has contributed in general (like sent a programming-related PR). -fourtf | https://fourtf.com | :/avatars/fourtf.png | Author, main developer -pajlada | https://pajlada.se | :/avatars/pajlada.png | Collaborator, co-developer -zneix | https://github.com/zneix | :/avatars/zneix.png | Collaborator +@header Maintainers -Cranken | https://github.com/Cranken | | Contributor -hemirt | https://github.com/hemirt | | Contributor -LajamerrMittesdine | https://github.com/LajamerrMittesdine | | Contributor -coral | https://github.com/coral | | Contributor, design -apa420 | https://github.com/apa420 | | Contributor -DatGuy1 | https://github.com/DatGuy1 | | Contributor -Confuseh | https://github.com/Confuseh | | Contributor -ch-ems | https://github.com/ch-ems | | Contributor -Bur0k | https://github.com/Bur0k | | Contributor -nuuls | https://github.com/nuuls | | Contributor -Chronophylos | https://github.com/Chronophylos | | Contributor -Ckath | https://github.com/Ckath | | Contributor -matijakevic | https://github.com/matijakevic | | Contributor -nforro | https://github.com/nforro | | Contributor -vanolpfan | https://github.com/vanolpfan | | Contributor -23rd | https://github.com/23rd | | Contributor -machgo | https://github.com/machgo | | Contributor -TranRed | https://github.com/TranRed | | Contributor -RAnders00 | https://github.com/RAnders00 | | Contributor -YungLPR | https://github.com/leon-richardt | | Contributor -Mm2PL | https://github.com/mm2pl | :/avatars/mm2pl.png | Contributor -gempir | https://github.com/gempir | | Contributor -mfmarlow | https://github.com/mfmarlow | | Contributor -dnsge | https://github.com/dnsge | | Contributor -y0dax | https://github.com/y0dax | | Contributor -Iulian Onofrei | https://github.com/revolter | :/avatars/revolter.jpg | Contributor -matthewde | https://github.com/m4tthewde | :/avatars/matthewde.jpg | Contributor -Karar Al-Remahy | https://github.com/KararTY | :/avatars/kararty.png | Contributor -Talen | https://github.com/talneoran | | Contributor -SLCH | https://github.com/SLCH | :/avatars/slch.png | Contributor -ALazyMeme | https://github.com/alazymeme | :/avatars/alazymeme.png | Contributor -xHeaveny_ | https://github.com/xHeaveny | :/avatars/xheaveny.png | Contributor -1xelerate | https://github.com/1xelerate | :/avatars/_1xelerate.png | Contributor -acdvs | https://github.com/acdvs | | Contributor -karl-police | https://github.com/karl-police | :/avatars/karlpolice.png | Contributor -brian6932 | https://github.com/brian6932 | :/avatars/brian6932.png | Contributor -hicupalot | https://github.com/hicupalot | :/avatars/hicupalot.png | Contributor -iProdigy | https://github.com/iProdigy | :/avatars/iprodigy.png | Contributor -Jaxkey | https://github.com/Jaxkey | :/avatars/jaxkey.png | Contributor +fourtf | https://fourtf.com | :/avatars/fourtf.png +pajlada | https://pajlada.se | :/avatars/pajlada.png + +@header Collaborators + +zneix | https://github.com/zneix | :/avatars/zneix.png +Mm2PL | https://github.com/mm2pl | :/avatars/mm2pl.png +YungLPR | https://github.com/leon-richardt | +dnsge | https://github.com/dnsge | +Felanbird | https://github.com/Felanbird | +kornes | https://github.com/kornes | + +@header Contributors + +Cranken | https://github.com/Cranken | +hemirt | https://github.com/hemirt | +LajamerrMittesdine | https://github.com/LajamerrMittesdine | +coral | https://github.com/coral | +apa420 | https://github.com/apa420 | +DatGuy1 | https://github.com/DatGuy1 | +Confuseh | https://github.com/Confuseh | +ch-ems | https://github.com/ch-ems | +Bur0k | https://github.com/Bur0k | +nuuls | https://github.com/nuuls | +Chronophylos | https://github.com/Chronophylos | +Ckath | https://github.com/Ckath | +matijakevic | https://github.com/matijakevic | +nforro | https://github.com/nforro | +vanolpfan | https://github.com/vanolpfan | +23rd | https://github.com/23rd | +machgo | https://github.com/machgo | +TranRed | https://github.com/TranRed | +RAnders00 | https://github.com/RAnders00 | +gempir | https://github.com/gempir | +mfmarlow | https://github.com/mfmarlow | +y0dax | https://github.com/y0dax | +Iulian Onofrei | https://github.com/revolter | :/avatars/revolter.jpg +matthewde | https://github.com/m4tthewde | :/avatars/matthewde.jpg +Karar Al-Remahy | https://github.com/KararTY | :/avatars/kararty.png +Talen | https://github.com/talneoran | +SLCH | https://github.com/SLCH | :/avatars/slch.png +ALazyMeme | https://github.com/alazymeme | :/avatars/alazymeme.png +xHeaveny_ | https://github.com/xHeaveny | :/avatars/xheaveny.png +1xelerate | https://github.com/xel86 | :/avatars/_1xelerate.png +acdvs | https://github.com/acdvs | +karl-police | https://github.com/karl-police | :/avatars/karlpolice.png +brian6932 | https://github.com/brian6932 | :/avatars/brian6932.png +hicupalot | https://github.com/hicupalot | :/avatars/hicupalot.png +iProdigy | https://github.com/iProdigy | :/avatars/iprodigy.png +Jaxkey | https://github.com/Jaxkey | :/avatars/jaxkey.png +Explooosion | https://github.com/Explooosion-code | :/avatars/explooosion_code.png +mohad12211 | https://github.com/mohad12211 | :/avatars/mohad12211.png +Wissididom | https://github.com/Wissididom | :/avatars/wissididom.png +03y | https://github.com/03y | +ScrubN | https://github.com/ScrubN | +Cyclone | https://github.com/PsycloneTM | :/avatars/cyclone.png +2547techno | https://github.com/2547techno | :/avatars/techno.png +ZonianMidian | https://github.com/ZonianMidian | :/avatars/zonianmidian.png +olafyang | https://github.com/olafyang | +chrrs | https://github.com/chrrs | +4rneee | https://github.com/4rneee | +crazysmc | https://github.com/crazysmc | :/avatars/crazysmc.png +SputNikPlop | https://github.com/SputNikPlop | +fraxx | https://github.com/fraxxio | :/avatars/fraxx.png +KleberPF | https://github.com/KleberPF | +nealxm | https://github.com/nealxm | :/avatars/nealxm.png +Niller2005 | https://github.com/Niller2005 | :/avatars/niller2005.png +JakeRYW | https://github.com/JakeRYW | :/avatars/jakeryw.png # If you are a contributor add yourself above this line -Defman21 | https://github.com/Defman21 | | Documentation -vilgotf | https://github.com/vilgotf | | Documentation -Ian321 | https://github.com/Ian321 | | Documentation -Yardanico | https://github.com/Yardanico | | Documentation -huti26 | https://github.com/huti26 | | Documentation -chrisduerr | https://github.com/chrisduerr | | Documentation +@header Documentation + +Defman21 | https://github.com/Defman21 | +vilgotf | https://github.com/vilgotf | +Ian321 | https://github.com/Ian321 | +Yardanico | https://github.com/Yardanico | +huti26 | https://github.com/huti26 | +chrisduerr | https://github.com/chrisduerr | # Otherwise add yourself right above this one diff --git a/resources/emoji.json b/resources/emoji.json index 8379b5b6c..cab15c805 100644 --- a/resources/emoji.json +++ b/resources/emoji.json @@ -1 +1 @@ -[{"name":"HASH KEY","unified":"0023-FE0F-20E3","non_qualified":"0023-20E3","docomo":"E6E0","au":"EB84","softbank":"E210","google":"FE82C","image":"0023-fe0f-20e3.png","sheet_x":0,"sheet_y":0,"short_name":"hash","short_names":["hash"],"text":null,"texts":null,"category":"Symbols","subcategory":"keycap","sort_order":1500,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"KEYCAP: *","unified":"002A-FE0F-20E3","non_qualified":"002A-20E3","docomo":null,"au":null,"softbank":null,"google":null,"image":"002a-fe0f-20e3.png","sheet_x":0,"sheet_y":1,"short_name":"keycap_star","short_names":["keycap_star"],"text":null,"texts":null,"category":"Symbols","subcategory":"keycap","sort_order":1501,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"KEYCAP 0","unified":"0030-FE0F-20E3","non_qualified":"0030-20E3","docomo":"E6EB","au":"E5AC","softbank":"E225","google":"FE837","image":"0030-fe0f-20e3.png","sheet_x":0,"sheet_y":2,"short_name":"zero","short_names":["zero"],"text":null,"texts":null,"category":"Symbols","subcategory":"keycap","sort_order":1502,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"KEYCAP 1","unified":"0031-FE0F-20E3","non_qualified":"0031-20E3","docomo":"E6E2","au":"E522","softbank":"E21C","google":"FE82E","image":"0031-fe0f-20e3.png","sheet_x":0,"sheet_y":3,"short_name":"one","short_names":["one"],"text":null,"texts":null,"category":"Symbols","subcategory":"keycap","sort_order":1503,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"KEYCAP 2","unified":"0032-FE0F-20E3","non_qualified":"0032-20E3","docomo":"E6E3","au":"E523","softbank":"E21D","google":"FE82F","image":"0032-fe0f-20e3.png","sheet_x":0,"sheet_y":4,"short_name":"two","short_names":["two"],"text":null,"texts":null,"category":"Symbols","subcategory":"keycap","sort_order":1504,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"KEYCAP 3","unified":"0033-FE0F-20E3","non_qualified":"0033-20E3","docomo":"E6E4","au":"E524","softbank":"E21E","google":"FE830","image":"0033-fe0f-20e3.png","sheet_x":0,"sheet_y":5,"short_name":"three","short_names":["three"],"text":null,"texts":null,"category":"Symbols","subcategory":"keycap","sort_order":1505,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"KEYCAP 4","unified":"0034-FE0F-20E3","non_qualified":"0034-20E3","docomo":"E6E5","au":"E525","softbank":"E21F","google":"FE831","image":"0034-fe0f-20e3.png","sheet_x":0,"sheet_y":6,"short_name":"four","short_names":["four"],"text":null,"texts":null,"category":"Symbols","subcategory":"keycap","sort_order":1506,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"KEYCAP 5","unified":"0035-FE0F-20E3","non_qualified":"0035-20E3","docomo":"E6E6","au":"E526","softbank":"E220","google":"FE832","image":"0035-fe0f-20e3.png","sheet_x":0,"sheet_y":7,"short_name":"five","short_names":["five"],"text":null,"texts":null,"category":"Symbols","subcategory":"keycap","sort_order":1507,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"KEYCAP 6","unified":"0036-FE0F-20E3","non_qualified":"0036-20E3","docomo":"E6E7","au":"E527","softbank":"E221","google":"FE833","image":"0036-fe0f-20e3.png","sheet_x":0,"sheet_y":8,"short_name":"six","short_names":["six"],"text":null,"texts":null,"category":"Symbols","subcategory":"keycap","sort_order":1508,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"KEYCAP 7","unified":"0037-FE0F-20E3","non_qualified":"0037-20E3","docomo":"E6E8","au":"E528","softbank":"E222","google":"FE834","image":"0037-fe0f-20e3.png","sheet_x":0,"sheet_y":9,"short_name":"seven","short_names":["seven"],"text":null,"texts":null,"category":"Symbols","subcategory":"keycap","sort_order":1509,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"KEYCAP 8","unified":"0038-FE0F-20E3","non_qualified":"0038-20E3","docomo":"E6E9","au":"E529","softbank":"E223","google":"FE835","image":"0038-fe0f-20e3.png","sheet_x":0,"sheet_y":10,"short_name":"eight","short_names":["eight"],"text":null,"texts":null,"category":"Symbols","subcategory":"keycap","sort_order":1510,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"KEYCAP 9","unified":"0039-FE0F-20E3","non_qualified":"0039-20E3","docomo":"E6EA","au":"E52A","softbank":"E224","google":"FE836","image":"0039-fe0f-20e3.png","sheet_x":0,"sheet_y":11,"short_name":"nine","short_names":["nine"],"text":null,"texts":null,"category":"Symbols","subcategory":"keycap","sort_order":1511,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"COPYRIGHT SIGN","unified":"00A9-FE0F","non_qualified":"00A9","docomo":"E731","au":"E558","softbank":"E24E","google":"FEB29","image":"00a9-fe0f.png","sheet_x":0,"sheet_y":12,"short_name":"copyright","short_names":["copyright"],"text":null,"texts":null,"category":"Symbols","subcategory":"other-symbol","sort_order":1497,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"REGISTERED SIGN","unified":"00AE-FE0F","non_qualified":"00AE","docomo":"E736","au":"E559","softbank":"E24F","google":"FEB2D","image":"00ae-fe0f.png","sheet_x":0,"sheet_y":13,"short_name":"registered","short_names":["registered"],"text":null,"texts":null,"category":"Symbols","subcategory":"other-symbol","sort_order":1498,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"MAHJONG TILE RED DRAGON","unified":"1F004","non_qualified":null,"docomo":null,"au":"E5D1","softbank":"E12D","google":"FE80B","image":"1f004.png","sheet_x":0,"sheet_y":14,"short_name":"mahjong","short_names":["mahjong"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1101,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PLAYING CARD BLACK JOKER","unified":"1F0CF","non_qualified":null,"docomo":null,"au":"EB6F","softbank":null,"google":"FE812","image":"1f0cf.png","sheet_x":0,"sheet_y":15,"short_name":"black_joker","short_names":["black_joker"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1100,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NEGATIVE SQUARED LATIN CAPITAL LETTER A","unified":"1F170-FE0F","non_qualified":"1F170","docomo":null,"au":"EB26","softbank":"E532","google":"FE50B","image":"1f170-fe0f.png","sheet_x":0,"sheet_y":16,"short_name":"a","short_names":["a"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1518,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NEGATIVE SQUARED LATIN CAPITAL LETTER B","unified":"1F171-FE0F","non_qualified":"1F171","docomo":null,"au":"EB27","softbank":"E533","google":"FE50C","image":"1f171-fe0f.png","sheet_x":0,"sheet_y":17,"short_name":"b","short_names":["b"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1520,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NEGATIVE SQUARED LATIN CAPITAL LETTER O","unified":"1F17E-FE0F","non_qualified":"1F17E","docomo":null,"au":"EB28","softbank":"E535","google":"FE50E","image":"1f17e-fe0f.png","sheet_x":0,"sheet_y":18,"short_name":"o2","short_names":["o2"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1529,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NEGATIVE SQUARED LATIN CAPITAL LETTER P","unified":"1F17F-FE0F","non_qualified":"1F17F","docomo":"E66C","au":"E4A6","softbank":"E14F","google":"FE7F6","image":"1f17f-fe0f.png","sheet_x":0,"sheet_y":19,"short_name":"parking","short_names":["parking"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1531,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NEGATIVE SQUARED AB","unified":"1F18E","non_qualified":null,"docomo":null,"au":"EB29","softbank":"E534","google":"FE50D","image":"1f18e.png","sheet_x":0,"sheet_y":20,"short_name":"ab","short_names":["ab"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1519,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED CL","unified":"1F191","non_qualified":null,"docomo":"E6DB","au":"E5AB","softbank":null,"google":"FEB84","image":"1f191.png","sheet_x":0,"sheet_y":21,"short_name":"cl","short_names":["cl"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1521,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED COOL","unified":"1F192","non_qualified":null,"docomo":null,"au":"EA85","softbank":"E214","google":"FEB38","image":"1f192.png","sheet_x":0,"sheet_y":22,"short_name":"cool","short_names":["cool"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1522,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED FREE","unified":"1F193","non_qualified":null,"docomo":"E6D7","au":"E578","softbank":null,"google":"FEB21","image":"1f193.png","sheet_x":0,"sheet_y":23,"short_name":"free","short_names":["free"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1523,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED ID","unified":"1F194","non_qualified":null,"docomo":"E6D8","au":"EA88","softbank":"E229","google":"FEB81","image":"1f194.png","sheet_x":0,"sheet_y":24,"short_name":"id","short_names":["id"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1525,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED NEW","unified":"1F195","non_qualified":null,"docomo":"E6DD","au":"E5B5","softbank":"E212","google":"FEB36","image":"1f195.png","sheet_x":0,"sheet_y":25,"short_name":"new","short_names":["new"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1527,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED NG","unified":"1F196","non_qualified":null,"docomo":"E72F","au":null,"softbank":null,"google":"FEB28","image":"1f196.png","sheet_x":0,"sheet_y":26,"short_name":"ng","short_names":["ng"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1528,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED OK","unified":"1F197","non_qualified":null,"docomo":"E70B","au":"E5AD","softbank":"E24D","google":"FEB27","image":"1f197.png","sheet_x":0,"sheet_y":27,"short_name":"ok","short_names":["ok"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1530,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED SOS","unified":"1F198","non_qualified":null,"docomo":null,"au":"E4E8","softbank":null,"google":"FEB4F","image":"1f198.png","sheet_x":0,"sheet_y":28,"short_name":"sos","short_names":["sos"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1532,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED UP WITH EXCLAMATION MARK","unified":"1F199","non_qualified":null,"docomo":null,"au":"E50F","softbank":"E213","google":"FEB37","image":"1f199.png","sheet_x":0,"sheet_y":29,"short_name":"up","short_names":["up"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1533,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED VS","unified":"1F19A","non_qualified":null,"docomo":null,"au":"E5D2","softbank":"E12E","google":"FEB32","image":"1f19a.png","sheet_x":0,"sheet_y":30,"short_name":"vs","short_names":["vs"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1534,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Ascension Island Flag","unified":"1F1E6-1F1E8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e6-1f1e8.png","sheet_x":0,"sheet_y":31,"short_name":"flag-ac","short_names":["flag-ac"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1594,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Andorra Flag","unified":"1F1E6-1F1E9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e6-1f1e9.png","sheet_x":0,"sheet_y":32,"short_name":"flag-ad","short_names":["flag-ad"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1595,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"United Arab Emirates Flag","unified":"1F1E6-1F1EA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e6-1f1ea.png","sheet_x":0,"sheet_y":33,"short_name":"flag-ae","short_names":["flag-ae"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1596,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Afghanistan Flag","unified":"1F1E6-1F1EB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e6-1f1eb.png","sheet_x":0,"sheet_y":34,"short_name":"flag-af","short_names":["flag-af"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1597,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Antigua & Barbuda Flag","unified":"1F1E6-1F1EC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e6-1f1ec.png","sheet_x":0,"sheet_y":35,"short_name":"flag-ag","short_names":["flag-ag"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1598,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Anguilla Flag","unified":"1F1E6-1F1EE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e6-1f1ee.png","sheet_x":0,"sheet_y":36,"short_name":"flag-ai","short_names":["flag-ai"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1599,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Albania Flag","unified":"1F1E6-1F1F1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e6-1f1f1.png","sheet_x":0,"sheet_y":37,"short_name":"flag-al","short_names":["flag-al"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1600,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Armenia Flag","unified":"1F1E6-1F1F2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e6-1f1f2.png","sheet_x":0,"sheet_y":38,"short_name":"flag-am","short_names":["flag-am"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1601,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Angola Flag","unified":"1F1E6-1F1F4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e6-1f1f4.png","sheet_x":0,"sheet_y":39,"short_name":"flag-ao","short_names":["flag-ao"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1602,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Antarctica Flag","unified":"1F1E6-1F1F6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e6-1f1f6.png","sheet_x":0,"sheet_y":40,"short_name":"flag-aq","short_names":["flag-aq"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1603,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Argentina Flag","unified":"1F1E6-1F1F7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e6-1f1f7.png","sheet_x":0,"sheet_y":41,"short_name":"flag-ar","short_names":["flag-ar"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1604,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"American Samoa Flag","unified":"1F1E6-1F1F8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e6-1f1f8.png","sheet_x":0,"sheet_y":42,"short_name":"flag-as","short_names":["flag-as"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1605,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Austria Flag","unified":"1F1E6-1F1F9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e6-1f1f9.png","sheet_x":0,"sheet_y":43,"short_name":"flag-at","short_names":["flag-at"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1606,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Australia Flag","unified":"1F1E6-1F1FA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e6-1f1fa.png","sheet_x":0,"sheet_y":44,"short_name":"flag-au","short_names":["flag-au"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1607,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Aruba Flag","unified":"1F1E6-1F1FC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e6-1f1fc.png","sheet_x":0,"sheet_y":45,"short_name":"flag-aw","short_names":["flag-aw"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1608,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"\u00c5land Islands Flag","unified":"1F1E6-1F1FD","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e6-1f1fd.png","sheet_x":0,"sheet_y":46,"short_name":"flag-ax","short_names":["flag-ax"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1609,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Azerbaijan Flag","unified":"1F1E6-1F1FF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e6-1f1ff.png","sheet_x":0,"sheet_y":47,"short_name":"flag-az","short_names":["flag-az"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1610,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Bosnia & Herzegovina Flag","unified":"1F1E7-1F1E6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e7-1f1e6.png","sheet_x":0,"sheet_y":48,"short_name":"flag-ba","short_names":["flag-ba"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1611,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Barbados Flag","unified":"1F1E7-1F1E7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e7-1f1e7.png","sheet_x":0,"sheet_y":49,"short_name":"flag-bb","short_names":["flag-bb"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1612,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Bangladesh Flag","unified":"1F1E7-1F1E9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e7-1f1e9.png","sheet_x":0,"sheet_y":50,"short_name":"flag-bd","short_names":["flag-bd"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1613,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Belgium Flag","unified":"1F1E7-1F1EA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e7-1f1ea.png","sheet_x":0,"sheet_y":51,"short_name":"flag-be","short_names":["flag-be"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1614,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Burkina Faso Flag","unified":"1F1E7-1F1EB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e7-1f1eb.png","sheet_x":0,"sheet_y":52,"short_name":"flag-bf","short_names":["flag-bf"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1615,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Bulgaria Flag","unified":"1F1E7-1F1EC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e7-1f1ec.png","sheet_x":0,"sheet_y":53,"short_name":"flag-bg","short_names":["flag-bg"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1616,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Bahrain Flag","unified":"1F1E7-1F1ED","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e7-1f1ed.png","sheet_x":0,"sheet_y":54,"short_name":"flag-bh","short_names":["flag-bh"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1617,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Burundi Flag","unified":"1F1E7-1F1EE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e7-1f1ee.png","sheet_x":0,"sheet_y":55,"short_name":"flag-bi","short_names":["flag-bi"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1618,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Benin Flag","unified":"1F1E7-1F1EF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e7-1f1ef.png","sheet_x":0,"sheet_y":56,"short_name":"flag-bj","short_names":["flag-bj"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1619,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"St. Barth\u00e9lemy Flag","unified":"1F1E7-1F1F1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e7-1f1f1.png","sheet_x":0,"sheet_y":57,"short_name":"flag-bl","short_names":["flag-bl"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1620,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Bermuda Flag","unified":"1F1E7-1F1F2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e7-1f1f2.png","sheet_x":0,"sheet_y":58,"short_name":"flag-bm","short_names":["flag-bm"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1621,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Brunei Flag","unified":"1F1E7-1F1F3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e7-1f1f3.png","sheet_x":0,"sheet_y":59,"short_name":"flag-bn","short_names":["flag-bn"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1622,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Bolivia Flag","unified":"1F1E7-1F1F4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e7-1f1f4.png","sheet_x":0,"sheet_y":60,"short_name":"flag-bo","short_names":["flag-bo"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1623,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Caribbean Netherlands Flag","unified":"1F1E7-1F1F6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e7-1f1f6.png","sheet_x":1,"sheet_y":0,"short_name":"flag-bq","short_names":["flag-bq"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1624,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Brazil Flag","unified":"1F1E7-1F1F7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e7-1f1f7.png","sheet_x":1,"sheet_y":1,"short_name":"flag-br","short_names":["flag-br"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1625,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Bahamas Flag","unified":"1F1E7-1F1F8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e7-1f1f8.png","sheet_x":1,"sheet_y":2,"short_name":"flag-bs","short_names":["flag-bs"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1626,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Bhutan Flag","unified":"1F1E7-1F1F9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e7-1f1f9.png","sheet_x":1,"sheet_y":3,"short_name":"flag-bt","short_names":["flag-bt"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1627,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Bouvet Island Flag","unified":"1F1E7-1F1FB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e7-1f1fb.png","sheet_x":1,"sheet_y":4,"short_name":"flag-bv","short_names":["flag-bv"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1628,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Botswana Flag","unified":"1F1E7-1F1FC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e7-1f1fc.png","sheet_x":1,"sheet_y":5,"short_name":"flag-bw","short_names":["flag-bw"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1629,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Belarus Flag","unified":"1F1E7-1F1FE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e7-1f1fe.png","sheet_x":1,"sheet_y":6,"short_name":"flag-by","short_names":["flag-by"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1630,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Belize Flag","unified":"1F1E7-1F1FF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e7-1f1ff.png","sheet_x":1,"sheet_y":7,"short_name":"flag-bz","short_names":["flag-bz"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1631,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Canada Flag","unified":"1F1E8-1F1E6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e8-1f1e6.png","sheet_x":1,"sheet_y":8,"short_name":"flag-ca","short_names":["flag-ca"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1632,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Cocos (Keeling) Islands Flag","unified":"1F1E8-1F1E8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e8-1f1e8.png","sheet_x":1,"sheet_y":9,"short_name":"flag-cc","short_names":["flag-cc"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1633,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Congo - Kinshasa Flag","unified":"1F1E8-1F1E9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e8-1f1e9.png","sheet_x":1,"sheet_y":10,"short_name":"flag-cd","short_names":["flag-cd"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1634,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Central African Republic Flag","unified":"1F1E8-1F1EB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e8-1f1eb.png","sheet_x":1,"sheet_y":11,"short_name":"flag-cf","short_names":["flag-cf"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1635,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Congo - Brazzaville Flag","unified":"1F1E8-1F1EC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e8-1f1ec.png","sheet_x":1,"sheet_y":12,"short_name":"flag-cg","short_names":["flag-cg"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1636,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Switzerland Flag","unified":"1F1E8-1F1ED","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e8-1f1ed.png","sheet_x":1,"sheet_y":13,"short_name":"flag-ch","short_names":["flag-ch"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1637,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"C\u00f4te d\u2019Ivoire Flag","unified":"1F1E8-1F1EE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e8-1f1ee.png","sheet_x":1,"sheet_y":14,"short_name":"flag-ci","short_names":["flag-ci"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1638,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Cook Islands Flag","unified":"1F1E8-1F1F0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e8-1f1f0.png","sheet_x":1,"sheet_y":15,"short_name":"flag-ck","short_names":["flag-ck"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1639,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Chile Flag","unified":"1F1E8-1F1F1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e8-1f1f1.png","sheet_x":1,"sheet_y":16,"short_name":"flag-cl","short_names":["flag-cl"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1640,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Cameroon Flag","unified":"1F1E8-1F1F2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e8-1f1f2.png","sheet_x":1,"sheet_y":17,"short_name":"flag-cm","short_names":["flag-cm"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1641,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"China Flag","unified":"1F1E8-1F1F3","non_qualified":null,"docomo":null,"au":"EB11","softbank":"E513","google":"FE4ED","image":"1f1e8-1f1f3.png","sheet_x":1,"sheet_y":18,"short_name":"cn","short_names":["cn","flag-cn"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1642,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Colombia Flag","unified":"1F1E8-1F1F4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e8-1f1f4.png","sheet_x":1,"sheet_y":19,"short_name":"flag-co","short_names":["flag-co"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1643,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Clipperton Island Flag","unified":"1F1E8-1F1F5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e8-1f1f5.png","sheet_x":1,"sheet_y":20,"short_name":"flag-cp","short_names":["flag-cp"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1644,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Costa Rica Flag","unified":"1F1E8-1F1F7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e8-1f1f7.png","sheet_x":1,"sheet_y":21,"short_name":"flag-cr","short_names":["flag-cr"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1645,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Cuba Flag","unified":"1F1E8-1F1FA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e8-1f1fa.png","sheet_x":1,"sheet_y":22,"short_name":"flag-cu","short_names":["flag-cu"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1646,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Cape Verde Flag","unified":"1F1E8-1F1FB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e8-1f1fb.png","sheet_x":1,"sheet_y":23,"short_name":"flag-cv","short_names":["flag-cv"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1647,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Cura\u00e7ao Flag","unified":"1F1E8-1F1FC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e8-1f1fc.png","sheet_x":1,"sheet_y":24,"short_name":"flag-cw","short_names":["flag-cw"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1648,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Christmas Island Flag","unified":"1F1E8-1F1FD","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e8-1f1fd.png","sheet_x":1,"sheet_y":25,"short_name":"flag-cx","short_names":["flag-cx"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1649,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Cyprus Flag","unified":"1F1E8-1F1FE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e8-1f1fe.png","sheet_x":1,"sheet_y":26,"short_name":"flag-cy","short_names":["flag-cy"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1650,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Czechia Flag","unified":"1F1E8-1F1FF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e8-1f1ff.png","sheet_x":1,"sheet_y":27,"short_name":"flag-cz","short_names":["flag-cz"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1651,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Germany Flag","unified":"1F1E9-1F1EA","non_qualified":null,"docomo":null,"au":"EB0E","softbank":"E50E","google":"FE4E8","image":"1f1e9-1f1ea.png","sheet_x":1,"sheet_y":28,"short_name":"de","short_names":["de","flag-de"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1652,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Diego Garcia Flag","unified":"1F1E9-1F1EC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e9-1f1ec.png","sheet_x":1,"sheet_y":29,"short_name":"flag-dg","short_names":["flag-dg"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1653,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Djibouti Flag","unified":"1F1E9-1F1EF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e9-1f1ef.png","sheet_x":1,"sheet_y":30,"short_name":"flag-dj","short_names":["flag-dj"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1654,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Denmark Flag","unified":"1F1E9-1F1F0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e9-1f1f0.png","sheet_x":1,"sheet_y":31,"short_name":"flag-dk","short_names":["flag-dk"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1655,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Dominica Flag","unified":"1F1E9-1F1F2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e9-1f1f2.png","sheet_x":1,"sheet_y":32,"short_name":"flag-dm","short_names":["flag-dm"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1656,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Dominican Republic Flag","unified":"1F1E9-1F1F4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e9-1f1f4.png","sheet_x":1,"sheet_y":33,"short_name":"flag-do","short_names":["flag-do"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1657,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Algeria Flag","unified":"1F1E9-1F1FF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e9-1f1ff.png","sheet_x":1,"sheet_y":34,"short_name":"flag-dz","short_names":["flag-dz"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1658,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Ceuta & Melilla Flag","unified":"1F1EA-1F1E6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ea-1f1e6.png","sheet_x":1,"sheet_y":35,"short_name":"flag-ea","short_names":["flag-ea"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1659,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Ecuador Flag","unified":"1F1EA-1F1E8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ea-1f1e8.png","sheet_x":1,"sheet_y":36,"short_name":"flag-ec","short_names":["flag-ec"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1660,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Estonia Flag","unified":"1F1EA-1F1EA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ea-1f1ea.png","sheet_x":1,"sheet_y":37,"short_name":"flag-ee","short_names":["flag-ee"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1661,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Egypt Flag","unified":"1F1EA-1F1EC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ea-1f1ec.png","sheet_x":1,"sheet_y":38,"short_name":"flag-eg","short_names":["flag-eg"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1662,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Western Sahara Flag","unified":"1F1EA-1F1ED","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ea-1f1ed.png","sheet_x":1,"sheet_y":39,"short_name":"flag-eh","short_names":["flag-eh"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1663,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Eritrea Flag","unified":"1F1EA-1F1F7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ea-1f1f7.png","sheet_x":1,"sheet_y":40,"short_name":"flag-er","short_names":["flag-er"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1664,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Spain Flag","unified":"1F1EA-1F1F8","non_qualified":null,"docomo":null,"au":"E5D5","softbank":"E511","google":"FE4EB","image":"1f1ea-1f1f8.png","sheet_x":1,"sheet_y":41,"short_name":"es","short_names":["es","flag-es"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1665,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Ethiopia Flag","unified":"1F1EA-1F1F9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ea-1f1f9.png","sheet_x":1,"sheet_y":42,"short_name":"flag-et","short_names":["flag-et"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1666,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"European Union Flag","unified":"1F1EA-1F1FA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ea-1f1fa.png","sheet_x":1,"sheet_y":43,"short_name":"flag-eu","short_names":["flag-eu"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1667,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Finland Flag","unified":"1F1EB-1F1EE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1eb-1f1ee.png","sheet_x":1,"sheet_y":44,"short_name":"flag-fi","short_names":["flag-fi"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1668,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Fiji Flag","unified":"1F1EB-1F1EF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1eb-1f1ef.png","sheet_x":1,"sheet_y":45,"short_name":"flag-fj","short_names":["flag-fj"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1669,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Falkland Islands Flag","unified":"1F1EB-1F1F0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1eb-1f1f0.png","sheet_x":1,"sheet_y":46,"short_name":"flag-fk","short_names":["flag-fk"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1670,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Micronesia Flag","unified":"1F1EB-1F1F2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1eb-1f1f2.png","sheet_x":1,"sheet_y":47,"short_name":"flag-fm","short_names":["flag-fm"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1671,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Faroe Islands Flag","unified":"1F1EB-1F1F4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1eb-1f1f4.png","sheet_x":1,"sheet_y":48,"short_name":"flag-fo","short_names":["flag-fo"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1672,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"France Flag","unified":"1F1EB-1F1F7","non_qualified":null,"docomo":null,"au":"EAFA","softbank":"E50D","google":"FE4E7","image":"1f1eb-1f1f7.png","sheet_x":1,"sheet_y":49,"short_name":"fr","short_names":["fr","flag-fr"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1673,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Gabon Flag","unified":"1F1EC-1F1E6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ec-1f1e6.png","sheet_x":1,"sheet_y":50,"short_name":"flag-ga","short_names":["flag-ga"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1674,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"United Kingdom Flag","unified":"1F1EC-1F1E7","non_qualified":null,"docomo":null,"au":"EB10","softbank":"E510","google":"FE4EA","image":"1f1ec-1f1e7.png","sheet_x":1,"sheet_y":51,"short_name":"gb","short_names":["gb","uk","flag-gb"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1675,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Grenada Flag","unified":"1F1EC-1F1E9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ec-1f1e9.png","sheet_x":1,"sheet_y":52,"short_name":"flag-gd","short_names":["flag-gd"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1676,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Georgia Flag","unified":"1F1EC-1F1EA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ec-1f1ea.png","sheet_x":1,"sheet_y":53,"short_name":"flag-ge","short_names":["flag-ge"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1677,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"French Guiana Flag","unified":"1F1EC-1F1EB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ec-1f1eb.png","sheet_x":1,"sheet_y":54,"short_name":"flag-gf","short_names":["flag-gf"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1678,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Guernsey Flag","unified":"1F1EC-1F1EC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ec-1f1ec.png","sheet_x":1,"sheet_y":55,"short_name":"flag-gg","short_names":["flag-gg"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1679,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Ghana Flag","unified":"1F1EC-1F1ED","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ec-1f1ed.png","sheet_x":1,"sheet_y":56,"short_name":"flag-gh","short_names":["flag-gh"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1680,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Gibraltar Flag","unified":"1F1EC-1F1EE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ec-1f1ee.png","sheet_x":1,"sheet_y":57,"short_name":"flag-gi","short_names":["flag-gi"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1681,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Greenland Flag","unified":"1F1EC-1F1F1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ec-1f1f1.png","sheet_x":1,"sheet_y":58,"short_name":"flag-gl","short_names":["flag-gl"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1682,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Gambia Flag","unified":"1F1EC-1F1F2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ec-1f1f2.png","sheet_x":1,"sheet_y":59,"short_name":"flag-gm","short_names":["flag-gm"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1683,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Guinea Flag","unified":"1F1EC-1F1F3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ec-1f1f3.png","sheet_x":1,"sheet_y":60,"short_name":"flag-gn","short_names":["flag-gn"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1684,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Guadeloupe Flag","unified":"1F1EC-1F1F5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ec-1f1f5.png","sheet_x":2,"sheet_y":0,"short_name":"flag-gp","short_names":["flag-gp"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1685,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Equatorial Guinea Flag","unified":"1F1EC-1F1F6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ec-1f1f6.png","sheet_x":2,"sheet_y":1,"short_name":"flag-gq","short_names":["flag-gq"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1686,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Greece Flag","unified":"1F1EC-1F1F7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ec-1f1f7.png","sheet_x":2,"sheet_y":2,"short_name":"flag-gr","short_names":["flag-gr"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1687,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"South Georgia & South Sandwich Islands Flag","unified":"1F1EC-1F1F8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ec-1f1f8.png","sheet_x":2,"sheet_y":3,"short_name":"flag-gs","short_names":["flag-gs"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1688,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Guatemala Flag","unified":"1F1EC-1F1F9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ec-1f1f9.png","sheet_x":2,"sheet_y":4,"short_name":"flag-gt","short_names":["flag-gt"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1689,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Guam Flag","unified":"1F1EC-1F1FA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ec-1f1fa.png","sheet_x":2,"sheet_y":5,"short_name":"flag-gu","short_names":["flag-gu"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1690,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Guinea-Bissau Flag","unified":"1F1EC-1F1FC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ec-1f1fc.png","sheet_x":2,"sheet_y":6,"short_name":"flag-gw","short_names":["flag-gw"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1691,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Guyana Flag","unified":"1F1EC-1F1FE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ec-1f1fe.png","sheet_x":2,"sheet_y":7,"short_name":"flag-gy","short_names":["flag-gy"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1692,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Hong Kong SAR China Flag","unified":"1F1ED-1F1F0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ed-1f1f0.png","sheet_x":2,"sheet_y":8,"short_name":"flag-hk","short_names":["flag-hk"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1693,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Heard & McDonald Islands Flag","unified":"1F1ED-1F1F2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ed-1f1f2.png","sheet_x":2,"sheet_y":9,"short_name":"flag-hm","short_names":["flag-hm"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1694,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Honduras Flag","unified":"1F1ED-1F1F3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ed-1f1f3.png","sheet_x":2,"sheet_y":10,"short_name":"flag-hn","short_names":["flag-hn"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1695,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Croatia Flag","unified":"1F1ED-1F1F7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ed-1f1f7.png","sheet_x":2,"sheet_y":11,"short_name":"flag-hr","short_names":["flag-hr"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1696,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Haiti Flag","unified":"1F1ED-1F1F9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ed-1f1f9.png","sheet_x":2,"sheet_y":12,"short_name":"flag-ht","short_names":["flag-ht"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1697,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Hungary Flag","unified":"1F1ED-1F1FA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ed-1f1fa.png","sheet_x":2,"sheet_y":13,"short_name":"flag-hu","short_names":["flag-hu"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1698,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Canary Islands Flag","unified":"1F1EE-1F1E8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ee-1f1e8.png","sheet_x":2,"sheet_y":14,"short_name":"flag-ic","short_names":["flag-ic"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1699,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Indonesia Flag","unified":"1F1EE-1F1E9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ee-1f1e9.png","sheet_x":2,"sheet_y":15,"short_name":"flag-id","short_names":["flag-id"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1700,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Ireland Flag","unified":"1F1EE-1F1EA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ee-1f1ea.png","sheet_x":2,"sheet_y":16,"short_name":"flag-ie","short_names":["flag-ie"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1701,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Israel Flag","unified":"1F1EE-1F1F1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ee-1f1f1.png","sheet_x":2,"sheet_y":17,"short_name":"flag-il","short_names":["flag-il"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1702,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Isle of Man Flag","unified":"1F1EE-1F1F2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ee-1f1f2.png","sheet_x":2,"sheet_y":18,"short_name":"flag-im","short_names":["flag-im"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1703,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"India Flag","unified":"1F1EE-1F1F3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ee-1f1f3.png","sheet_x":2,"sheet_y":19,"short_name":"flag-in","short_names":["flag-in"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1704,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"British Indian Ocean Territory Flag","unified":"1F1EE-1F1F4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ee-1f1f4.png","sheet_x":2,"sheet_y":20,"short_name":"flag-io","short_names":["flag-io"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1705,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Iraq Flag","unified":"1F1EE-1F1F6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ee-1f1f6.png","sheet_x":2,"sheet_y":21,"short_name":"flag-iq","short_names":["flag-iq"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1706,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Iran Flag","unified":"1F1EE-1F1F7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ee-1f1f7.png","sheet_x":2,"sheet_y":22,"short_name":"flag-ir","short_names":["flag-ir"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1707,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Iceland Flag","unified":"1F1EE-1F1F8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ee-1f1f8.png","sheet_x":2,"sheet_y":23,"short_name":"flag-is","short_names":["flag-is"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1708,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Italy Flag","unified":"1F1EE-1F1F9","non_qualified":null,"docomo":null,"au":"EB0F","softbank":"E50F","google":"FE4E9","image":"1f1ee-1f1f9.png","sheet_x":2,"sheet_y":24,"short_name":"it","short_names":["it","flag-it"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1709,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Jersey Flag","unified":"1F1EF-1F1EA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ef-1f1ea.png","sheet_x":2,"sheet_y":25,"short_name":"flag-je","short_names":["flag-je"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1710,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Jamaica Flag","unified":"1F1EF-1F1F2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ef-1f1f2.png","sheet_x":2,"sheet_y":26,"short_name":"flag-jm","short_names":["flag-jm"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1711,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Jordan Flag","unified":"1F1EF-1F1F4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ef-1f1f4.png","sheet_x":2,"sheet_y":27,"short_name":"flag-jo","short_names":["flag-jo"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1712,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Japan Flag","unified":"1F1EF-1F1F5","non_qualified":null,"docomo":null,"au":"E4CC","softbank":"E50B","google":"FE4E5","image":"1f1ef-1f1f5.png","sheet_x":2,"sheet_y":28,"short_name":"jp","short_names":["jp","flag-jp"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1713,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Kenya Flag","unified":"1F1F0-1F1EA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f0-1f1ea.png","sheet_x":2,"sheet_y":29,"short_name":"flag-ke","short_names":["flag-ke"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1714,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Kyrgyzstan Flag","unified":"1F1F0-1F1EC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f0-1f1ec.png","sheet_x":2,"sheet_y":30,"short_name":"flag-kg","short_names":["flag-kg"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1715,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Cambodia Flag","unified":"1F1F0-1F1ED","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f0-1f1ed.png","sheet_x":2,"sheet_y":31,"short_name":"flag-kh","short_names":["flag-kh"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1716,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Kiribati Flag","unified":"1F1F0-1F1EE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f0-1f1ee.png","sheet_x":2,"sheet_y":32,"short_name":"flag-ki","short_names":["flag-ki"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1717,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Comoros Flag","unified":"1F1F0-1F1F2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f0-1f1f2.png","sheet_x":2,"sheet_y":33,"short_name":"flag-km","short_names":["flag-km"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1718,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"St. Kitts & Nevis Flag","unified":"1F1F0-1F1F3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f0-1f1f3.png","sheet_x":2,"sheet_y":34,"short_name":"flag-kn","short_names":["flag-kn"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1719,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"North Korea Flag","unified":"1F1F0-1F1F5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f0-1f1f5.png","sheet_x":2,"sheet_y":35,"short_name":"flag-kp","short_names":["flag-kp"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1720,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"South Korea Flag","unified":"1F1F0-1F1F7","non_qualified":null,"docomo":null,"au":"EB12","softbank":"E514","google":"FE4EE","image":"1f1f0-1f1f7.png","sheet_x":2,"sheet_y":36,"short_name":"kr","short_names":["kr","flag-kr"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1721,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Kuwait Flag","unified":"1F1F0-1F1FC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f0-1f1fc.png","sheet_x":2,"sheet_y":37,"short_name":"flag-kw","short_names":["flag-kw"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1722,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Cayman Islands Flag","unified":"1F1F0-1F1FE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f0-1f1fe.png","sheet_x":2,"sheet_y":38,"short_name":"flag-ky","short_names":["flag-ky"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1723,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Kazakhstan Flag","unified":"1F1F0-1F1FF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f0-1f1ff.png","sheet_x":2,"sheet_y":39,"short_name":"flag-kz","short_names":["flag-kz"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1724,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Laos Flag","unified":"1F1F1-1F1E6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f1-1f1e6.png","sheet_x":2,"sheet_y":40,"short_name":"flag-la","short_names":["flag-la"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1725,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Lebanon Flag","unified":"1F1F1-1F1E7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f1-1f1e7.png","sheet_x":2,"sheet_y":41,"short_name":"flag-lb","short_names":["flag-lb"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1726,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"St. Lucia Flag","unified":"1F1F1-1F1E8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f1-1f1e8.png","sheet_x":2,"sheet_y":42,"short_name":"flag-lc","short_names":["flag-lc"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1727,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Liechtenstein Flag","unified":"1F1F1-1F1EE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f1-1f1ee.png","sheet_x":2,"sheet_y":43,"short_name":"flag-li","short_names":["flag-li"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1728,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Sri Lanka Flag","unified":"1F1F1-1F1F0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f1-1f1f0.png","sheet_x":2,"sheet_y":44,"short_name":"flag-lk","short_names":["flag-lk"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1729,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Liberia Flag","unified":"1F1F1-1F1F7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f1-1f1f7.png","sheet_x":2,"sheet_y":45,"short_name":"flag-lr","short_names":["flag-lr"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1730,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Lesotho Flag","unified":"1F1F1-1F1F8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f1-1f1f8.png","sheet_x":2,"sheet_y":46,"short_name":"flag-ls","short_names":["flag-ls"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1731,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Lithuania Flag","unified":"1F1F1-1F1F9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f1-1f1f9.png","sheet_x":2,"sheet_y":47,"short_name":"flag-lt","short_names":["flag-lt"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1732,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Luxembourg Flag","unified":"1F1F1-1F1FA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f1-1f1fa.png","sheet_x":2,"sheet_y":48,"short_name":"flag-lu","short_names":["flag-lu"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1733,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Latvia Flag","unified":"1F1F1-1F1FB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f1-1f1fb.png","sheet_x":2,"sheet_y":49,"short_name":"flag-lv","short_names":["flag-lv"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1734,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Libya Flag","unified":"1F1F1-1F1FE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f1-1f1fe.png","sheet_x":2,"sheet_y":50,"short_name":"flag-ly","short_names":["flag-ly"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1735,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Morocco Flag","unified":"1F1F2-1F1E6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1e6.png","sheet_x":2,"sheet_y":51,"short_name":"flag-ma","short_names":["flag-ma"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1736,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Monaco Flag","unified":"1F1F2-1F1E8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1e8.png","sheet_x":2,"sheet_y":52,"short_name":"flag-mc","short_names":["flag-mc"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1737,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Moldova Flag","unified":"1F1F2-1F1E9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1e9.png","sheet_x":2,"sheet_y":53,"short_name":"flag-md","short_names":["flag-md"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1738,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Montenegro Flag","unified":"1F1F2-1F1EA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1ea.png","sheet_x":2,"sheet_y":54,"short_name":"flag-me","short_names":["flag-me"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1739,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"St. Martin Flag","unified":"1F1F2-1F1EB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1eb.png","sheet_x":2,"sheet_y":55,"short_name":"flag-mf","short_names":["flag-mf"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1740,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Madagascar Flag","unified":"1F1F2-1F1EC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1ec.png","sheet_x":2,"sheet_y":56,"short_name":"flag-mg","short_names":["flag-mg"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1741,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Marshall Islands Flag","unified":"1F1F2-1F1ED","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1ed.png","sheet_x":2,"sheet_y":57,"short_name":"flag-mh","short_names":["flag-mh"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1742,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"North Macedonia Flag","unified":"1F1F2-1F1F0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1f0.png","sheet_x":2,"sheet_y":58,"short_name":"flag-mk","short_names":["flag-mk"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1743,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Mali Flag","unified":"1F1F2-1F1F1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1f1.png","sheet_x":2,"sheet_y":59,"short_name":"flag-ml","short_names":["flag-ml"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1744,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Myanmar (Burma) Flag","unified":"1F1F2-1F1F2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1f2.png","sheet_x":2,"sheet_y":60,"short_name":"flag-mm","short_names":["flag-mm"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1745,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Mongolia Flag","unified":"1F1F2-1F1F3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1f3.png","sheet_x":3,"sheet_y":0,"short_name":"flag-mn","short_names":["flag-mn"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1746,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Macao SAR China Flag","unified":"1F1F2-1F1F4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1f4.png","sheet_x":3,"sheet_y":1,"short_name":"flag-mo","short_names":["flag-mo"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1747,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Northern Mariana Islands Flag","unified":"1F1F2-1F1F5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1f5.png","sheet_x":3,"sheet_y":2,"short_name":"flag-mp","short_names":["flag-mp"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1748,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Martinique Flag","unified":"1F1F2-1F1F6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1f6.png","sheet_x":3,"sheet_y":3,"short_name":"flag-mq","short_names":["flag-mq"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1749,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Mauritania Flag","unified":"1F1F2-1F1F7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1f7.png","sheet_x":3,"sheet_y":4,"short_name":"flag-mr","short_names":["flag-mr"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1750,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Montserrat Flag","unified":"1F1F2-1F1F8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1f8.png","sheet_x":3,"sheet_y":5,"short_name":"flag-ms","short_names":["flag-ms"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1751,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Malta Flag","unified":"1F1F2-1F1F9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1f9.png","sheet_x":3,"sheet_y":6,"short_name":"flag-mt","short_names":["flag-mt"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1752,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Mauritius Flag","unified":"1F1F2-1F1FA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1fa.png","sheet_x":3,"sheet_y":7,"short_name":"flag-mu","short_names":["flag-mu"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1753,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Maldives Flag","unified":"1F1F2-1F1FB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1fb.png","sheet_x":3,"sheet_y":8,"short_name":"flag-mv","short_names":["flag-mv"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1754,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Malawi Flag","unified":"1F1F2-1F1FC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1fc.png","sheet_x":3,"sheet_y":9,"short_name":"flag-mw","short_names":["flag-mw"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1755,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Mexico Flag","unified":"1F1F2-1F1FD","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1fd.png","sheet_x":3,"sheet_y":10,"short_name":"flag-mx","short_names":["flag-mx"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1756,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Malaysia Flag","unified":"1F1F2-1F1FE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1fe.png","sheet_x":3,"sheet_y":11,"short_name":"flag-my","short_names":["flag-my"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1757,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Mozambique Flag","unified":"1F1F2-1F1FF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1ff.png","sheet_x":3,"sheet_y":12,"short_name":"flag-mz","short_names":["flag-mz"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1758,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Namibia Flag","unified":"1F1F3-1F1E6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f3-1f1e6.png","sheet_x":3,"sheet_y":13,"short_name":"flag-na","short_names":["flag-na"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1759,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"New Caledonia Flag","unified":"1F1F3-1F1E8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f3-1f1e8.png","sheet_x":3,"sheet_y":14,"short_name":"flag-nc","short_names":["flag-nc"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1760,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Niger Flag","unified":"1F1F3-1F1EA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f3-1f1ea.png","sheet_x":3,"sheet_y":15,"short_name":"flag-ne","short_names":["flag-ne"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1761,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Norfolk Island Flag","unified":"1F1F3-1F1EB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f3-1f1eb.png","sheet_x":3,"sheet_y":16,"short_name":"flag-nf","short_names":["flag-nf"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1762,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Nigeria Flag","unified":"1F1F3-1F1EC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f3-1f1ec.png","sheet_x":3,"sheet_y":17,"short_name":"flag-ng","short_names":["flag-ng"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1763,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Nicaragua Flag","unified":"1F1F3-1F1EE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f3-1f1ee.png","sheet_x":3,"sheet_y":18,"short_name":"flag-ni","short_names":["flag-ni"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1764,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Netherlands Flag","unified":"1F1F3-1F1F1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f3-1f1f1.png","sheet_x":3,"sheet_y":19,"short_name":"flag-nl","short_names":["flag-nl"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1765,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Norway Flag","unified":"1F1F3-1F1F4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f3-1f1f4.png","sheet_x":3,"sheet_y":20,"short_name":"flag-no","short_names":["flag-no"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1766,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Nepal Flag","unified":"1F1F3-1F1F5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f3-1f1f5.png","sheet_x":3,"sheet_y":21,"short_name":"flag-np","short_names":["flag-np"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1767,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Nauru Flag","unified":"1F1F3-1F1F7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f3-1f1f7.png","sheet_x":3,"sheet_y":22,"short_name":"flag-nr","short_names":["flag-nr"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1768,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Niue Flag","unified":"1F1F3-1F1FA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f3-1f1fa.png","sheet_x":3,"sheet_y":23,"short_name":"flag-nu","short_names":["flag-nu"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1769,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"New Zealand Flag","unified":"1F1F3-1F1FF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f3-1f1ff.png","sheet_x":3,"sheet_y":24,"short_name":"flag-nz","short_names":["flag-nz"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1770,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Oman Flag","unified":"1F1F4-1F1F2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f4-1f1f2.png","sheet_x":3,"sheet_y":25,"short_name":"flag-om","short_names":["flag-om"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1771,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Panama Flag","unified":"1F1F5-1F1E6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f5-1f1e6.png","sheet_x":3,"sheet_y":26,"short_name":"flag-pa","short_names":["flag-pa"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1772,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Peru Flag","unified":"1F1F5-1F1EA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f5-1f1ea.png","sheet_x":3,"sheet_y":27,"short_name":"flag-pe","short_names":["flag-pe"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1773,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"French Polynesia Flag","unified":"1F1F5-1F1EB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f5-1f1eb.png","sheet_x":3,"sheet_y":28,"short_name":"flag-pf","short_names":["flag-pf"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1774,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Papua New Guinea Flag","unified":"1F1F5-1F1EC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f5-1f1ec.png","sheet_x":3,"sheet_y":29,"short_name":"flag-pg","short_names":["flag-pg"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1775,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Philippines Flag","unified":"1F1F5-1F1ED","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f5-1f1ed.png","sheet_x":3,"sheet_y":30,"short_name":"flag-ph","short_names":["flag-ph"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1776,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Pakistan Flag","unified":"1F1F5-1F1F0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f5-1f1f0.png","sheet_x":3,"sheet_y":31,"short_name":"flag-pk","short_names":["flag-pk"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1777,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Poland Flag","unified":"1F1F5-1F1F1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f5-1f1f1.png","sheet_x":3,"sheet_y":32,"short_name":"flag-pl","short_names":["flag-pl"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1778,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"St. Pierre & Miquelon Flag","unified":"1F1F5-1F1F2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f5-1f1f2.png","sheet_x":3,"sheet_y":33,"short_name":"flag-pm","short_names":["flag-pm"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1779,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Pitcairn Islands Flag","unified":"1F1F5-1F1F3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f5-1f1f3.png","sheet_x":3,"sheet_y":34,"short_name":"flag-pn","short_names":["flag-pn"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1780,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Puerto Rico Flag","unified":"1F1F5-1F1F7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f5-1f1f7.png","sheet_x":3,"sheet_y":35,"short_name":"flag-pr","short_names":["flag-pr"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1781,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Palestinian Territories Flag","unified":"1F1F5-1F1F8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f5-1f1f8.png","sheet_x":3,"sheet_y":36,"short_name":"flag-ps","short_names":["flag-ps"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1782,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Portugal Flag","unified":"1F1F5-1F1F9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f5-1f1f9.png","sheet_x":3,"sheet_y":37,"short_name":"flag-pt","short_names":["flag-pt"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1783,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Palau Flag","unified":"1F1F5-1F1FC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f5-1f1fc.png","sheet_x":3,"sheet_y":38,"short_name":"flag-pw","short_names":["flag-pw"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1784,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Paraguay Flag","unified":"1F1F5-1F1FE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f5-1f1fe.png","sheet_x":3,"sheet_y":39,"short_name":"flag-py","short_names":["flag-py"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1785,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Qatar Flag","unified":"1F1F6-1F1E6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f6-1f1e6.png","sheet_x":3,"sheet_y":40,"short_name":"flag-qa","short_names":["flag-qa"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1786,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"R\u00e9union Flag","unified":"1F1F7-1F1EA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f7-1f1ea.png","sheet_x":3,"sheet_y":41,"short_name":"flag-re","short_names":["flag-re"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1787,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Romania Flag","unified":"1F1F7-1F1F4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f7-1f1f4.png","sheet_x":3,"sheet_y":42,"short_name":"flag-ro","short_names":["flag-ro"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1788,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Serbia Flag","unified":"1F1F7-1F1F8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f7-1f1f8.png","sheet_x":3,"sheet_y":43,"short_name":"flag-rs","short_names":["flag-rs"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1789,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Russia Flag","unified":"1F1F7-1F1FA","non_qualified":null,"docomo":null,"au":"E5D6","softbank":"E512","google":"FE4EC","image":"1f1f7-1f1fa.png","sheet_x":3,"sheet_y":44,"short_name":"ru","short_names":["ru","flag-ru"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1790,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Rwanda Flag","unified":"1F1F7-1F1FC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f7-1f1fc.png","sheet_x":3,"sheet_y":45,"short_name":"flag-rw","short_names":["flag-rw"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1791,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Saudi Arabia Flag","unified":"1F1F8-1F1E6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f8-1f1e6.png","sheet_x":3,"sheet_y":46,"short_name":"flag-sa","short_names":["flag-sa"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1792,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Solomon Islands Flag","unified":"1F1F8-1F1E7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f8-1f1e7.png","sheet_x":3,"sheet_y":47,"short_name":"flag-sb","short_names":["flag-sb"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1793,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Seychelles Flag","unified":"1F1F8-1F1E8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f8-1f1e8.png","sheet_x":3,"sheet_y":48,"short_name":"flag-sc","short_names":["flag-sc"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1794,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Sudan Flag","unified":"1F1F8-1F1E9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f8-1f1e9.png","sheet_x":3,"sheet_y":49,"short_name":"flag-sd","short_names":["flag-sd"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1795,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Sweden Flag","unified":"1F1F8-1F1EA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f8-1f1ea.png","sheet_x":3,"sheet_y":50,"short_name":"flag-se","short_names":["flag-se"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1796,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Singapore Flag","unified":"1F1F8-1F1EC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f8-1f1ec.png","sheet_x":3,"sheet_y":51,"short_name":"flag-sg","short_names":["flag-sg"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1797,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"St. Helena Flag","unified":"1F1F8-1F1ED","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f8-1f1ed.png","sheet_x":3,"sheet_y":52,"short_name":"flag-sh","short_names":["flag-sh"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1798,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Slovenia Flag","unified":"1F1F8-1F1EE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f8-1f1ee.png","sheet_x":3,"sheet_y":53,"short_name":"flag-si","short_names":["flag-si"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1799,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Svalbard & Jan Mayen Flag","unified":"1F1F8-1F1EF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f8-1f1ef.png","sheet_x":3,"sheet_y":54,"short_name":"flag-sj","short_names":["flag-sj"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1800,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Slovakia Flag","unified":"1F1F8-1F1F0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f8-1f1f0.png","sheet_x":3,"sheet_y":55,"short_name":"flag-sk","short_names":["flag-sk"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1801,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Sierra Leone Flag","unified":"1F1F8-1F1F1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f8-1f1f1.png","sheet_x":3,"sheet_y":56,"short_name":"flag-sl","short_names":["flag-sl"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1802,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"San Marino Flag","unified":"1F1F8-1F1F2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f8-1f1f2.png","sheet_x":3,"sheet_y":57,"short_name":"flag-sm","short_names":["flag-sm"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1803,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Senegal Flag","unified":"1F1F8-1F1F3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f8-1f1f3.png","sheet_x":3,"sheet_y":58,"short_name":"flag-sn","short_names":["flag-sn"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1804,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Somalia Flag","unified":"1F1F8-1F1F4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f8-1f1f4.png","sheet_x":3,"sheet_y":59,"short_name":"flag-so","short_names":["flag-so"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1805,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Suriname Flag","unified":"1F1F8-1F1F7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f8-1f1f7.png","sheet_x":3,"sheet_y":60,"short_name":"flag-sr","short_names":["flag-sr"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1806,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"South Sudan Flag","unified":"1F1F8-1F1F8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f8-1f1f8.png","sheet_x":4,"sheet_y":0,"short_name":"flag-ss","short_names":["flag-ss"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1807,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"S\u00e3o Tom\u00e9 & Pr\u00edncipe Flag","unified":"1F1F8-1F1F9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f8-1f1f9.png","sheet_x":4,"sheet_y":1,"short_name":"flag-st","short_names":["flag-st"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1808,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"El Salvador Flag","unified":"1F1F8-1F1FB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f8-1f1fb.png","sheet_x":4,"sheet_y":2,"short_name":"flag-sv","short_names":["flag-sv"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1809,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Sint Maarten Flag","unified":"1F1F8-1F1FD","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f8-1f1fd.png","sheet_x":4,"sheet_y":3,"short_name":"flag-sx","short_names":["flag-sx"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1810,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Syria Flag","unified":"1F1F8-1F1FE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f8-1f1fe.png","sheet_x":4,"sheet_y":4,"short_name":"flag-sy","short_names":["flag-sy"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1811,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Eswatini Flag","unified":"1F1F8-1F1FF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f8-1f1ff.png","sheet_x":4,"sheet_y":5,"short_name":"flag-sz","short_names":["flag-sz"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1812,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Tristan da Cunha Flag","unified":"1F1F9-1F1E6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f9-1f1e6.png","sheet_x":4,"sheet_y":6,"short_name":"flag-ta","short_names":["flag-ta"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1813,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Turks & Caicos Islands Flag","unified":"1F1F9-1F1E8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f9-1f1e8.png","sheet_x":4,"sheet_y":7,"short_name":"flag-tc","short_names":["flag-tc"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1814,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Chad Flag","unified":"1F1F9-1F1E9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f9-1f1e9.png","sheet_x":4,"sheet_y":8,"short_name":"flag-td","short_names":["flag-td"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1815,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"French Southern Territories Flag","unified":"1F1F9-1F1EB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f9-1f1eb.png","sheet_x":4,"sheet_y":9,"short_name":"flag-tf","short_names":["flag-tf"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1816,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Togo Flag","unified":"1F1F9-1F1EC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f9-1f1ec.png","sheet_x":4,"sheet_y":10,"short_name":"flag-tg","short_names":["flag-tg"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1817,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Thailand Flag","unified":"1F1F9-1F1ED","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f9-1f1ed.png","sheet_x":4,"sheet_y":11,"short_name":"flag-th","short_names":["flag-th"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1818,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Tajikistan Flag","unified":"1F1F9-1F1EF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f9-1f1ef.png","sheet_x":4,"sheet_y":12,"short_name":"flag-tj","short_names":["flag-tj"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1819,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Tokelau Flag","unified":"1F1F9-1F1F0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f9-1f1f0.png","sheet_x":4,"sheet_y":13,"short_name":"flag-tk","short_names":["flag-tk"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1820,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Timor-Leste Flag","unified":"1F1F9-1F1F1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f9-1f1f1.png","sheet_x":4,"sheet_y":14,"short_name":"flag-tl","short_names":["flag-tl"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1821,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Turkmenistan Flag","unified":"1F1F9-1F1F2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f9-1f1f2.png","sheet_x":4,"sheet_y":15,"short_name":"flag-tm","short_names":["flag-tm"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1822,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Tunisia Flag","unified":"1F1F9-1F1F3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f9-1f1f3.png","sheet_x":4,"sheet_y":16,"short_name":"flag-tn","short_names":["flag-tn"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1823,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Tonga Flag","unified":"1F1F9-1F1F4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f9-1f1f4.png","sheet_x":4,"sheet_y":17,"short_name":"flag-to","short_names":["flag-to"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1824,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Turkey Flag","unified":"1F1F9-1F1F7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f9-1f1f7.png","sheet_x":4,"sheet_y":18,"short_name":"flag-tr","short_names":["flag-tr"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1825,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Trinidad & Tobago Flag","unified":"1F1F9-1F1F9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f9-1f1f9.png","sheet_x":4,"sheet_y":19,"short_name":"flag-tt","short_names":["flag-tt"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1826,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Tuvalu Flag","unified":"1F1F9-1F1FB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f9-1f1fb.png","sheet_x":4,"sheet_y":20,"short_name":"flag-tv","short_names":["flag-tv"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1827,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Taiwan Flag","unified":"1F1F9-1F1FC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f9-1f1fc.png","sheet_x":4,"sheet_y":21,"short_name":"flag-tw","short_names":["flag-tw"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1828,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Tanzania Flag","unified":"1F1F9-1F1FF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f9-1f1ff.png","sheet_x":4,"sheet_y":22,"short_name":"flag-tz","short_names":["flag-tz"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1829,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Ukraine Flag","unified":"1F1FA-1F1E6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1fa-1f1e6.png","sheet_x":4,"sheet_y":23,"short_name":"flag-ua","short_names":["flag-ua"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1830,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Uganda Flag","unified":"1F1FA-1F1EC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1fa-1f1ec.png","sheet_x":4,"sheet_y":24,"short_name":"flag-ug","short_names":["flag-ug"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1831,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"U.S. Outlying Islands Flag","unified":"1F1FA-1F1F2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1fa-1f1f2.png","sheet_x":4,"sheet_y":25,"short_name":"flag-um","short_names":["flag-um"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1832,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"United Nations Flag","unified":"1F1FA-1F1F3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1fa-1f1f3.png","sheet_x":4,"sheet_y":26,"short_name":"flag-un","short_names":["flag-un"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1833,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"United States Flag","unified":"1F1FA-1F1F8","non_qualified":null,"docomo":null,"au":"E573","softbank":"E50C","google":"FE4E6","image":"1f1fa-1f1f8.png","sheet_x":4,"sheet_y":27,"short_name":"us","short_names":["us","flag-us"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1834,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Uruguay Flag","unified":"1F1FA-1F1FE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1fa-1f1fe.png","sheet_x":4,"sheet_y":28,"short_name":"flag-uy","short_names":["flag-uy"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1835,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Uzbekistan Flag","unified":"1F1FA-1F1FF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1fa-1f1ff.png","sheet_x":4,"sheet_y":29,"short_name":"flag-uz","short_names":["flag-uz"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1836,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Vatican City Flag","unified":"1F1FB-1F1E6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1fb-1f1e6.png","sheet_x":4,"sheet_y":30,"short_name":"flag-va","short_names":["flag-va"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1837,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"St. Vincent & Grenadines Flag","unified":"1F1FB-1F1E8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1fb-1f1e8.png","sheet_x":4,"sheet_y":31,"short_name":"flag-vc","short_names":["flag-vc"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1838,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Venezuela Flag","unified":"1F1FB-1F1EA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1fb-1f1ea.png","sheet_x":4,"sheet_y":32,"short_name":"flag-ve","short_names":["flag-ve"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1839,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"British Virgin Islands Flag","unified":"1F1FB-1F1EC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1fb-1f1ec.png","sheet_x":4,"sheet_y":33,"short_name":"flag-vg","short_names":["flag-vg"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1840,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"U.S. Virgin Islands Flag","unified":"1F1FB-1F1EE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1fb-1f1ee.png","sheet_x":4,"sheet_y":34,"short_name":"flag-vi","short_names":["flag-vi"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1841,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Vietnam Flag","unified":"1F1FB-1F1F3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1fb-1f1f3.png","sheet_x":4,"sheet_y":35,"short_name":"flag-vn","short_names":["flag-vn"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1842,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Vanuatu Flag","unified":"1F1FB-1F1FA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1fb-1f1fa.png","sheet_x":4,"sheet_y":36,"short_name":"flag-vu","short_names":["flag-vu"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1843,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Wallis & Futuna Flag","unified":"1F1FC-1F1EB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1fc-1f1eb.png","sheet_x":4,"sheet_y":37,"short_name":"flag-wf","short_names":["flag-wf"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1844,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Samoa Flag","unified":"1F1FC-1F1F8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1fc-1f1f8.png","sheet_x":4,"sheet_y":38,"short_name":"flag-ws","short_names":["flag-ws"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1845,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Kosovo Flag","unified":"1F1FD-1F1F0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1fd-1f1f0.png","sheet_x":4,"sheet_y":39,"short_name":"flag-xk","short_names":["flag-xk"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1846,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Yemen Flag","unified":"1F1FE-1F1EA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1fe-1f1ea.png","sheet_x":4,"sheet_y":40,"short_name":"flag-ye","short_names":["flag-ye"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1847,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Mayotte Flag","unified":"1F1FE-1F1F9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1fe-1f1f9.png","sheet_x":4,"sheet_y":41,"short_name":"flag-yt","short_names":["flag-yt"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1848,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"South Africa Flag","unified":"1F1FF-1F1E6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ff-1f1e6.png","sheet_x":4,"sheet_y":42,"short_name":"flag-za","short_names":["flag-za"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1849,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Zambia Flag","unified":"1F1FF-1F1F2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ff-1f1f2.png","sheet_x":4,"sheet_y":43,"short_name":"flag-zm","short_names":["flag-zm"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1850,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Zimbabwe Flag","unified":"1F1FF-1F1FC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ff-1f1fc.png","sheet_x":4,"sheet_y":44,"short_name":"flag-zw","short_names":["flag-zw"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1851,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED KATAKANA KOKO","unified":"1F201","non_qualified":null,"docomo":null,"au":null,"softbank":"E203","google":"FEB24","image":"1f201.png","sheet_x":4,"sheet_y":45,"short_name":"koko","short_names":["koko"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1535,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED KATAKANA SA","unified":"1F202-FE0F","non_qualified":"1F202","docomo":null,"au":"EA87","softbank":"E228","google":"FEB3F","image":"1f202-fe0f.png","sheet_x":4,"sheet_y":46,"short_name":"sa","short_names":["sa"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1536,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED CJK UNIFIED IDEOGRAPH-7121","unified":"1F21A","non_qualified":null,"docomo":null,"au":null,"softbank":"E216","google":"FEB3A","image":"1f21a.png","sheet_x":4,"sheet_y":47,"short_name":"u7121","short_names":["u7121"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1542,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED CJK UNIFIED IDEOGRAPH-6307","unified":"1F22F","non_qualified":null,"docomo":null,"au":"EA8B","softbank":"E22C","google":"FEB40","image":"1f22f.png","sheet_x":4,"sheet_y":48,"short_name":"u6307","short_names":["u6307"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1539,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED CJK UNIFIED IDEOGRAPH-7981","unified":"1F232","non_qualified":null,"docomo":"E738","au":null,"softbank":null,"google":"FEB2E","image":"1f232.png","sheet_x":4,"sheet_y":49,"short_name":"u7981","short_names":["u7981"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1543,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED CJK UNIFIED IDEOGRAPH-7A7A","unified":"1F233","non_qualified":null,"docomo":"E739","au":"EA8A","softbank":"E22B","google":"FEB2F","image":"1f233.png","sheet_x":4,"sheet_y":50,"short_name":"u7a7a","short_names":["u7a7a"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1547,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED CJK UNIFIED IDEOGRAPH-5408","unified":"1F234","non_qualified":null,"docomo":"E73A","au":null,"softbank":null,"google":"FEB30","image":"1f234.png","sheet_x":4,"sheet_y":51,"short_name":"u5408","short_names":["u5408"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1546,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED CJK UNIFIED IDEOGRAPH-6E80","unified":"1F235","non_qualified":null,"docomo":"E73B","au":"EA89","softbank":"E22A","google":"FEB31","image":"1f235.png","sheet_x":4,"sheet_y":52,"short_name":"u6e80","short_names":["u6e80"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1551,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED CJK UNIFIED IDEOGRAPH-6709","unified":"1F236","non_qualified":null,"docomo":null,"au":null,"softbank":"E215","google":"FEB39","image":"1f236.png","sheet_x":4,"sheet_y":53,"short_name":"u6709","short_names":["u6709"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1538,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED CJK UNIFIED IDEOGRAPH-6708","unified":"1F237-FE0F","non_qualified":"1F237","docomo":null,"au":null,"softbank":"E217","google":"FEB3B","image":"1f237-fe0f.png","sheet_x":4,"sheet_y":54,"short_name":"u6708","short_names":["u6708"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1537,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED CJK UNIFIED IDEOGRAPH-7533","unified":"1F238","non_qualified":null,"docomo":null,"au":null,"softbank":"E218","google":"FEB3C","image":"1f238.png","sheet_x":4,"sheet_y":55,"short_name":"u7533","short_names":["u7533"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1545,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED CJK UNIFIED IDEOGRAPH-5272","unified":"1F239","non_qualified":null,"docomo":null,"au":"EA86","softbank":"E227","google":"FEB3E","image":"1f239.png","sheet_x":4,"sheet_y":56,"short_name":"u5272","short_names":["u5272"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1541,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED CJK UNIFIED IDEOGRAPH-55B6","unified":"1F23A","non_qualified":null,"docomo":null,"au":"EA8C","softbank":"E22D","google":"FEB41","image":"1f23a.png","sheet_x":4,"sheet_y":57,"short_name":"u55b6","short_names":["u55b6"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1550,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CIRCLED IDEOGRAPH ADVANTAGE","unified":"1F250","non_qualified":null,"docomo":null,"au":"E4F7","softbank":"E226","google":"FEB3D","image":"1f250.png","sheet_x":4,"sheet_y":58,"short_name":"ideograph_advantage","short_names":["ideograph_advantage"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1540,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CIRCLED IDEOGRAPH ACCEPT","unified":"1F251","non_qualified":null,"docomo":null,"au":"EB01","softbank":null,"google":"FEB50","image":"1f251.png","sheet_x":4,"sheet_y":59,"short_name":"accept","short_names":["accept"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1544,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CYCLONE","unified":"1F300","non_qualified":null,"docomo":"E643","au":"E469","softbank":"E443","google":"FE005","image":"1f300.png","sheet_x":4,"sheet_y":60,"short_name":"cyclone","short_names":["cyclone"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1010,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FOGGY","unified":"1F301","non_qualified":null,"docomo":"E644","au":"E598","softbank":null,"google":"FE006","image":"1f301.png","sheet_x":5,"sheet_y":0,"short_name":"foggy","short_names":["foggy"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-other","sort_order":857,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOSED UMBRELLA","unified":"1F302","non_qualified":null,"docomo":"E645","au":"EAE8","softbank":"E43C","google":"FE007","image":"1f302.png","sheet_x":5,"sheet_y":1,"short_name":"closed_umbrella","short_names":["closed_umbrella"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1012,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NIGHT WITH STARS","unified":"1F303","non_qualified":null,"docomo":"E6B3","au":"EAF1","softbank":"E44B","google":"FE008","image":"1f303.png","sheet_x":5,"sheet_y":2,"short_name":"night_with_stars","short_names":["night_with_stars"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-other","sort_order":858,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SUNRISE OVER MOUNTAINS","unified":"1F304","non_qualified":null,"docomo":"E63E","au":"EAF4","softbank":"E04D","google":"FE009","image":"1f304.png","sheet_x":5,"sheet_y":3,"short_name":"sunrise_over_mountains","short_names":["sunrise_over_mountains"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-other","sort_order":860,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SUNRISE","unified":"1F305","non_qualified":null,"docomo":"E63E","au":"EAF4","softbank":"E449","google":"FE00A","image":"1f305.png","sheet_x":5,"sheet_y":4,"short_name":"sunrise","short_names":["sunrise"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-other","sort_order":861,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CITYSCAPE AT DUSK","unified":"1F306","non_qualified":null,"docomo":null,"au":"E5DA","softbank":"E146","google":"FE00B","image":"1f306.png","sheet_x":5,"sheet_y":5,"short_name":"city_sunset","short_names":["city_sunset"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-other","sort_order":862,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SUNSET OVER BUILDINGS","unified":"1F307","non_qualified":null,"docomo":"E63E","au":"E5DA","softbank":"E44A","google":"FE00C","image":"1f307.png","sheet_x":5,"sheet_y":6,"short_name":"city_sunrise","short_names":["city_sunrise"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-other","sort_order":863,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RAINBOW","unified":"1F308","non_qualified":null,"docomo":null,"au":"EAF2","softbank":"E44C","google":"FE00D","image":"1f308.png","sheet_x":5,"sheet_y":7,"short_name":"rainbow","short_names":["rainbow"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1011,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BRIDGE AT NIGHT","unified":"1F309","non_qualified":null,"docomo":"E6B3","au":"E4BF","softbank":null,"google":"FE010","image":"1f309.png","sheet_x":5,"sheet_y":8,"short_name":"bridge_at_night","short_names":["bridge_at_night"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-other","sort_order":864,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WATER WAVE","unified":"1F30A","non_qualified":null,"docomo":"E73F","au":"EB7C","softbank":"E43E","google":"FE038","image":"1f30a.png","sheet_x":5,"sheet_y":9,"short_name":"ocean","short_names":["ocean"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1023,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"VOLCANO","unified":"1F30B","non_qualified":null,"docomo":null,"au":"EB53","softbank":null,"google":"FE03A","image":"1f30b.png","sheet_x":5,"sheet_y":10,"short_name":"volcano","short_names":["volcano"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-geographic","sort_order":815,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MILKY WAY","unified":"1F30C","non_qualified":null,"docomo":"E6B3","au":"EB5F","softbank":null,"google":"FE03B","image":"1f30c.png","sheet_x":5,"sheet_y":11,"short_name":"milky_way","short_names":["milky_way"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":997,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EARTH GLOBE EUROPE-AFRICA","unified":"1F30D","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f30d.png","sheet_x":5,"sheet_y":12,"short_name":"earth_africa","short_names":["earth_africa"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-map","sort_order":806,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EARTH GLOBE AMERICAS","unified":"1F30E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f30e.png","sheet_x":5,"sheet_y":13,"short_name":"earth_americas","short_names":["earth_americas"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-map","sort_order":807,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EARTH GLOBE ASIA-AUSTRALIA","unified":"1F30F","non_qualified":null,"docomo":null,"au":"E5B3","softbank":null,"google":"FE039","image":"1f30f.png","sheet_x":5,"sheet_y":14,"short_name":"earth_asia","short_names":["earth_asia"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-map","sort_order":808,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GLOBE WITH MERIDIANS","unified":"1F310","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f310.png","sheet_x":5,"sheet_y":15,"short_name":"globe_with_meridians","short_names":["globe_with_meridians"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-map","sort_order":809,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NEW MOON SYMBOL","unified":"1F311","non_qualified":null,"docomo":"E69C","au":"E5A8","softbank":null,"google":"FE011","image":"1f311.png","sheet_x":5,"sheet_y":16,"short_name":"new_moon","short_names":["new_moon"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":977,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WAXING CRESCENT MOON SYMBOL","unified":"1F312","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f312.png","sheet_x":5,"sheet_y":17,"short_name":"waxing_crescent_moon","short_names":["waxing_crescent_moon"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":978,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FIRST QUARTER MOON SYMBOL","unified":"1F313","non_qualified":null,"docomo":"E69E","au":"E5AA","softbank":null,"google":"FE013","image":"1f313.png","sheet_x":5,"sheet_y":18,"short_name":"first_quarter_moon","short_names":["first_quarter_moon"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":979,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WAXING GIBBOUS MOON SYMBOL","unified":"1F314","non_qualified":null,"docomo":"E69D","au":"E5A9","softbank":null,"google":"FE012","image":"1f314.png","sheet_x":5,"sheet_y":19,"short_name":"moon","short_names":["moon","waxing_gibbous_moon"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":980,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FULL MOON SYMBOL","unified":"1F315","non_qualified":null,"docomo":"E6A0","au":null,"softbank":null,"google":"FE015","image":"1f315.png","sheet_x":5,"sheet_y":20,"short_name":"full_moon","short_names":["full_moon"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":981,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WANING GIBBOUS MOON SYMBOL","unified":"1F316","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f316.png","sheet_x":5,"sheet_y":21,"short_name":"waning_gibbous_moon","short_names":["waning_gibbous_moon"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":982,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LAST QUARTER MOON SYMBOL","unified":"1F317","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f317.png","sheet_x":5,"sheet_y":22,"short_name":"last_quarter_moon","short_names":["last_quarter_moon"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":983,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WANING CRESCENT MOON SYMBOL","unified":"1F318","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f318.png","sheet_x":5,"sheet_y":23,"short_name":"waning_crescent_moon","short_names":["waning_crescent_moon"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":984,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CRESCENT MOON","unified":"1F319","non_qualified":null,"docomo":"E69F","au":"E486","softbank":"E04C","google":"FE014","image":"1f319.png","sheet_x":5,"sheet_y":24,"short_name":"crescent_moon","short_names":["crescent_moon"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":985,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NEW MOON WITH FACE","unified":"1F31A","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f31a.png","sheet_x":5,"sheet_y":25,"short_name":"new_moon_with_face","short_names":["new_moon_with_face"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":986,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FIRST QUARTER MOON WITH FACE","unified":"1F31B","non_qualified":null,"docomo":"E69E","au":"E489","softbank":null,"google":"FE016","image":"1f31b.png","sheet_x":5,"sheet_y":26,"short_name":"first_quarter_moon_with_face","short_names":["first_quarter_moon_with_face"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":987,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LAST QUARTER MOON WITH FACE","unified":"1F31C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f31c.png","sheet_x":5,"sheet_y":27,"short_name":"last_quarter_moon_with_face","short_names":["last_quarter_moon_with_face"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":988,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FULL MOON WITH FACE","unified":"1F31D","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f31d.png","sheet_x":5,"sheet_y":28,"short_name":"full_moon_with_face","short_names":["full_moon_with_face"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":991,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SUN WITH FACE","unified":"1F31E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f31e.png","sheet_x":5,"sheet_y":29,"short_name":"sun_with_face","short_names":["sun_with_face"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":992,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GLOWING STAR","unified":"1F31F","non_qualified":null,"docomo":null,"au":"E48B","softbank":"E335","google":"FEB69","image":"1f31f.png","sheet_x":5,"sheet_y":30,"short_name":"star2","short_names":["star2"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":995,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SHOOTING STAR","unified":"1F320","non_qualified":null,"docomo":null,"au":"E468","softbank":null,"google":"FEB6A","image":"1f320.png","sheet_x":5,"sheet_y":31,"short_name":"stars","short_names":["stars"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":996,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"THERMOMETER","unified":"1F321-FE0F","non_qualified":"1F321","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f321-fe0f.png","sheet_x":5,"sheet_y":32,"short_name":"thermometer","short_names":["thermometer"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":989,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SUN BEHIND SMALL CLOUD","unified":"1F324-FE0F","non_qualified":"1F324","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f324-fe0f.png","sheet_x":5,"sheet_y":33,"short_name":"mostly_sunny","short_names":["mostly_sunny","sun_small_cloud"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1001,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SUN BEHIND LARGE CLOUD","unified":"1F325-FE0F","non_qualified":"1F325","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f325-fe0f.png","sheet_x":5,"sheet_y":34,"short_name":"barely_sunny","short_names":["barely_sunny","sun_behind_cloud"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1002,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SUN BEHIND RAIN CLOUD","unified":"1F326-FE0F","non_qualified":"1F326","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f326-fe0f.png","sheet_x":5,"sheet_y":35,"short_name":"partly_sunny_rain","short_names":["partly_sunny_rain","sun_behind_rain_cloud"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1003,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOUD WITH RAIN","unified":"1F327-FE0F","non_qualified":"1F327","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f327-fe0f.png","sheet_x":5,"sheet_y":36,"short_name":"rain_cloud","short_names":["rain_cloud"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1004,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOUD WITH SNOW","unified":"1F328-FE0F","non_qualified":"1F328","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f328-fe0f.png","sheet_x":5,"sheet_y":37,"short_name":"snow_cloud","short_names":["snow_cloud"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1005,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOUD WITH LIGHTNING","unified":"1F329-FE0F","non_qualified":"1F329","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f329-fe0f.png","sheet_x":5,"sheet_y":38,"short_name":"lightning","short_names":["lightning","lightning_cloud"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1006,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TORNADO","unified":"1F32A-FE0F","non_qualified":"1F32A","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f32a-fe0f.png","sheet_x":5,"sheet_y":39,"short_name":"tornado","short_names":["tornado","tornado_cloud"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1007,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FOG","unified":"1F32B-FE0F","non_qualified":"1F32B","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f32b-fe0f.png","sheet_x":5,"sheet_y":40,"short_name":"fog","short_names":["fog"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1008,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WIND FACE","unified":"1F32C-FE0F","non_qualified":"1F32C","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f32c-fe0f.png","sheet_x":5,"sheet_y":41,"short_name":"wind_blowing_face","short_names":["wind_blowing_face"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1009,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HOT DOG","unified":"1F32D","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f32d.png","sheet_x":5,"sheet_y":42,"short_name":"hotdog","short_names":["hotdog"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":725,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TACO","unified":"1F32E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f32e.png","sheet_x":5,"sheet_y":43,"short_name":"taco","short_names":["taco"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":727,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BURRITO","unified":"1F32F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f32f.png","sheet_x":5,"sheet_y":44,"short_name":"burrito","short_names":["burrito"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":728,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CHESTNUT","unified":"1F330","non_qualified":null,"docomo":null,"au":"EB38","softbank":null,"google":"FE04C","image":"1f330.png","sheet_x":5,"sheet_y":45,"short_name":"chestnut","short_names":["chestnut"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-vegetable","sort_order":708,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SEEDLING","unified":"1F331","non_qualified":null,"docomo":"E746","au":"EB7D","softbank":null,"google":"FE03E","image":"1f331.png","sheet_x":5,"sheet_y":46,"short_name":"seedling","short_names":["seedling"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-other","sort_order":659,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EVERGREEN TREE","unified":"1F332","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f332.png","sheet_x":5,"sheet_y":47,"short_name":"evergreen_tree","short_names":["evergreen_tree"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-other","sort_order":661,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DECIDUOUS TREE","unified":"1F333","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f333.png","sheet_x":5,"sheet_y":48,"short_name":"deciduous_tree","short_names":["deciduous_tree"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-other","sort_order":662,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PALM TREE","unified":"1F334","non_qualified":null,"docomo":null,"au":"E4E2","softbank":"E307","google":"FE047","image":"1f334.png","sheet_x":5,"sheet_y":49,"short_name":"palm_tree","short_names":["palm_tree"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-other","sort_order":663,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CACTUS","unified":"1F335","non_qualified":null,"docomo":null,"au":"EA96","softbank":"E308","google":"FE048","image":"1f335.png","sheet_x":5,"sheet_y":50,"short_name":"cactus","short_names":["cactus"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-other","sort_order":664,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HOT PEPPER","unified":"1F336-FE0F","non_qualified":"1F336","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f336-fe0f.png","sheet_x":5,"sheet_y":51,"short_name":"hot_pepper","short_names":["hot_pepper"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-vegetable","sort_order":698,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TULIP","unified":"1F337","non_qualified":null,"docomo":"E743","au":"E4E4","softbank":"E304","google":"FE03D","image":"1f337.png","sheet_x":5,"sheet_y":52,"short_name":"tulip","short_names":["tulip"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-flower","sort_order":658,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CHERRY BLOSSOM","unified":"1F338","non_qualified":null,"docomo":"E748","au":"E4CA","softbank":"E030","google":"FE040","image":"1f338.png","sheet_x":5,"sheet_y":53,"short_name":"cherry_blossom","short_names":["cherry_blossom"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-flower","sort_order":649,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ROSE","unified":"1F339","non_qualified":null,"docomo":null,"au":"E5BA","softbank":"E032","google":"FE041","image":"1f339.png","sheet_x":5,"sheet_y":54,"short_name":"rose","short_names":["rose"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-flower","sort_order":653,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HIBISCUS","unified":"1F33A","non_qualified":null,"docomo":null,"au":"EA94","softbank":"E303","google":"FE045","image":"1f33a.png","sheet_x":5,"sheet_y":55,"short_name":"hibiscus","short_names":["hibiscus"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-flower","sort_order":655,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SUNFLOWER","unified":"1F33B","non_qualified":null,"docomo":null,"au":"E4E3","softbank":"E305","google":"FE046","image":"1f33b.png","sheet_x":5,"sheet_y":56,"short_name":"sunflower","short_names":["sunflower"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-flower","sort_order":656,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLOSSOM","unified":"1F33C","non_qualified":null,"docomo":null,"au":"EB49","softbank":null,"google":"FE04D","image":"1f33c.png","sheet_x":5,"sheet_y":57,"short_name":"blossom","short_names":["blossom"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-flower","sort_order":657,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EAR OF MAIZE","unified":"1F33D","non_qualified":null,"docomo":null,"au":"EB36","softbank":null,"google":"FE04A","image":"1f33d.png","sheet_x":5,"sheet_y":58,"short_name":"corn","short_names":["corn"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-vegetable","sort_order":697,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EAR OF RICE","unified":"1F33E","non_qualified":null,"docomo":null,"au":null,"softbank":"E444","google":"FE049","image":"1f33e.png","sheet_x":5,"sheet_y":59,"short_name":"ear_of_rice","short_names":["ear_of_rice"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-other","sort_order":665,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HERB","unified":"1F33F","non_qualified":null,"docomo":"E741","au":"EB82","softbank":null,"google":"FE04E","image":"1f33f.png","sheet_x":5,"sheet_y":60,"short_name":"herb","short_names":["herb"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-other","sort_order":666,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FOUR LEAF CLOVER","unified":"1F340","non_qualified":null,"docomo":"E741","au":"E513","softbank":"E110","google":"FE03C","image":"1f340.png","sheet_x":6,"sheet_y":0,"short_name":"four_leaf_clover","short_names":["four_leaf_clover"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-other","sort_order":668,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MAPLE LEAF","unified":"1F341","non_qualified":null,"docomo":"E747","au":"E4CE","softbank":"E118","google":"FE03F","image":"1f341.png","sheet_x":6,"sheet_y":1,"short_name":"maple_leaf","short_names":["maple_leaf"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-other","sort_order":669,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FALLEN LEAF","unified":"1F342","non_qualified":null,"docomo":"E747","au":"E5CD","softbank":"E119","google":"FE042","image":"1f342.png","sheet_x":6,"sheet_y":2,"short_name":"fallen_leaf","short_names":["fallen_leaf"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-other","sort_order":670,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LEAF FLUTTERING IN WIND","unified":"1F343","non_qualified":null,"docomo":null,"au":"E5CD","softbank":"E447","google":"FE043","image":"1f343.png","sheet_x":6,"sheet_y":3,"short_name":"leaves","short_names":["leaves"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-other","sort_order":671,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MUSHROOM","unified":"1F344","non_qualified":null,"docomo":null,"au":"EB37","softbank":null,"google":"FE04B","image":"1f344.png","sheet_x":6,"sheet_y":4,"short_name":"mushroom","short_names":["mushroom"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-vegetable","sort_order":705,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TOMATO","unified":"1F345","non_qualified":null,"docomo":null,"au":"EABB","softbank":"E349","google":"FE055","image":"1f345.png","sheet_x":6,"sheet_y":5,"short_name":"tomato","short_names":["tomato"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-fruit","sort_order":690,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"AUBERGINE","unified":"1F346","non_qualified":null,"docomo":null,"au":"EABC","softbank":"E34A","google":"FE056","image":"1f346.png","sheet_x":6,"sheet_y":6,"short_name":"eggplant","short_names":["eggplant"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-vegetable","sort_order":694,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GRAPES","unified":"1F347","non_qualified":null,"docomo":null,"au":"EB34","softbank":null,"google":"FE059","image":"1f347.png","sheet_x":6,"sheet_y":7,"short_name":"grapes","short_names":["grapes"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-fruit","sort_order":674,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MELON","unified":"1F348","non_qualified":null,"docomo":null,"au":"EB32","softbank":null,"google":"FE057","image":"1f348.png","sheet_x":6,"sheet_y":8,"short_name":"melon","short_names":["melon"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-fruit","sort_order":675,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WATERMELON","unified":"1F349","non_qualified":null,"docomo":null,"au":"E4CD","softbank":"E348","google":"FE054","image":"1f349.png","sheet_x":6,"sheet_y":9,"short_name":"watermelon","short_names":["watermelon"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-fruit","sort_order":676,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TANGERINE","unified":"1F34A","non_qualified":null,"docomo":null,"au":"EABA","softbank":"E346","google":"FE052","image":"1f34a.png","sheet_x":6,"sheet_y":10,"short_name":"tangerine","short_names":["tangerine"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-fruit","sort_order":677,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LEMON","unified":"1F34B","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f34b.png","sheet_x":6,"sheet_y":11,"short_name":"lemon","short_names":["lemon"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-fruit","sort_order":678,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BANANA","unified":"1F34C","non_qualified":null,"docomo":"E744","au":"EB35","softbank":null,"google":"FE050","image":"1f34c.png","sheet_x":6,"sheet_y":12,"short_name":"banana","short_names":["banana"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-fruit","sort_order":679,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PINEAPPLE","unified":"1F34D","non_qualified":null,"docomo":null,"au":"EB33","softbank":null,"google":"FE058","image":"1f34d.png","sheet_x":6,"sheet_y":13,"short_name":"pineapple","short_names":["pineapple"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-fruit","sort_order":680,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RED APPLE","unified":"1F34E","non_qualified":null,"docomo":"E745","au":"EAB9","softbank":"E345","google":"FE051","image":"1f34e.png","sheet_x":6,"sheet_y":14,"short_name":"apple","short_names":["apple"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-fruit","sort_order":682,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GREEN APPLE","unified":"1F34F","non_qualified":null,"docomo":"E745","au":"EB5A","softbank":null,"google":"FE05B","image":"1f34f.png","sheet_x":6,"sheet_y":15,"short_name":"green_apple","short_names":["green_apple"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-fruit","sort_order":683,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PEAR","unified":"1F350","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f350.png","sheet_x":6,"sheet_y":16,"short_name":"pear","short_names":["pear"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-fruit","sort_order":684,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PEACH","unified":"1F351","non_qualified":null,"docomo":null,"au":"EB39","softbank":null,"google":"FE05A","image":"1f351.png","sheet_x":6,"sheet_y":17,"short_name":"peach","short_names":["peach"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-fruit","sort_order":685,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CHERRIES","unified":"1F352","non_qualified":null,"docomo":"E742","au":"E4D2","softbank":null,"google":"FE04F","image":"1f352.png","sheet_x":6,"sheet_y":18,"short_name":"cherries","short_names":["cherries"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-fruit","sort_order":686,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"STRAWBERRY","unified":"1F353","non_qualified":null,"docomo":null,"au":"E4D4","softbank":"E347","google":"FE053","image":"1f353.png","sheet_x":6,"sheet_y":19,"short_name":"strawberry","short_names":["strawberry"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-fruit","sort_order":687,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HAMBURGER","unified":"1F354","non_qualified":null,"docomo":"E673","au":"E4D6","softbank":"E120","google":"FE960","image":"1f354.png","sheet_x":6,"sheet_y":20,"short_name":"hamburger","short_names":["hamburger"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":722,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SLICE OF PIZZA","unified":"1F355","non_qualified":null,"docomo":null,"au":"EB3B","softbank":null,"google":"FE975","image":"1f355.png","sheet_x":6,"sheet_y":21,"short_name":"pizza","short_names":["pizza"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":724,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MEAT ON BONE","unified":"1F356","non_qualified":null,"docomo":null,"au":"E4C4","softbank":null,"google":"FE972","image":"1f356.png","sheet_x":6,"sheet_y":22,"short_name":"meat_on_bone","short_names":["meat_on_bone"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":718,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"POULTRY LEG","unified":"1F357","non_qualified":null,"docomo":null,"au":"EB3C","softbank":null,"google":"FE976","image":"1f357.png","sheet_x":6,"sheet_y":23,"short_name":"poultry_leg","short_names":["poultry_leg"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":719,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RICE CRACKER","unified":"1F358","non_qualified":null,"docomo":null,"au":"EAB3","softbank":"E33D","google":"FE969","image":"1f358.png","sheet_x":6,"sheet_y":24,"short_name":"rice_cracker","short_names":["rice_cracker"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-asian","sort_order":744,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RICE BALL","unified":"1F359","non_qualified":null,"docomo":"E749","au":"E4D5","softbank":"E342","google":"FE961","image":"1f359.png","sheet_x":6,"sheet_y":25,"short_name":"rice_ball","short_names":["rice_ball"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-asian","sort_order":745,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"COOKED RICE","unified":"1F35A","non_qualified":null,"docomo":"E74C","au":"EAB4","softbank":"E33E","google":"FE96A","image":"1f35a.png","sheet_x":6,"sheet_y":26,"short_name":"rice","short_names":["rice"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-asian","sort_order":746,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CURRY AND RICE","unified":"1F35B","non_qualified":null,"docomo":null,"au":"EAB6","softbank":"E341","google":"FE96C","image":"1f35b.png","sheet_x":6,"sheet_y":27,"short_name":"curry","short_names":["curry"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-asian","sort_order":747,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"STEAMING BOWL","unified":"1F35C","non_qualified":null,"docomo":"E74C","au":"E5B4","softbank":"E340","google":"FE963","image":"1f35c.png","sheet_x":6,"sheet_y":28,"short_name":"ramen","short_names":["ramen"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-asian","sort_order":748,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SPAGHETTI","unified":"1F35D","non_qualified":null,"docomo":null,"au":"EAB5","softbank":"E33F","google":"FE96B","image":"1f35d.png","sheet_x":6,"sheet_y":29,"short_name":"spaghetti","short_names":["spaghetti"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-asian","sort_order":749,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BREAD","unified":"1F35E","non_qualified":null,"docomo":"E74D","au":"EAAF","softbank":"E339","google":"FE964","image":"1f35e.png","sheet_x":6,"sheet_y":30,"short_name":"bread","short_names":["bread"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":709,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FRENCH FRIES","unified":"1F35F","non_qualified":null,"docomo":null,"au":"EAB1","softbank":"E33B","google":"FE967","image":"1f35f.png","sheet_x":6,"sheet_y":31,"short_name":"fries","short_names":["fries"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":723,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ROASTED SWEET POTATO","unified":"1F360","non_qualified":null,"docomo":null,"au":"EB3A","softbank":null,"google":"FE974","image":"1f360.png","sheet_x":6,"sheet_y":32,"short_name":"sweet_potato","short_names":["sweet_potato"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-asian","sort_order":750,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DANGO","unified":"1F361","non_qualified":null,"docomo":null,"au":"EAB2","softbank":"E33C","google":"FE968","image":"1f361.png","sheet_x":6,"sheet_y":33,"short_name":"dango","short_names":["dango"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-asian","sort_order":756,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ODEN","unified":"1F362","non_qualified":null,"docomo":null,"au":"EAB7","softbank":"E343","google":"FE96D","image":"1f362.png","sheet_x":6,"sheet_y":34,"short_name":"oden","short_names":["oden"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-asian","sort_order":751,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SUSHI","unified":"1F363","non_qualified":null,"docomo":null,"au":"EAB8","softbank":"E344","google":"FE96E","image":"1f363.png","sheet_x":6,"sheet_y":35,"short_name":"sushi","short_names":["sushi"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-asian","sort_order":752,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FRIED SHRIMP","unified":"1F364","non_qualified":null,"docomo":null,"au":"EB70","softbank":null,"google":"FE97F","image":"1f364.png","sheet_x":6,"sheet_y":36,"short_name":"fried_shrimp","short_names":["fried_shrimp"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-asian","sort_order":753,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FISH CAKE WITH SWIRL DESIGN","unified":"1F365","non_qualified":null,"docomo":"E643","au":"E4ED","softbank":null,"google":"FE973","image":"1f365.png","sheet_x":6,"sheet_y":37,"short_name":"fish_cake","short_names":["fish_cake"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-asian","sort_order":754,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SOFT ICE CREAM","unified":"1F366","non_qualified":null,"docomo":null,"au":"EAB0","softbank":"E33A","google":"FE966","image":"1f366.png","sheet_x":6,"sheet_y":38,"short_name":"icecream","short_names":["icecream"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-sweet","sort_order":765,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SHAVED ICE","unified":"1F367","non_qualified":null,"docomo":null,"au":"EAEA","softbank":"E43F","google":"FE971","image":"1f367.png","sheet_x":6,"sheet_y":39,"short_name":"shaved_ice","short_names":["shaved_ice"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-sweet","sort_order":766,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ICE CREAM","unified":"1F368","non_qualified":null,"docomo":null,"au":"EB4A","softbank":null,"google":"FE977","image":"1f368.png","sheet_x":6,"sheet_y":40,"short_name":"ice_cream","short_names":["ice_cream"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-sweet","sort_order":767,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DOUGHNUT","unified":"1F369","non_qualified":null,"docomo":null,"au":"EB4B","softbank":null,"google":"FE978","image":"1f369.png","sheet_x":6,"sheet_y":41,"short_name":"doughnut","short_names":["doughnut"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-sweet","sort_order":768,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"COOKIE","unified":"1F36A","non_qualified":null,"docomo":null,"au":"EB4C","softbank":null,"google":"FE979","image":"1f36a.png","sheet_x":6,"sheet_y":42,"short_name":"cookie","short_names":["cookie"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-sweet","sort_order":769,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CHOCOLATE BAR","unified":"1F36B","non_qualified":null,"docomo":null,"au":"EB4D","softbank":null,"google":"FE97A","image":"1f36b.png","sheet_x":6,"sheet_y":43,"short_name":"chocolate_bar","short_names":["chocolate_bar"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-sweet","sort_order":774,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CANDY","unified":"1F36C","non_qualified":null,"docomo":null,"au":"EB4E","softbank":null,"google":"FE97B","image":"1f36c.png","sheet_x":6,"sheet_y":44,"short_name":"candy","short_names":["candy"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-sweet","sort_order":775,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LOLLIPOP","unified":"1F36D","non_qualified":null,"docomo":null,"au":"EB4F","softbank":null,"google":"FE97C","image":"1f36d.png","sheet_x":6,"sheet_y":45,"short_name":"lollipop","short_names":["lollipop"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-sweet","sort_order":776,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CUSTARD","unified":"1F36E","non_qualified":null,"docomo":null,"au":"EB56","softbank":null,"google":"FE97D","image":"1f36e.png","sheet_x":6,"sheet_y":46,"short_name":"custard","short_names":["custard"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-sweet","sort_order":777,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HONEY POT","unified":"1F36F","non_qualified":null,"docomo":null,"au":"EB59","softbank":null,"google":"FE97E","image":"1f36f.png","sheet_x":6,"sheet_y":47,"short_name":"honey_pot","short_names":["honey_pot"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-sweet","sort_order":778,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SHORTCAKE","unified":"1F370","non_qualified":null,"docomo":"E74A","au":"E4D0","softbank":"E046","google":"FE962","image":"1f370.png","sheet_x":6,"sheet_y":48,"short_name":"cake","short_names":["cake"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-sweet","sort_order":771,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BENTO BOX","unified":"1F371","non_qualified":null,"docomo":null,"au":"EABD","softbank":"E34C","google":"FE96F","image":"1f371.png","sheet_x":6,"sheet_y":49,"short_name":"bento","short_names":["bento"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-asian","sort_order":743,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"POT OF FOOD","unified":"1F372","non_qualified":null,"docomo":null,"au":"EABE","softbank":"E34D","google":"FE970","image":"1f372.png","sheet_x":6,"sheet_y":50,"short_name":"stew","short_names":["stew"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":735,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"COOKING","unified":"1F373","non_qualified":null,"docomo":null,"au":"E4D1","softbank":"E147","google":"FE965","image":"1f373.png","sheet_x":6,"sheet_y":51,"short_name":"fried_egg","short_names":["fried_egg","cooking"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":733,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FORK AND KNIFE","unified":"1F374","non_qualified":null,"docomo":"E66F","au":"E4AC","softbank":"E043","google":"FE980","image":"1f374.png","sheet_x":6,"sheet_y":52,"short_name":"fork_and_knife","short_names":["fork_and_knife"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"dishware","sort_order":801,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TEACUP WITHOUT HANDLE","unified":"1F375","non_qualified":null,"docomo":"E71E","au":"EAAE","softbank":"E338","google":"FE984","image":"1f375.png","sheet_x":6,"sheet_y":53,"short_name":"tea","short_names":["tea"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"drink","sort_order":783,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SAKE BOTTLE AND CUP","unified":"1F376","non_qualified":null,"docomo":"E74B","au":"EA97","softbank":"E30B","google":"FE985","image":"1f376.png","sheet_x":6,"sheet_y":54,"short_name":"sake","short_names":["sake"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"drink","sort_order":784,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WINE GLASS","unified":"1F377","non_qualified":null,"docomo":"E756","au":"E4C1","softbank":null,"google":"FE986","image":"1f377.png","sheet_x":6,"sheet_y":55,"short_name":"wine_glass","short_names":["wine_glass"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"drink","sort_order":786,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"COCKTAIL GLASS","unified":"1F378","non_qualified":null,"docomo":"E671","au":"E4C2","softbank":"E044","google":"FE982","image":"1f378.png","sheet_x":6,"sheet_y":56,"short_name":"cocktail","short_names":["cocktail"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"drink","sort_order":787,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TROPICAL DRINK","unified":"1F379","non_qualified":null,"docomo":"E671","au":"EB3E","softbank":null,"google":"FE988","image":"1f379.png","sheet_x":6,"sheet_y":57,"short_name":"tropical_drink","short_names":["tropical_drink"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"drink","sort_order":788,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BEER MUG","unified":"1F37A","non_qualified":null,"docomo":"E672","au":"E4C3","softbank":"E047","google":"FE983","image":"1f37a.png","sheet_x":6,"sheet_y":58,"short_name":"beer","short_names":["beer"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"drink","sort_order":789,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLINKING BEER MUGS","unified":"1F37B","non_qualified":null,"docomo":"E672","au":"EA98","softbank":"E30C","google":"FE987","image":"1f37b.png","sheet_x":6,"sheet_y":59,"short_name":"beers","short_names":["beers"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"drink","sort_order":790,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BABY BOTTLE","unified":"1F37C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f37c.png","sheet_x":6,"sheet_y":60,"short_name":"baby_bottle","short_names":["baby_bottle"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"drink","sort_order":779,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FORK AND KNIFE WITH PLATE","unified":"1F37D-FE0F","non_qualified":"1F37D","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f37d-fe0f.png","sheet_x":7,"sheet_y":0,"short_name":"knife_fork_plate","short_names":["knife_fork_plate"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"dishware","sort_order":800,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BOTTLE WITH POPPING CORK","unified":"1F37E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f37e.png","sheet_x":7,"sheet_y":1,"short_name":"champagne","short_names":["champagne"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"drink","sort_order":785,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"POPCORN","unified":"1F37F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f37f.png","sheet_x":7,"sheet_y":2,"short_name":"popcorn","short_names":["popcorn"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":739,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RIBBON","unified":"1F380","non_qualified":null,"docomo":"E684","au":"E59F","softbank":"E314","google":"FE50F","image":"1f380.png","sheet_x":7,"sheet_y":3,"short_name":"ribbon","short_names":["ribbon"],"text":null,"texts":null,"category":"Activities","subcategory":"event","sort_order":1040,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WRAPPED PRESENT","unified":"1F381","non_qualified":null,"docomo":"E685","au":"E4CF","softbank":"E112","google":"FE510","image":"1f381.png","sheet_x":7,"sheet_y":4,"short_name":"gift","short_names":["gift"],"text":null,"texts":null,"category":"Activities","subcategory":"event","sort_order":1041,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BIRTHDAY CAKE","unified":"1F382","non_qualified":null,"docomo":"E686","au":"E5A0","softbank":"E34B","google":"FE511","image":"1f382.png","sheet_x":7,"sheet_y":5,"short_name":"birthday","short_names":["birthday"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-sweet","sort_order":770,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"JACK-O-LANTERN","unified":"1F383","non_qualified":null,"docomo":null,"au":"EAEE","softbank":"E445","google":"FE51F","image":"1f383.png","sheet_x":7,"sheet_y":6,"short_name":"jack_o_lantern","short_names":["jack_o_lantern"],"text":null,"texts":null,"category":"Activities","subcategory":"event","sort_order":1024,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CHRISTMAS TREE","unified":"1F384","non_qualified":null,"docomo":"E6A4","au":"E4C9","softbank":"E033","google":"FE512","image":"1f384.png","sheet_x":7,"sheet_y":7,"short_name":"christmas_tree","short_names":["christmas_tree"],"text":null,"texts":null,"category":"Activities","subcategory":"event","sort_order":1025,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FATHER CHRISTMAS","unified":"1F385","non_qualified":null,"docomo":null,"au":"EAF0","softbank":"E448","google":"FE513","image":"1f385.png","sheet_x":7,"sheet_y":8,"short_name":"santa","short_names":["santa"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":364,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F385-1F3FB","non_qualified":null,"image":"1f385-1f3fb.png","sheet_x":7,"sheet_y":9,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F385-1F3FC","non_qualified":null,"image":"1f385-1f3fc.png","sheet_x":7,"sheet_y":10,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F385-1F3FD","non_qualified":null,"image":"1f385-1f3fd.png","sheet_x":7,"sheet_y":11,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F385-1F3FE","non_qualified":null,"image":"1f385-1f3fe.png","sheet_x":7,"sheet_y":12,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F385-1F3FF","non_qualified":null,"image":"1f385-1f3ff.png","sheet_x":7,"sheet_y":13,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"FIREWORKS","unified":"1F386","non_qualified":null,"docomo":null,"au":"E5CC","softbank":"E117","google":"FE515","image":"1f386.png","sheet_x":7,"sheet_y":14,"short_name":"fireworks","short_names":["fireworks"],"text":null,"texts":null,"category":"Activities","subcategory":"event","sort_order":1026,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FIREWORK SPARKLER","unified":"1F387","non_qualified":null,"docomo":null,"au":"EAEB","softbank":"E440","google":"FE51D","image":"1f387.png","sheet_x":7,"sheet_y":15,"short_name":"sparkler","short_names":["sparkler"],"text":null,"texts":null,"category":"Activities","subcategory":"event","sort_order":1027,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BALLOON","unified":"1F388","non_qualified":null,"docomo":null,"au":"EA9B","softbank":"E310","google":"FE516","image":"1f388.png","sheet_x":7,"sheet_y":16,"short_name":"balloon","short_names":["balloon"],"text":null,"texts":null,"category":"Activities","subcategory":"event","sort_order":1030,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PARTY POPPER","unified":"1F389","non_qualified":null,"docomo":null,"au":"EA9C","softbank":"E312","google":"FE517","image":"1f389.png","sheet_x":7,"sheet_y":17,"short_name":"tada","short_names":["tada"],"text":null,"texts":null,"category":"Activities","subcategory":"event","sort_order":1031,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CONFETTI BALL","unified":"1F38A","non_qualified":null,"docomo":null,"au":"E46F","softbank":null,"google":"FE520","image":"1f38a.png","sheet_x":7,"sheet_y":18,"short_name":"confetti_ball","short_names":["confetti_ball"],"text":null,"texts":null,"category":"Activities","subcategory":"event","sort_order":1032,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TANABATA TREE","unified":"1F38B","non_qualified":null,"docomo":null,"au":"EB3D","softbank":null,"google":"FE521","image":"1f38b.png","sheet_x":7,"sheet_y":19,"short_name":"tanabata_tree","short_names":["tanabata_tree"],"text":null,"texts":null,"category":"Activities","subcategory":"event","sort_order":1033,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CROSSED FLAGS","unified":"1F38C","non_qualified":null,"docomo":null,"au":"E5D9","softbank":"E143","google":"FE514","image":"1f38c.png","sheet_x":7,"sheet_y":20,"short_name":"crossed_flags","short_names":["crossed_flags"],"text":null,"texts":null,"category":"Flags","subcategory":"flag","sort_order":1588,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PINE DECORATION","unified":"1F38D","non_qualified":null,"docomo":null,"au":"EAE3","softbank":"E436","google":"FE518","image":"1f38d.png","sheet_x":7,"sheet_y":21,"short_name":"bamboo","short_names":["bamboo"],"text":null,"texts":null,"category":"Activities","subcategory":"event","sort_order":1034,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"JAPANESE DOLLS","unified":"1F38E","non_qualified":null,"docomo":null,"au":"EAE4","softbank":"E438","google":"FE519","image":"1f38e.png","sheet_x":7,"sheet_y":22,"short_name":"dolls","short_names":["dolls"],"text":null,"texts":null,"category":"Activities","subcategory":"event","sort_order":1035,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CARP STREAMER","unified":"1F38F","non_qualified":null,"docomo":null,"au":"EAE7","softbank":"E43B","google":"FE51C","image":"1f38f.png","sheet_x":7,"sheet_y":23,"short_name":"flags","short_names":["flags"],"text":null,"texts":null,"category":"Activities","subcategory":"event","sort_order":1036,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WIND CHIME","unified":"1F390","non_qualified":null,"docomo":null,"au":"EAED","softbank":"E442","google":"FE51E","image":"1f390.png","sheet_x":7,"sheet_y":24,"short_name":"wind_chime","short_names":["wind_chime"],"text":null,"texts":null,"category":"Activities","subcategory":"event","sort_order":1037,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MOON VIEWING CEREMONY","unified":"1F391","non_qualified":null,"docomo":null,"au":"EAEF","softbank":"E446","google":"FE017","image":"1f391.png","sheet_x":7,"sheet_y":25,"short_name":"rice_scene","short_names":["rice_scene"],"text":null,"texts":null,"category":"Activities","subcategory":"event","sort_order":1038,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SCHOOL SATCHEL","unified":"1F392","non_qualified":null,"docomo":null,"au":"EAE6","softbank":"E43A","google":"FE51B","image":"1f392.png","sheet_x":7,"sheet_y":26,"short_name":"school_satchel","short_names":["school_satchel"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1134,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GRADUATION CAP","unified":"1F393","non_qualified":null,"docomo":null,"au":"EAE5","softbank":"E439","google":"FE51A","image":"1f393.png","sheet_x":7,"sheet_y":27,"short_name":"mortar_board","short_names":["mortar_board"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1147,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MILITARY MEDAL","unified":"1F396-FE0F","non_qualified":"1F396","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f396-fe0f.png","sheet_x":7,"sheet_y":28,"short_name":"medal","short_names":["medal"],"text":null,"texts":null,"category":"Activities","subcategory":"award-medal","sort_order":1045,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"REMINDER RIBBON","unified":"1F397-FE0F","non_qualified":"1F397","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f397-fe0f.png","sheet_x":7,"sheet_y":29,"short_name":"reminder_ribbon","short_names":["reminder_ribbon"],"text":null,"texts":null,"category":"Activities","subcategory":"event","sort_order":1042,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"STUDIO MICROPHONE","unified":"1F399-FE0F","non_qualified":"1F399","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f399-fe0f.png","sheet_x":7,"sheet_y":30,"short_name":"studio_microphone","short_names":["studio_microphone"],"text":null,"texts":null,"category":"Objects","subcategory":"music","sort_order":1167,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LEVEL SLIDER","unified":"1F39A-FE0F","non_qualified":"1F39A","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f39a-fe0f.png","sheet_x":7,"sheet_y":31,"short_name":"level_slider","short_names":["level_slider"],"text":null,"texts":null,"category":"Objects","subcategory":"music","sort_order":1168,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CONTROL KNOBS","unified":"1F39B-FE0F","non_qualified":"1F39B","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f39b-fe0f.png","sheet_x":7,"sheet_y":32,"short_name":"control_knobs","short_names":["control_knobs"],"text":null,"texts":null,"category":"Objects","subcategory":"music","sort_order":1169,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FILM FRAMES","unified":"1F39E-FE0F","non_qualified":"1F39E","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f39e-fe0f.png","sheet_x":7,"sheet_y":33,"short_name":"film_frames","short_names":["film_frames"],"text":null,"texts":null,"category":"Objects","subcategory":"light & video","sort_order":1203,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ADMISSION TICKETS","unified":"1F39F-FE0F","non_qualified":"1F39F","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f39f-fe0f.png","sheet_x":7,"sheet_y":34,"short_name":"admission_tickets","short_names":["admission_tickets"],"text":null,"texts":null,"category":"Activities","subcategory":"event","sort_order":1043,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CAROUSEL HORSE","unified":"1F3A0","non_qualified":null,"docomo":"E679","au":null,"softbank":null,"google":"FE7FC","image":"1f3a0.png","sheet_x":7,"sheet_y":35,"short_name":"carousel_horse","short_names":["carousel_horse"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-other","sort_order":866,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FERRIS WHEEL","unified":"1F3A1","non_qualified":null,"docomo":null,"au":"E46D","softbank":"E124","google":"FE7FD","image":"1f3a1.png","sheet_x":7,"sheet_y":36,"short_name":"ferris_wheel","short_names":["ferris_wheel"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-other","sort_order":868,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ROLLER COASTER","unified":"1F3A2","non_qualified":null,"docomo":null,"au":"EAE2","softbank":"E433","google":"FE7FE","image":"1f3a2.png","sheet_x":7,"sheet_y":37,"short_name":"roller_coaster","short_names":["roller_coaster"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-other","sort_order":869,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FISHING POLE AND FISH","unified":"1F3A3","non_qualified":null,"docomo":"E751","au":"EB42","softbank":null,"google":"FE7FF","image":"1f3a3.png","sheet_x":7,"sheet_y":38,"short_name":"fishing_pole_and_fish","short_names":["fishing_pole_and_fish"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1072,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MICROPHONE","unified":"1F3A4","non_qualified":null,"docomo":"E676","au":"E503","softbank":"E03C","google":"FE800","image":"1f3a4.png","sheet_x":7,"sheet_y":39,"short_name":"microphone","short_names":["microphone"],"text":null,"texts":null,"category":"Objects","subcategory":"music","sort_order":1170,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MOVIE CAMERA","unified":"1F3A5","non_qualified":null,"docomo":"E677","au":"E517","softbank":"E03D","google":"FE801","image":"1f3a5.png","sheet_x":7,"sheet_y":40,"short_name":"movie_camera","short_names":["movie_camera"],"text":null,"texts":null,"category":"Objects","subcategory":"light & video","sort_order":1202,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CINEMA","unified":"1F3A6","non_qualified":null,"docomo":"E677","au":"E517","softbank":"E507","google":"FE802","image":"1f3a6.png","sheet_x":7,"sheet_y":41,"short_name":"cinema","short_names":["cinema"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1455,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HEADPHONE","unified":"1F3A7","non_qualified":null,"docomo":"E67A","au":"E508","softbank":"E30A","google":"FE803","image":"1f3a7.png","sheet_x":7,"sheet_y":42,"short_name":"headphones","short_names":["headphones"],"text":null,"texts":null,"category":"Objects","subcategory":"music","sort_order":1171,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ARTIST PALETTE","unified":"1F3A8","non_qualified":null,"docomo":"E67B","au":"E59C","softbank":"E502","google":"FE804","image":"1f3a8.png","sheet_x":7,"sheet_y":43,"short_name":"art","short_names":["art"],"text":null,"texts":null,"category":"Activities","subcategory":"arts & crafts","sort_order":1105,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TOP HAT","unified":"1F3A9","non_qualified":null,"docomo":"E67C","au":"EAF5","softbank":"E503","google":"FE805","image":"1f3a9.png","sheet_x":7,"sheet_y":44,"short_name":"tophat","short_names":["tophat"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1146,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CIRCUS TENT","unified":"1F3AA","non_qualified":null,"docomo":"E67D","au":"E59E","softbank":null,"google":"FE806","image":"1f3aa.png","sheet_x":7,"sheet_y":45,"short_name":"circus_tent","short_names":["circus_tent"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-other","sort_order":871,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TICKET","unified":"1F3AB","non_qualified":null,"docomo":"E67E","au":"E49E","softbank":"E125","google":"FE807","image":"1f3ab.png","sheet_x":7,"sheet_y":46,"short_name":"ticket","short_names":["ticket"],"text":null,"texts":null,"category":"Activities","subcategory":"event","sort_order":1044,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLAPPER BOARD","unified":"1F3AC","non_qualified":null,"docomo":"E6AC","au":"E4BE","softbank":"E324","google":"FE808","image":"1f3ac.png","sheet_x":7,"sheet_y":47,"short_name":"clapper","short_names":["clapper"],"text":null,"texts":null,"category":"Objects","subcategory":"light & video","sort_order":1205,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PERFORMING ARTS","unified":"1F3AD","non_qualified":null,"docomo":null,"au":"E59D","softbank":null,"google":"FE809","image":"1f3ad.png","sheet_x":7,"sheet_y":48,"short_name":"performing_arts","short_names":["performing_arts"],"text":null,"texts":null,"category":"Activities","subcategory":"arts & crafts","sort_order":1103,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"VIDEO GAME","unified":"1F3AE","non_qualified":null,"docomo":"E68B","au":"E4C6","softbank":null,"google":"FE80A","image":"1f3ae.png","sheet_x":7,"sheet_y":49,"short_name":"video_game","short_names":["video_game"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1086,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DIRECT HIT","unified":"1F3AF","non_qualified":null,"docomo":null,"au":"E4C5","softbank":"E130","google":"FE80C","image":"1f3af.png","sheet_x":7,"sheet_y":50,"short_name":"dart","short_names":["dart"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1078,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SLOT MACHINE","unified":"1F3B0","non_qualified":null,"docomo":null,"au":"E46E","softbank":"E133","google":"FE80D","image":"1f3b0.png","sheet_x":7,"sheet_y":51,"short_name":"slot_machine","short_names":["slot_machine"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1088,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BILLIARDS","unified":"1F3B1","non_qualified":null,"docomo":null,"au":"EADD","softbank":"E42C","google":"FE80E","image":"1f3b1.png","sheet_x":7,"sheet_y":52,"short_name":"8ball","short_names":["8ball"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1081,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GAME DIE","unified":"1F3B2","non_qualified":null,"docomo":null,"au":"E4C8","softbank":null,"google":"FE80F","image":"1f3b2.png","sheet_x":7,"sheet_y":53,"short_name":"game_die","short_names":["game_die"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1089,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BOWLING","unified":"1F3B3","non_qualified":null,"docomo":null,"au":"EB43","softbank":null,"google":"FE810","image":"1f3b3.png","sheet_x":7,"sheet_y":54,"short_name":"bowling","short_names":["bowling"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1060,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FLOWER PLAYING CARDS","unified":"1F3B4","non_qualified":null,"docomo":null,"au":"EB6E","softbank":null,"google":"FE811","image":"1f3b4.png","sheet_x":7,"sheet_y":55,"short_name":"flower_playing_cards","short_names":["flower_playing_cards"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1102,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MUSICAL NOTE","unified":"1F3B5","non_qualified":null,"docomo":"E6F6","au":"E5BE","softbank":"E03E","google":"FE813","image":"1f3b5.png","sheet_x":7,"sheet_y":56,"short_name":"musical_note","short_names":["musical_note"],"text":null,"texts":null,"category":"Objects","subcategory":"music","sort_order":1165,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MULTIPLE MUSICAL NOTES","unified":"1F3B6","non_qualified":null,"docomo":"E6FF","au":"E505","softbank":"E326","google":"FE814","image":"1f3b6.png","sheet_x":7,"sheet_y":57,"short_name":"notes","short_names":["notes"],"text":null,"texts":null,"category":"Objects","subcategory":"music","sort_order":1166,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SAXOPHONE","unified":"1F3B7","non_qualified":null,"docomo":null,"au":null,"softbank":"E040","google":"FE815","image":"1f3b7.png","sheet_x":7,"sheet_y":58,"short_name":"saxophone","short_names":["saxophone"],"text":null,"texts":null,"category":"Objects","subcategory":"musical-instrument","sort_order":1173,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GUITAR","unified":"1F3B8","non_qualified":null,"docomo":null,"au":"E506","softbank":"E041","google":"FE816","image":"1f3b8.png","sheet_x":7,"sheet_y":59,"short_name":"guitar","short_names":["guitar"],"text":null,"texts":null,"category":"Objects","subcategory":"musical-instrument","sort_order":1175,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MUSICAL KEYBOARD","unified":"1F3B9","non_qualified":null,"docomo":null,"au":"EB40","softbank":null,"google":"FE817","image":"1f3b9.png","sheet_x":7,"sheet_y":60,"short_name":"musical_keyboard","short_names":["musical_keyboard"],"text":null,"texts":null,"category":"Objects","subcategory":"musical-instrument","sort_order":1176,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TRUMPET","unified":"1F3BA","non_qualified":null,"docomo":null,"au":"EADC","softbank":"E042","google":"FE818","image":"1f3ba.png","sheet_x":8,"sheet_y":0,"short_name":"trumpet","short_names":["trumpet"],"text":null,"texts":null,"category":"Objects","subcategory":"musical-instrument","sort_order":1177,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"VIOLIN","unified":"1F3BB","non_qualified":null,"docomo":null,"au":"E507","softbank":null,"google":"FE819","image":"1f3bb.png","sheet_x":8,"sheet_y":1,"short_name":"violin","short_names":["violin"],"text":null,"texts":null,"category":"Objects","subcategory":"musical-instrument","sort_order":1178,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MUSICAL SCORE","unified":"1F3BC","non_qualified":null,"docomo":"E6FF","au":"EACC","softbank":null,"google":"FE81A","image":"1f3bc.png","sheet_x":8,"sheet_y":2,"short_name":"musical_score","short_names":["musical_score"],"text":null,"texts":null,"category":"Objects","subcategory":"music","sort_order":1164,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RUNNING SHIRT WITH SASH","unified":"1F3BD","non_qualified":null,"docomo":"E652","au":null,"softbank":null,"google":"FE7D0","image":"1f3bd.png","sheet_x":8,"sheet_y":3,"short_name":"running_shirt_with_sash","short_names":["running_shirt_with_sash"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1074,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TENNIS RACQUET AND BALL","unified":"1F3BE","non_qualified":null,"docomo":"E655","au":"E4B7","softbank":"E015","google":"FE7D3","image":"1f3be.png","sheet_x":8,"sheet_y":4,"short_name":"tennis","short_names":["tennis"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1058,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SKI AND SKI BOOT","unified":"1F3BF","non_qualified":null,"docomo":"E657","au":"EAAC","softbank":"E013","google":"FE7D5","image":"1f3bf.png","sheet_x":8,"sheet_y":5,"short_name":"ski","short_names":["ski"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1075,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BASKETBALL AND HOOP","unified":"1F3C0","non_qualified":null,"docomo":"E658","au":"E59A","softbank":"E42A","google":"FE7D6","image":"1f3c0.png","sheet_x":8,"sheet_y":6,"short_name":"basketball","short_names":["basketball"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1054,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CHEQUERED FLAG","unified":"1F3C1","non_qualified":null,"docomo":"E659","au":"E4B9","softbank":"E132","google":"FE7D7","image":"1f3c1.png","sheet_x":8,"sheet_y":7,"short_name":"checkered_flag","short_names":["checkered_flag"],"text":null,"texts":null,"category":"Flags","subcategory":"flag","sort_order":1586,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SNOWBOARDER","unified":"1F3C2","non_qualified":null,"docomo":"E712","au":"E4B8","softbank":null,"google":"FE7D8","image":"1f3c2.png","sheet_x":8,"sheet_y":8,"short_name":"snowboarder","short_names":["snowboarder"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":437,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F3C2-1F3FB","non_qualified":null,"image":"1f3c2-1f3fb.png","sheet_x":8,"sheet_y":9,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F3C2-1F3FC","non_qualified":null,"image":"1f3c2-1f3fc.png","sheet_x":8,"sheet_y":10,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F3C2-1F3FD","non_qualified":null,"image":"1f3c2-1f3fd.png","sheet_x":8,"sheet_y":11,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F3C2-1F3FE","non_qualified":null,"image":"1f3c2-1f3fe.png","sheet_x":8,"sheet_y":12,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F3C2-1F3FF","non_qualified":null,"image":"1f3c2-1f3ff.png","sheet_x":8,"sheet_y":13,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN RUNNING","unified":"1F3C3-200D-2640-FE0F","non_qualified":"1F3C3-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3c3-200d-2640-fe0f.png","sheet_x":8,"sheet_y":14,"short_name":"woman-running","short_names":["woman-running"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":421,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F3C3-1F3FB-200D-2640-FE0F","non_qualified":"1F3C3-1F3FB-200D-2640","image":"1f3c3-1f3fb-200d-2640-fe0f.png","sheet_x":8,"sheet_y":15,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F3C3-1F3FC-200D-2640-FE0F","non_qualified":"1F3C3-1F3FC-200D-2640","image":"1f3c3-1f3fc-200d-2640-fe0f.png","sheet_x":8,"sheet_y":16,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F3C3-1F3FD-200D-2640-FE0F","non_qualified":"1F3C3-1F3FD-200D-2640","image":"1f3c3-1f3fd-200d-2640-fe0f.png","sheet_x":8,"sheet_y":17,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F3C3-1F3FE-200D-2640-FE0F","non_qualified":"1F3C3-1F3FE-200D-2640","image":"1f3c3-1f3fe-200d-2640-fe0f.png","sheet_x":8,"sheet_y":18,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F3C3-1F3FF-200D-2640-FE0F","non_qualified":"1F3C3-1F3FF-200D-2640","image":"1f3c3-1f3ff-200d-2640-fe0f.png","sheet_x":8,"sheet_y":19,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN RUNNING","unified":"1F3C3-200D-2642-FE0F","non_qualified":"1F3C3-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3c3-200d-2642-fe0f.png","sheet_x":8,"sheet_y":20,"short_name":"man-running","short_names":["man-running"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":420,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F3C3-1F3FB-200D-2642-FE0F","non_qualified":"1F3C3-1F3FB-200D-2642","image":"1f3c3-1f3fb-200d-2642-fe0f.png","sheet_x":8,"sheet_y":21,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F3C3-1F3FC-200D-2642-FE0F","non_qualified":"1F3C3-1F3FC-200D-2642","image":"1f3c3-1f3fc-200d-2642-fe0f.png","sheet_x":8,"sheet_y":22,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F3C3-1F3FD-200D-2642-FE0F","non_qualified":"1F3C3-1F3FD-200D-2642","image":"1f3c3-1f3fd-200d-2642-fe0f.png","sheet_x":8,"sheet_y":23,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F3C3-1F3FE-200D-2642-FE0F","non_qualified":"1F3C3-1F3FE-200D-2642","image":"1f3c3-1f3fe-200d-2642-fe0f.png","sheet_x":8,"sheet_y":24,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F3C3-1F3FF-200D-2642-FE0F","non_qualified":"1F3C3-1F3FF-200D-2642","image":"1f3c3-1f3ff-200d-2642-fe0f.png","sheet_x":8,"sheet_y":25,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F3C3"},{"name":"RUNNER","unified":"1F3C3","non_qualified":null,"docomo":"E733","au":"E46B","softbank":"E115","google":"FE7D9","image":"1f3c3.png","sheet_x":8,"sheet_y":26,"short_name":"runner","short_names":["runner","running"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":419,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F3C3-1F3FB","non_qualified":null,"image":"1f3c3-1f3fb.png","sheet_x":8,"sheet_y":27,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F3C3-1F3FC","non_qualified":null,"image":"1f3c3-1f3fc.png","sheet_x":8,"sheet_y":28,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F3C3-1F3FD","non_qualified":null,"image":"1f3c3-1f3fd.png","sheet_x":8,"sheet_y":29,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F3C3-1F3FE","non_qualified":null,"image":"1f3c3-1f3fe.png","sheet_x":8,"sheet_y":30,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F3C3-1F3FF","non_qualified":null,"image":"1f3c3-1f3ff.png","sheet_x":8,"sheet_y":31,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F3C3-200D-2642-FE0F"},{"name":"WOMAN SURFING","unified":"1F3C4-200D-2640-FE0F","non_qualified":"1F3C4-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3c4-200d-2640-fe0f.png","sheet_x":8,"sheet_y":32,"short_name":"woman-surfing","short_names":["woman-surfing"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":443,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F3C4-1F3FB-200D-2640-FE0F","non_qualified":"1F3C4-1F3FB-200D-2640","image":"1f3c4-1f3fb-200d-2640-fe0f.png","sheet_x":8,"sheet_y":33,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F3C4-1F3FC-200D-2640-FE0F","non_qualified":"1F3C4-1F3FC-200D-2640","image":"1f3c4-1f3fc-200d-2640-fe0f.png","sheet_x":8,"sheet_y":34,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F3C4-1F3FD-200D-2640-FE0F","non_qualified":"1F3C4-1F3FD-200D-2640","image":"1f3c4-1f3fd-200d-2640-fe0f.png","sheet_x":8,"sheet_y":35,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F3C4-1F3FE-200D-2640-FE0F","non_qualified":"1F3C4-1F3FE-200D-2640","image":"1f3c4-1f3fe-200d-2640-fe0f.png","sheet_x":8,"sheet_y":36,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F3C4-1F3FF-200D-2640-FE0F","non_qualified":"1F3C4-1F3FF-200D-2640","image":"1f3c4-1f3ff-200d-2640-fe0f.png","sheet_x":8,"sheet_y":37,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN SURFING","unified":"1F3C4-200D-2642-FE0F","non_qualified":"1F3C4-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3c4-200d-2642-fe0f.png","sheet_x":8,"sheet_y":38,"short_name":"man-surfing","short_names":["man-surfing"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":442,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F3C4-1F3FB-200D-2642-FE0F","non_qualified":"1F3C4-1F3FB-200D-2642","image":"1f3c4-1f3fb-200d-2642-fe0f.png","sheet_x":8,"sheet_y":39,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F3C4-1F3FC-200D-2642-FE0F","non_qualified":"1F3C4-1F3FC-200D-2642","image":"1f3c4-1f3fc-200d-2642-fe0f.png","sheet_x":8,"sheet_y":40,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F3C4-1F3FD-200D-2642-FE0F","non_qualified":"1F3C4-1F3FD-200D-2642","image":"1f3c4-1f3fd-200d-2642-fe0f.png","sheet_x":8,"sheet_y":41,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F3C4-1F3FE-200D-2642-FE0F","non_qualified":"1F3C4-1F3FE-200D-2642","image":"1f3c4-1f3fe-200d-2642-fe0f.png","sheet_x":8,"sheet_y":42,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F3C4-1F3FF-200D-2642-FE0F","non_qualified":"1F3C4-1F3FF-200D-2642","image":"1f3c4-1f3ff-200d-2642-fe0f.png","sheet_x":8,"sheet_y":43,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F3C4"},{"name":"SURFER","unified":"1F3C4","non_qualified":null,"docomo":"E712","au":"EB41","softbank":"E017","google":"FE7DA","image":"1f3c4.png","sheet_x":8,"sheet_y":44,"short_name":"surfer","short_names":["surfer"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":441,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F3C4-1F3FB","non_qualified":null,"image":"1f3c4-1f3fb.png","sheet_x":8,"sheet_y":45,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F3C4-1F3FC","non_qualified":null,"image":"1f3c4-1f3fc.png","sheet_x":8,"sheet_y":46,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F3C4-1F3FD","non_qualified":null,"image":"1f3c4-1f3fd.png","sheet_x":8,"sheet_y":47,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F3C4-1F3FE","non_qualified":null,"image":"1f3c4-1f3fe.png","sheet_x":8,"sheet_y":48,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F3C4-1F3FF","non_qualified":null,"image":"1f3c4-1f3ff.png","sheet_x":8,"sheet_y":49,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F3C4-200D-2642-FE0F"},{"name":"SPORTS MEDAL","unified":"1F3C5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3c5.png","sheet_x":8,"sheet_y":50,"short_name":"sports_medal","short_names":["sports_medal"],"text":null,"texts":null,"category":"Activities","subcategory":"award-medal","sort_order":1047,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TROPHY","unified":"1F3C6","non_qualified":null,"docomo":null,"au":"E5D3","softbank":"E131","google":"FE7DB","image":"1f3c6.png","sheet_x":8,"sheet_y":51,"short_name":"trophy","short_names":["trophy"],"text":null,"texts":null,"category":"Activities","subcategory":"award-medal","sort_order":1046,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HORSE RACING","unified":"1F3C7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3c7.png","sheet_x":8,"sheet_y":52,"short_name":"horse_racing","short_names":["horse_racing"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":435,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F3C7-1F3FB","non_qualified":null,"image":"1f3c7-1f3fb.png","sheet_x":8,"sheet_y":53,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F3C7-1F3FC","non_qualified":null,"image":"1f3c7-1f3fc.png","sheet_x":8,"sheet_y":54,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F3C7-1F3FD","non_qualified":null,"image":"1f3c7-1f3fd.png","sheet_x":8,"sheet_y":55,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F3C7-1F3FE","non_qualified":null,"image":"1f3c7-1f3fe.png","sheet_x":8,"sheet_y":56,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F3C7-1F3FF","non_qualified":null,"image":"1f3c7-1f3ff.png","sheet_x":8,"sheet_y":57,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"AMERICAN FOOTBALL","unified":"1F3C8","non_qualified":null,"docomo":null,"au":"E4BB","softbank":"E42B","google":"FE7DD","image":"1f3c8.png","sheet_x":8,"sheet_y":58,"short_name":"football","short_names":["football"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1056,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RUGBY FOOTBALL","unified":"1F3C9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3c9.png","sheet_x":8,"sheet_y":59,"short_name":"rugby_football","short_names":["rugby_football"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1057,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WOMAN SWIMMING","unified":"1F3CA-200D-2640-FE0F","non_qualified":"1F3CA-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3ca-200d-2640-fe0f.png","sheet_x":8,"sheet_y":60,"short_name":"woman-swimming","short_names":["woman-swimming"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":449,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F3CA-1F3FB-200D-2640-FE0F","non_qualified":"1F3CA-1F3FB-200D-2640","image":"1f3ca-1f3fb-200d-2640-fe0f.png","sheet_x":9,"sheet_y":0,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F3CA-1F3FC-200D-2640-FE0F","non_qualified":"1F3CA-1F3FC-200D-2640","image":"1f3ca-1f3fc-200d-2640-fe0f.png","sheet_x":9,"sheet_y":1,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F3CA-1F3FD-200D-2640-FE0F","non_qualified":"1F3CA-1F3FD-200D-2640","image":"1f3ca-1f3fd-200d-2640-fe0f.png","sheet_x":9,"sheet_y":2,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F3CA-1F3FE-200D-2640-FE0F","non_qualified":"1F3CA-1F3FE-200D-2640","image":"1f3ca-1f3fe-200d-2640-fe0f.png","sheet_x":9,"sheet_y":3,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F3CA-1F3FF-200D-2640-FE0F","non_qualified":"1F3CA-1F3FF-200D-2640","image":"1f3ca-1f3ff-200d-2640-fe0f.png","sheet_x":9,"sheet_y":4,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN SWIMMING","unified":"1F3CA-200D-2642-FE0F","non_qualified":"1F3CA-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3ca-200d-2642-fe0f.png","sheet_x":9,"sheet_y":5,"short_name":"man-swimming","short_names":["man-swimming"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":448,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F3CA-1F3FB-200D-2642-FE0F","non_qualified":"1F3CA-1F3FB-200D-2642","image":"1f3ca-1f3fb-200d-2642-fe0f.png","sheet_x":9,"sheet_y":6,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F3CA-1F3FC-200D-2642-FE0F","non_qualified":"1F3CA-1F3FC-200D-2642","image":"1f3ca-1f3fc-200d-2642-fe0f.png","sheet_x":9,"sheet_y":7,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F3CA-1F3FD-200D-2642-FE0F","non_qualified":"1F3CA-1F3FD-200D-2642","image":"1f3ca-1f3fd-200d-2642-fe0f.png","sheet_x":9,"sheet_y":8,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F3CA-1F3FE-200D-2642-FE0F","non_qualified":"1F3CA-1F3FE-200D-2642","image":"1f3ca-1f3fe-200d-2642-fe0f.png","sheet_x":9,"sheet_y":9,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F3CA-1F3FF-200D-2642-FE0F","non_qualified":"1F3CA-1F3FF-200D-2642","image":"1f3ca-1f3ff-200d-2642-fe0f.png","sheet_x":9,"sheet_y":10,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F3CA"},{"name":"SWIMMER","unified":"1F3CA","non_qualified":null,"docomo":null,"au":"EADE","softbank":"E42D","google":"FE7DE","image":"1f3ca.png","sheet_x":9,"sheet_y":11,"short_name":"swimmer","short_names":["swimmer"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":447,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F3CA-1F3FB","non_qualified":null,"image":"1f3ca-1f3fb.png","sheet_x":9,"sheet_y":12,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F3CA-1F3FC","non_qualified":null,"image":"1f3ca-1f3fc.png","sheet_x":9,"sheet_y":13,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F3CA-1F3FD","non_qualified":null,"image":"1f3ca-1f3fd.png","sheet_x":9,"sheet_y":14,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F3CA-1F3FE","non_qualified":null,"image":"1f3ca-1f3fe.png","sheet_x":9,"sheet_y":15,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F3CA-1F3FF","non_qualified":null,"image":"1f3ca-1f3ff.png","sheet_x":9,"sheet_y":16,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F3CA-200D-2642-FE0F"},{"name":"WOMAN LIFTING WEIGHTS","unified":"1F3CB-FE0F-200D-2640-FE0F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3cb-fe0f-200d-2640-fe0f.png","sheet_x":9,"sheet_y":17,"short_name":"woman-lifting-weights","short_names":["woman-lifting-weights"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":455,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"1F3CB-1F3FB-200D-2640-FE0F","non_qualified":"1F3CB-1F3FB-200D-2640","image":"1f3cb-1f3fb-200d-2640-fe0f.png","sheet_x":9,"sheet_y":18,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F3CB-1F3FC-200D-2640-FE0F","non_qualified":"1F3CB-1F3FC-200D-2640","image":"1f3cb-1f3fc-200d-2640-fe0f.png","sheet_x":9,"sheet_y":19,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F3CB-1F3FD-200D-2640-FE0F","non_qualified":"1F3CB-1F3FD-200D-2640","image":"1f3cb-1f3fd-200d-2640-fe0f.png","sheet_x":9,"sheet_y":20,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F3CB-1F3FE-200D-2640-FE0F","non_qualified":"1F3CB-1F3FE-200D-2640","image":"1f3cb-1f3fe-200d-2640-fe0f.png","sheet_x":9,"sheet_y":21,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F3CB-1F3FF-200D-2640-FE0F","non_qualified":"1F3CB-1F3FF-200D-2640","image":"1f3cb-1f3ff-200d-2640-fe0f.png","sheet_x":9,"sheet_y":22,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN LIFTING WEIGHTS","unified":"1F3CB-FE0F-200D-2642-FE0F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3cb-fe0f-200d-2642-fe0f.png","sheet_x":9,"sheet_y":23,"short_name":"man-lifting-weights","short_names":["man-lifting-weights"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":454,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"1F3CB-1F3FB-200D-2642-FE0F","non_qualified":"1F3CB-1F3FB-200D-2642","image":"1f3cb-1f3fb-200d-2642-fe0f.png","sheet_x":9,"sheet_y":24,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F3CB-1F3FC-200D-2642-FE0F","non_qualified":"1F3CB-1F3FC-200D-2642","image":"1f3cb-1f3fc-200d-2642-fe0f.png","sheet_x":9,"sheet_y":25,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F3CB-1F3FD-200D-2642-FE0F","non_qualified":"1F3CB-1F3FD-200D-2642","image":"1f3cb-1f3fd-200d-2642-fe0f.png","sheet_x":9,"sheet_y":26,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F3CB-1F3FE-200D-2642-FE0F","non_qualified":"1F3CB-1F3FE-200D-2642","image":"1f3cb-1f3fe-200d-2642-fe0f.png","sheet_x":9,"sheet_y":27,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F3CB-1F3FF-200D-2642-FE0F","non_qualified":"1F3CB-1F3FF-200D-2642","image":"1f3cb-1f3ff-200d-2642-fe0f.png","sheet_x":9,"sheet_y":28,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F3CB-FE0F"},{"name":"PERSON LIFTING WEIGHTS","unified":"1F3CB-FE0F","non_qualified":"1F3CB","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3cb-fe0f.png","sheet_x":9,"sheet_y":29,"short_name":"weight_lifter","short_names":["weight_lifter"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":453,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F3CB-1F3FB","non_qualified":null,"image":"1f3cb-1f3fb.png","sheet_x":9,"sheet_y":30,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F3CB-1F3FC","non_qualified":null,"image":"1f3cb-1f3fc.png","sheet_x":9,"sheet_y":31,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F3CB-1F3FD","non_qualified":null,"image":"1f3cb-1f3fd.png","sheet_x":9,"sheet_y":32,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F3CB-1F3FE","non_qualified":null,"image":"1f3cb-1f3fe.png","sheet_x":9,"sheet_y":33,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F3CB-1F3FF","non_qualified":null,"image":"1f3cb-1f3ff.png","sheet_x":9,"sheet_y":34,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F3CB-FE0F-200D-2642-FE0F"},{"name":"WOMAN GOLFING","unified":"1F3CC-FE0F-200D-2640-FE0F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3cc-fe0f-200d-2640-fe0f.png","sheet_x":9,"sheet_y":35,"short_name":"woman-golfing","short_names":["woman-golfing"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":440,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"1F3CC-1F3FB-200D-2640-FE0F","non_qualified":"1F3CC-1F3FB-200D-2640","image":"1f3cc-1f3fb-200d-2640-fe0f.png","sheet_x":9,"sheet_y":36,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F3CC-1F3FC-200D-2640-FE0F","non_qualified":"1F3CC-1F3FC-200D-2640","image":"1f3cc-1f3fc-200d-2640-fe0f.png","sheet_x":9,"sheet_y":37,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F3CC-1F3FD-200D-2640-FE0F","non_qualified":"1F3CC-1F3FD-200D-2640","image":"1f3cc-1f3fd-200d-2640-fe0f.png","sheet_x":9,"sheet_y":38,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F3CC-1F3FE-200D-2640-FE0F","non_qualified":"1F3CC-1F3FE-200D-2640","image":"1f3cc-1f3fe-200d-2640-fe0f.png","sheet_x":9,"sheet_y":39,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F3CC-1F3FF-200D-2640-FE0F","non_qualified":"1F3CC-1F3FF-200D-2640","image":"1f3cc-1f3ff-200d-2640-fe0f.png","sheet_x":9,"sheet_y":40,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN GOLFING","unified":"1F3CC-FE0F-200D-2642-FE0F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3cc-fe0f-200d-2642-fe0f.png","sheet_x":9,"sheet_y":41,"short_name":"man-golfing","short_names":["man-golfing"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":439,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"1F3CC-1F3FB-200D-2642-FE0F","non_qualified":"1F3CC-1F3FB-200D-2642","image":"1f3cc-1f3fb-200d-2642-fe0f.png","sheet_x":9,"sheet_y":42,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F3CC-1F3FC-200D-2642-FE0F","non_qualified":"1F3CC-1F3FC-200D-2642","image":"1f3cc-1f3fc-200d-2642-fe0f.png","sheet_x":9,"sheet_y":43,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F3CC-1F3FD-200D-2642-FE0F","non_qualified":"1F3CC-1F3FD-200D-2642","image":"1f3cc-1f3fd-200d-2642-fe0f.png","sheet_x":9,"sheet_y":44,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F3CC-1F3FE-200D-2642-FE0F","non_qualified":"1F3CC-1F3FE-200D-2642","image":"1f3cc-1f3fe-200d-2642-fe0f.png","sheet_x":9,"sheet_y":45,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F3CC-1F3FF-200D-2642-FE0F","non_qualified":"1F3CC-1F3FF-200D-2642","image":"1f3cc-1f3ff-200d-2642-fe0f.png","sheet_x":9,"sheet_y":46,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F3CC-FE0F"},{"name":"PERSON GOLFING","unified":"1F3CC-FE0F","non_qualified":"1F3CC","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3cc-fe0f.png","sheet_x":9,"sheet_y":47,"short_name":"golfer","short_names":["golfer"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":438,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F3CC-1F3FB","non_qualified":null,"image":"1f3cc-1f3fb.png","sheet_x":9,"sheet_y":48,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F3CC-1F3FC","non_qualified":null,"image":"1f3cc-1f3fc.png","sheet_x":9,"sheet_y":49,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F3CC-1F3FD","non_qualified":null,"image":"1f3cc-1f3fd.png","sheet_x":9,"sheet_y":50,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F3CC-1F3FE","non_qualified":null,"image":"1f3cc-1f3fe.png","sheet_x":9,"sheet_y":51,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F3CC-1F3FF","non_qualified":null,"image":"1f3cc-1f3ff.png","sheet_x":9,"sheet_y":52,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F3CC-FE0F-200D-2642-FE0F"},{"name":"MOTORCYCLE","unified":"1F3CD-FE0F","non_qualified":"1F3CD","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3cd-fe0f.png","sheet_x":9,"sheet_y":53,"short_name":"racing_motorcycle","short_names":["racing_motorcycle"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":902,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RACING CAR","unified":"1F3CE-FE0F","non_qualified":"1F3CE","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3ce-fe0f.png","sheet_x":9,"sheet_y":54,"short_name":"racing_car","short_names":["racing_car"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":901,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CRICKET BAT AND BALL","unified":"1F3CF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3cf.png","sheet_x":9,"sheet_y":55,"short_name":"cricket_bat_and_ball","short_names":["cricket_bat_and_ball"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1061,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"VOLLEYBALL","unified":"1F3D0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3d0.png","sheet_x":9,"sheet_y":56,"short_name":"volleyball","short_names":["volleyball"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1055,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FIELD HOCKEY STICK AND BALL","unified":"1F3D1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3d1.png","sheet_x":9,"sheet_y":57,"short_name":"field_hockey_stick_and_ball","short_names":["field_hockey_stick_and_ball"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1062,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ICE HOCKEY STICK AND PUCK","unified":"1F3D2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3d2.png","sheet_x":9,"sheet_y":58,"short_name":"ice_hockey_stick_and_puck","short_names":["ice_hockey_stick_and_puck"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1063,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TABLE TENNIS PADDLE AND BALL","unified":"1F3D3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3d3.png","sheet_x":9,"sheet_y":59,"short_name":"table_tennis_paddle_and_ball","short_names":["table_tennis_paddle_and_ball"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1065,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SNOW-CAPPED MOUNTAIN","unified":"1F3D4-FE0F","non_qualified":"1F3D4","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3d4-fe0f.png","sheet_x":9,"sheet_y":60,"short_name":"snow_capped_mountain","short_names":["snow_capped_mountain"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-geographic","sort_order":813,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CAMPING","unified":"1F3D5-FE0F","non_qualified":"1F3D5","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3d5-fe0f.png","sheet_x":10,"sheet_y":0,"short_name":"camping","short_names":["camping"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-geographic","sort_order":817,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BEACH WITH UMBRELLA","unified":"1F3D6-FE0F","non_qualified":"1F3D6","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3d6-fe0f.png","sheet_x":10,"sheet_y":1,"short_name":"beach_with_umbrella","short_names":["beach_with_umbrella"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-geographic","sort_order":818,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BUILDING CONSTRUCTION","unified":"1F3D7-FE0F","non_qualified":"1F3D7","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3d7-fe0f.png","sheet_x":10,"sheet_y":2,"short_name":"building_construction","short_names":["building_construction"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":824,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HOUSES","unified":"1F3D8-FE0F","non_qualified":"1F3D8","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3d8-fe0f.png","sheet_x":10,"sheet_y":3,"short_name":"house_buildings","short_names":["house_buildings"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":829,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CITYSCAPE","unified":"1F3D9-FE0F","non_qualified":"1F3D9","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3d9-fe0f.png","sheet_x":10,"sheet_y":4,"short_name":"cityscape","short_names":["cityscape"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-other","sort_order":859,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DERELICT HOUSE","unified":"1F3DA-FE0F","non_qualified":"1F3DA","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3da-fe0f.png","sheet_x":10,"sheet_y":5,"short_name":"derelict_house_building","short_names":["derelict_house_building"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":830,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLASSICAL BUILDING","unified":"1F3DB-FE0F","non_qualified":"1F3DB","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3db-fe0f.png","sheet_x":10,"sheet_y":6,"short_name":"classical_building","short_names":["classical_building"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":823,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DESERT","unified":"1F3DC-FE0F","non_qualified":"1F3DC","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3dc-fe0f.png","sheet_x":10,"sheet_y":7,"short_name":"desert","short_names":["desert"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-geographic","sort_order":819,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DESERT ISLAND","unified":"1F3DD-FE0F","non_qualified":"1F3DD","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3dd-fe0f.png","sheet_x":10,"sheet_y":8,"short_name":"desert_island","short_names":["desert_island"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-geographic","sort_order":820,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NATIONAL PARK","unified":"1F3DE-FE0F","non_qualified":"1F3DE","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3de-fe0f.png","sheet_x":10,"sheet_y":9,"short_name":"national_park","short_names":["national_park"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-geographic","sort_order":821,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"STADIUM","unified":"1F3DF-FE0F","non_qualified":"1F3DF","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3df-fe0f.png","sheet_x":10,"sheet_y":10,"short_name":"stadium","short_names":["stadium"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":822,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HOUSE BUILDING","unified":"1F3E0","non_qualified":null,"docomo":"E663","au":"E4AB","softbank":"E036","google":"FE4B0","image":"1f3e0.png","sheet_x":10,"sheet_y":11,"short_name":"house","short_names":["house"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":831,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HOUSE WITH GARDEN","unified":"1F3E1","non_qualified":null,"docomo":"E663","au":"EB09","softbank":null,"google":"FE4B1","image":"1f3e1.png","sheet_x":10,"sheet_y":12,"short_name":"house_with_garden","short_names":["house_with_garden"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":832,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"OFFICE BUILDING","unified":"1F3E2","non_qualified":null,"docomo":"E664","au":"E4AD","softbank":"E038","google":"FE4B2","image":"1f3e2.png","sheet_x":10,"sheet_y":13,"short_name":"office","short_names":["office"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":833,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"JAPANESE POST OFFICE","unified":"1F3E3","non_qualified":null,"docomo":"E665","au":"E5DE","softbank":"E153","google":"FE4B3","image":"1f3e3.png","sheet_x":10,"sheet_y":14,"short_name":"post_office","short_names":["post_office"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":834,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EUROPEAN POST OFFICE","unified":"1F3E4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3e4.png","sheet_x":10,"sheet_y":15,"short_name":"european_post_office","short_names":["european_post_office"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":835,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HOSPITAL","unified":"1F3E5","non_qualified":null,"docomo":"E666","au":"E5DF","softbank":"E155","google":"FE4B4","image":"1f3e5.png","sheet_x":10,"sheet_y":16,"short_name":"hospital","short_names":["hospital"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":836,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BANK","unified":"1F3E6","non_qualified":null,"docomo":"E667","au":"E4AA","softbank":"E14D","google":"FE4B5","image":"1f3e6.png","sheet_x":10,"sheet_y":17,"short_name":"bank","short_names":["bank"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":837,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"AUTOMATED TELLER MACHINE","unified":"1F3E7","non_qualified":null,"docomo":"E668","au":"E4A3","softbank":"E154","google":"FE4B6","image":"1f3e7.png","sheet_x":10,"sheet_y":18,"short_name":"atm","short_names":["atm"],"text":null,"texts":null,"category":"Symbols","subcategory":"transport-sign","sort_order":1365,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HOTEL","unified":"1F3E8","non_qualified":null,"docomo":"E669","au":"EA81","softbank":"E158","google":"FE4B7","image":"1f3e8.png","sheet_x":10,"sheet_y":19,"short_name":"hotel","short_names":["hotel"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":838,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LOVE HOTEL","unified":"1F3E9","non_qualified":null,"docomo":"E669-E6EF","au":"EAF3","softbank":"E501","google":"FE4B8","image":"1f3e9.png","sheet_x":10,"sheet_y":20,"short_name":"love_hotel","short_names":["love_hotel"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":839,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CONVENIENCE STORE","unified":"1F3EA","non_qualified":null,"docomo":"E66A","au":"E4A4","softbank":"E156","google":"FE4B9","image":"1f3ea.png","sheet_x":10,"sheet_y":21,"short_name":"convenience_store","short_names":["convenience_store"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":840,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SCHOOL","unified":"1F3EB","non_qualified":null,"docomo":"E73E","au":"EA80","softbank":"E157","google":"FE4BA","image":"1f3eb.png","sheet_x":10,"sheet_y":22,"short_name":"school","short_names":["school"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":841,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DEPARTMENT STORE","unified":"1F3EC","non_qualified":null,"docomo":null,"au":"EAF6","softbank":"E504","google":"FE4BD","image":"1f3ec.png","sheet_x":10,"sheet_y":23,"short_name":"department_store","short_names":["department_store"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":842,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACTORY","unified":"1F3ED","non_qualified":null,"docomo":null,"au":"EAF9","softbank":"E508","google":"FE4C0","image":"1f3ed.png","sheet_x":10,"sheet_y":24,"short_name":"factory","short_names":["factory"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":843,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"IZAKAYA LANTERN","unified":"1F3EE","non_qualified":null,"docomo":"E74B","au":"E4BD","softbank":null,"google":"FE4C2","image":"1f3ee.png","sheet_x":10,"sheet_y":25,"short_name":"izakaya_lantern","short_names":["izakaya_lantern","lantern"],"text":null,"texts":null,"category":"Objects","subcategory":"light & video","sort_order":1216,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"JAPANESE CASTLE","unified":"1F3EF","non_qualified":null,"docomo":null,"au":"EAF7","softbank":"E505","google":"FE4BE","image":"1f3ef.png","sheet_x":10,"sheet_y":26,"short_name":"japanese_castle","short_names":["japanese_castle"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":844,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EUROPEAN CASTLE","unified":"1F3F0","non_qualified":null,"docomo":null,"au":"EAF8","softbank":"E506","google":"FE4BF","image":"1f3f0.png","sheet_x":10,"sheet_y":27,"short_name":"european_castle","short_names":["european_castle"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":845,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RAINBOW FLAG","unified":"1F3F3-FE0F-200D-1F308","non_qualified":"1F3F3-200D-1F308","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3f3-fe0f-200d-1f308.png","sheet_x":10,"sheet_y":28,"short_name":"rainbow-flag","short_names":["rainbow-flag"],"text":null,"texts":null,"category":"Flags","subcategory":"flag","sort_order":1591,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TRANSGENDER FLAG","unified":"1F3F3-FE0F-200D-26A7-FE0F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3f3-fe0f-200d-26a7-fe0f.png","sheet_x":10,"sheet_y":29,"short_name":"transgender_flag","short_names":["transgender_flag"],"text":null,"texts":null,"category":"Flags","subcategory":"flag","sort_order":1592,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"WHITE FLAG","unified":"1F3F3-FE0F","non_qualified":"1F3F3","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3f3-fe0f.png","sheet_x":10,"sheet_y":30,"short_name":"waving_white_flag","short_names":["waving_white_flag"],"text":null,"texts":null,"category":"Flags","subcategory":"flag","sort_order":1590,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PIRATE FLAG","unified":"1F3F4-200D-2620-FE0F","non_qualified":"1F3F4-200D-2620","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3f4-200d-2620-fe0f.png","sheet_x":10,"sheet_y":31,"short_name":"pirate_flag","short_names":["pirate_flag"],"text":null,"texts":null,"category":"Flags","subcategory":"flag","sort_order":1593,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"England Flag","unified":"1F3F4-E0067-E0062-E0065-E006E-E0067-E007F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3f4-e0067-e0062-e0065-e006e-e0067-e007f.png","sheet_x":10,"sheet_y":32,"short_name":"flag-england","short_names":["flag-england"],"text":null,"texts":null,"category":"Flags","subcategory":"subdivision-flag","sort_order":1852,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Scotland Flag","unified":"1F3F4-E0067-E0062-E0073-E0063-E0074-E007F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3f4-e0067-e0062-e0073-e0063-e0074-e007f.png","sheet_x":10,"sheet_y":33,"short_name":"flag-scotland","short_names":["flag-scotland"],"text":null,"texts":null,"category":"Flags","subcategory":"subdivision-flag","sort_order":1853,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Wales Flag","unified":"1F3F4-E0067-E0062-E0077-E006C-E0073-E007F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3f4-e0067-e0062-e0077-e006c-e0073-e007f.png","sheet_x":10,"sheet_y":34,"short_name":"flag-wales","short_names":["flag-wales"],"text":null,"texts":null,"category":"Flags","subcategory":"subdivision-flag","sort_order":1854,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WAVING BLACK FLAG","unified":"1F3F4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3f4.png","sheet_x":10,"sheet_y":35,"short_name":"waving_black_flag","short_names":["waving_black_flag"],"text":null,"texts":null,"category":"Flags","subcategory":"flag","sort_order":1589,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ROSETTE","unified":"1F3F5-FE0F","non_qualified":"1F3F5","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3f5-fe0f.png","sheet_x":10,"sheet_y":36,"short_name":"rosette","short_names":["rosette"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-flower","sort_order":652,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LABEL","unified":"1F3F7-FE0F","non_qualified":"1F3F7","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3f7-fe0f.png","sheet_x":10,"sheet_y":37,"short_name":"label","short_names":["label"],"text":null,"texts":null,"category":"Objects","subcategory":"book-paper","sort_order":1234,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BADMINTON RACQUET AND SHUTTLECOCK","unified":"1F3F8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3f8.png","sheet_x":10,"sheet_y":38,"short_name":"badminton_racquet_and_shuttlecock","short_names":["badminton_racquet_and_shuttlecock"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1066,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BOW AND ARROW","unified":"1F3F9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3f9.png","sheet_x":10,"sheet_y":39,"short_name":"bow_and_arrow","short_names":["bow_and_arrow"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1303,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"AMPHORA","unified":"1F3FA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3fa.png","sheet_x":10,"sheet_y":40,"short_name":"amphora","short_names":["amphora"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"dishware","sort_order":805,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EMOJI MODIFIER FITZPATRICK TYPE-1-2","unified":"1F3FB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3fb.png","sheet_x":10,"sheet_y":41,"short_name":"skin-tone-2","short_names":["skin-tone-2"],"text":null,"texts":null,"category":"Component","subcategory":"skin-tone","sort_order":525,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EMOJI MODIFIER FITZPATRICK TYPE-3","unified":"1F3FC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3fc.png","sheet_x":10,"sheet_y":42,"short_name":"skin-tone-3","short_names":["skin-tone-3"],"text":null,"texts":null,"category":"Component","subcategory":"skin-tone","sort_order":526,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EMOJI MODIFIER FITZPATRICK TYPE-4","unified":"1F3FD","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3fd.png","sheet_x":10,"sheet_y":43,"short_name":"skin-tone-4","short_names":["skin-tone-4"],"text":null,"texts":null,"category":"Component","subcategory":"skin-tone","sort_order":527,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EMOJI MODIFIER FITZPATRICK TYPE-5","unified":"1F3FE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3fe.png","sheet_x":10,"sheet_y":44,"short_name":"skin-tone-5","short_names":["skin-tone-5"],"text":null,"texts":null,"category":"Component","subcategory":"skin-tone","sort_order":528,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EMOJI MODIFIER FITZPATRICK TYPE-6","unified":"1F3FF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3ff.png","sheet_x":10,"sheet_y":45,"short_name":"skin-tone-6","short_names":["skin-tone-6"],"text":null,"texts":null,"category":"Component","subcategory":"skin-tone","sort_order":529,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RAT","unified":"1F400","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f400.png","sheet_x":10,"sheet_y":46,"short_name":"rat","short_names":["rat"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":576,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MOUSE","unified":"1F401","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f401.png","sheet_x":10,"sheet_y":47,"short_name":"mouse2","short_names":["mouse2"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":575,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"OX","unified":"1F402","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f402.png","sheet_x":10,"sheet_y":48,"short_name":"ox","short_names":["ox"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":556,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WATER BUFFALO","unified":"1F403","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f403.png","sheet_x":10,"sheet_y":49,"short_name":"water_buffalo","short_names":["water_buffalo"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":557,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"COW","unified":"1F404","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f404.png","sheet_x":10,"sheet_y":50,"short_name":"cow2","short_names":["cow2"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":558,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TIGER","unified":"1F405","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f405.png","sheet_x":10,"sheet_y":51,"short_name":"tiger2","short_names":["tiger2"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":547,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LEOPARD","unified":"1F406","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f406.png","sheet_x":10,"sheet_y":52,"short_name":"leopard","short_names":["leopard"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":548,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RABBIT","unified":"1F407","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f407.png","sheet_x":10,"sheet_y":53,"short_name":"rabbit2","short_names":["rabbit2"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":579,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK CAT","unified":"1F408-200D-2B1B","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f408-200d-2b1b.png","sheet_x":10,"sheet_y":54,"short_name":"black_cat","short_names":["black_cat"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":544,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CAT","unified":"1F408","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f408.png","sheet_x":10,"sheet_y":55,"short_name":"cat2","short_names":["cat2"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":543,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DRAGON","unified":"1F409","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f409.png","sheet_x":10,"sheet_y":56,"short_name":"dragon","short_names":["dragon"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-reptile","sort_order":618,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CROCODILE","unified":"1F40A","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f40a.png","sheet_x":10,"sheet_y":57,"short_name":"crocodile","short_names":["crocodile"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-reptile","sort_order":613,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WHALE","unified":"1F40B","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f40b.png","sheet_x":10,"sheet_y":58,"short_name":"whale2","short_names":["whale2"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-marine","sort_order":622,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SNAIL","unified":"1F40C","non_qualified":null,"docomo":"E74E","au":"EB7E","softbank":null,"google":"FE1B9","image":"1f40c.png","sheet_x":10,"sheet_y":59,"short_name":"snail","short_names":["snail"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bug","sort_order":632,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SNAKE","unified":"1F40D","non_qualified":null,"docomo":null,"au":"EB22","softbank":"E52D","google":"FE1D3","image":"1f40d.png","sheet_x":10,"sheet_y":60,"short_name":"snake","short_names":["snake"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-reptile","sort_order":616,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HORSE","unified":"1F40E","non_qualified":null,"docomo":"E754","au":"E4D8","softbank":"E134","google":"FE7DC","image":"1f40e.png","sheet_x":11,"sheet_y":0,"short_name":"racehorse","short_names":["racehorse"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":550,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RAM","unified":"1F40F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f40f.png","sheet_x":11,"sheet_y":1,"short_name":"ram","short_names":["ram"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":563,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GOAT","unified":"1F410","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f410.png","sheet_x":11,"sheet_y":2,"short_name":"goat","short_names":["goat"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":565,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SHEEP","unified":"1F411","non_qualified":null,"docomo":null,"au":"E48F","softbank":"E529","google":"FE1CF","image":"1f411.png","sheet_x":11,"sheet_y":3,"short_name":"sheep","short_names":["sheep"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":564,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MONKEY","unified":"1F412","non_qualified":null,"docomo":null,"au":"E4D9","softbank":"E528","google":"FE1CE","image":"1f412.png","sheet_x":11,"sheet_y":4,"short_name":"monkey","short_names":["monkey"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":531,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ROOSTER","unified":"1F413","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f413.png","sheet_x":11,"sheet_y":5,"short_name":"rooster","short_names":["rooster"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bird","sort_order":596,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CHICKEN","unified":"1F414","non_qualified":null,"docomo":null,"au":"EB23","softbank":"E52E","google":"FE1D4","image":"1f414.png","sheet_x":11,"sheet_y":6,"short_name":"chicken","short_names":["chicken"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bird","sort_order":595,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SERVICE DOG","unified":"1F415-200D-1F9BA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f415-200d-1f9ba.png","sheet_x":11,"sheet_y":7,"short_name":"service_dog","short_names":["service_dog"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":537,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DOG","unified":"1F415","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f415.png","sheet_x":11,"sheet_y":8,"short_name":"dog2","short_names":["dog2"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":535,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PIG","unified":"1F416","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f416.png","sheet_x":11,"sheet_y":9,"short_name":"pig2","short_names":["pig2"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":560,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BOAR","unified":"1F417","non_qualified":null,"docomo":null,"au":"EB24","softbank":"E52F","google":"FE1D5","image":"1f417.png","sheet_x":11,"sheet_y":10,"short_name":"boar","short_names":["boar"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":561,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ELEPHANT","unified":"1F418","non_qualified":null,"docomo":null,"au":"EB1F","softbank":"E526","google":"FE1CC","image":"1f418.png","sheet_x":11,"sheet_y":11,"short_name":"elephant","short_names":["elephant"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":570,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"OCTOPUS","unified":"1F419","non_qualified":null,"docomo":null,"au":"E5C7","softbank":"E10A","google":"FE1C5","image":"1f419.png","sheet_x":11,"sheet_y":12,"short_name":"octopus","short_names":["octopus"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-marine","sort_order":629,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SPIRAL SHELL","unified":"1F41A","non_qualified":null,"docomo":null,"au":"EAEC","softbank":"E441","google":"FE1C6","image":"1f41a.png","sheet_x":11,"sheet_y":13,"short_name":"shell","short_names":["shell"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-marine","sort_order":630,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BUG","unified":"1F41B","non_qualified":null,"docomo":null,"au":"EB1E","softbank":"E525","google":"FE1CB","image":"1f41b.png","sheet_x":11,"sheet_y":14,"short_name":"bug","short_names":["bug"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bug","sort_order":634,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ANT","unified":"1F41C","non_qualified":null,"docomo":null,"au":"E4DD","softbank":null,"google":"FE1DA","image":"1f41c.png","sheet_x":11,"sheet_y":15,"short_name":"ant","short_names":["ant"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bug","sort_order":635,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HONEYBEE","unified":"1F41D","non_qualified":null,"docomo":null,"au":"EB57","softbank":null,"google":"FE1E1","image":"1f41d.png","sheet_x":11,"sheet_y":16,"short_name":"bee","short_names":["bee","honeybee"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bug","sort_order":636,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LADY BEETLE","unified":"1F41E","non_qualified":null,"docomo":null,"au":"EB58","softbank":null,"google":"FE1E2","image":"1f41e.png","sheet_x":11,"sheet_y":17,"short_name":"ladybug","short_names":["ladybug","lady_beetle"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bug","sort_order":638,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FISH","unified":"1F41F","non_qualified":null,"docomo":"E751","au":"E49A","softbank":"E019","google":"FE1BD","image":"1f41f.png","sheet_x":11,"sheet_y":18,"short_name":"fish","short_names":["fish"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-marine","sort_order":625,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TROPICAL FISH","unified":"1F420","non_qualified":null,"docomo":"E751","au":"EB1D","softbank":"E522","google":"FE1C9","image":"1f420.png","sheet_x":11,"sheet_y":19,"short_name":"tropical_fish","short_names":["tropical_fish"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-marine","sort_order":626,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLOWFISH","unified":"1F421","non_qualified":null,"docomo":"E751","au":"E4D3","softbank":null,"google":"FE1D9","image":"1f421.png","sheet_x":11,"sheet_y":20,"short_name":"blowfish","short_names":["blowfish"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-marine","sort_order":627,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TURTLE","unified":"1F422","non_qualified":null,"docomo":null,"au":"E5D4","softbank":null,"google":"FE1DC","image":"1f422.png","sheet_x":11,"sheet_y":21,"short_name":"turtle","short_names":["turtle"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-reptile","sort_order":614,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HATCHING CHICK","unified":"1F423","non_qualified":null,"docomo":"E74F","au":"E5DB","softbank":null,"google":"FE1DD","image":"1f423.png","sheet_x":11,"sheet_y":22,"short_name":"hatching_chick","short_names":["hatching_chick"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bird","sort_order":597,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BABY CHICK","unified":"1F424","non_qualified":null,"docomo":"E74F","au":"E4E0","softbank":"E523","google":"FE1BA","image":"1f424.png","sheet_x":11,"sheet_y":23,"short_name":"baby_chick","short_names":["baby_chick"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bird","sort_order":598,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FRONT-FACING BABY CHICK","unified":"1F425","non_qualified":null,"docomo":"E74F","au":"EB76","softbank":null,"google":"FE1BB","image":"1f425.png","sheet_x":11,"sheet_y":24,"short_name":"hatched_chick","short_names":["hatched_chick"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bird","sort_order":599,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BIRD","unified":"1F426","non_qualified":null,"docomo":"E74F","au":"E4E0","softbank":"E521","google":"FE1C8","image":"1f426.png","sheet_x":11,"sheet_y":25,"short_name":"bird","short_names":["bird"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bird","sort_order":600,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PENGUIN","unified":"1F427","non_qualified":null,"docomo":"E750","au":"E4DC","softbank":"E055","google":"FE1BC","image":"1f427.png","sheet_x":11,"sheet_y":26,"short_name":"penguin","short_names":["penguin"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bird","sort_order":601,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"KOALA","unified":"1F428","non_qualified":null,"docomo":null,"au":"EB20","softbank":"E527","google":"FE1CD","image":"1f428.png","sheet_x":11,"sheet_y":27,"short_name":"koala","short_names":["koala"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":586,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"POODLE","unified":"1F429","non_qualified":null,"docomo":"E6A1","au":"E4DF","softbank":null,"google":"FE1D8","image":"1f429.png","sheet_x":11,"sheet_y":28,"short_name":"poodle","short_names":["poodle"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":538,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DROMEDARY CAMEL","unified":"1F42A","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f42a.png","sheet_x":11,"sheet_y":29,"short_name":"dromedary_camel","short_names":["dromedary_camel"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":566,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BACTRIAN CAMEL","unified":"1F42B","non_qualified":null,"docomo":null,"au":"EB25","softbank":"E530","google":"FE1D6","image":"1f42b.png","sheet_x":11,"sheet_y":30,"short_name":"camel","short_names":["camel"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":567,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DOLPHIN","unified":"1F42C","non_qualified":null,"docomo":null,"au":"EB1B","softbank":"E520","google":"FE1C7","image":"1f42c.png","sheet_x":11,"sheet_y":31,"short_name":"dolphin","short_names":["dolphin","flipper"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-marine","sort_order":623,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MOUSE FACE","unified":"1F42D","non_qualified":null,"docomo":null,"au":"E5C2","softbank":"E053","google":"FE1C2","image":"1f42d.png","sheet_x":11,"sheet_y":32,"short_name":"mouse","short_names":["mouse"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":574,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"COW FACE","unified":"1F42E","non_qualified":null,"docomo":null,"au":"EB21","softbank":"E52B","google":"FE1D1","image":"1f42e.png","sheet_x":11,"sheet_y":33,"short_name":"cow","short_names":["cow"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":555,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TIGER FACE","unified":"1F42F","non_qualified":null,"docomo":null,"au":"E5C0","softbank":"E050","google":"FE1C0","image":"1f42f.png","sheet_x":11,"sheet_y":34,"short_name":"tiger","short_names":["tiger"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":546,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RABBIT FACE","unified":"1F430","non_qualified":null,"docomo":null,"au":"E4D7","softbank":"E52C","google":"FE1D2","image":"1f430.png","sheet_x":11,"sheet_y":35,"short_name":"rabbit","short_names":["rabbit"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":578,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CAT FACE","unified":"1F431","non_qualified":null,"docomo":"E6A2","au":"E4DB","softbank":"E04F","google":"FE1B8","image":"1f431.png","sheet_x":11,"sheet_y":36,"short_name":"cat","short_names":["cat"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":542,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DRAGON FACE","unified":"1F432","non_qualified":null,"docomo":null,"au":"EB3F","softbank":null,"google":"FE1DE","image":"1f432.png","sheet_x":11,"sheet_y":37,"short_name":"dragon_face","short_names":["dragon_face"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-reptile","sort_order":617,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SPOUTING WHALE","unified":"1F433","non_qualified":null,"docomo":null,"au":"E470","softbank":"E054","google":"FE1C3","image":"1f433.png","sheet_x":11,"sheet_y":38,"short_name":"whale","short_names":["whale"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-marine","sort_order":621,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HORSE FACE","unified":"1F434","non_qualified":null,"docomo":"E754","au":"E4D8","softbank":"E01A","google":"FE1BE","image":"1f434.png","sheet_x":11,"sheet_y":39,"short_name":"horse","short_names":["horse"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":549,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MONKEY FACE","unified":"1F435","non_qualified":null,"docomo":null,"au":"E4D9","softbank":"E109","google":"FE1C4","image":"1f435.png","sheet_x":11,"sheet_y":40,"short_name":"monkey_face","short_names":["monkey_face"],"text":null,"texts":[":o)"],"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":530,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DOG FACE","unified":"1F436","non_qualified":null,"docomo":"E6A1","au":"E4E1","softbank":"E052","google":"FE1B7","image":"1f436.png","sheet_x":11,"sheet_y":41,"short_name":"dog","short_names":["dog"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":534,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PIG FACE","unified":"1F437","non_qualified":null,"docomo":"E755","au":"E4DE","softbank":"E10B","google":"FE1BF","image":"1f437.png","sheet_x":11,"sheet_y":42,"short_name":"pig","short_names":["pig"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":559,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FROG FACE","unified":"1F438","non_qualified":null,"docomo":null,"au":"E4DA","softbank":"E531","google":"FE1D7","image":"1f438.png","sheet_x":11,"sheet_y":43,"short_name":"frog","short_names":["frog"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-amphibian","sort_order":612,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HAMSTER FACE","unified":"1F439","non_qualified":null,"docomo":null,"au":null,"softbank":"E524","google":"FE1CA","image":"1f439.png","sheet_x":11,"sheet_y":44,"short_name":"hamster","short_names":["hamster"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":577,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WOLF FACE","unified":"1F43A","non_qualified":null,"docomo":"E6A1","au":"E4E1","softbank":"E52A","google":"FE1D0","image":"1f43a.png","sheet_x":11,"sheet_y":45,"short_name":"wolf","short_names":["wolf"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":539,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"POLAR BEAR","unified":"1F43B-200D-2744-FE0F","non_qualified":"1F43B-200D-2744","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f43b-200d-2744-fe0f.png","sheet_x":11,"sheet_y":46,"short_name":"polar_bear","short_names":["polar_bear"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":585,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BEAR FACE","unified":"1F43B","non_qualified":null,"docomo":null,"au":"E5C1","softbank":"E051","google":"FE1C1","image":"1f43b.png","sheet_x":11,"sheet_y":47,"short_name":"bear","short_names":["bear"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":584,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PANDA FACE","unified":"1F43C","non_qualified":null,"docomo":null,"au":"EB46","softbank":null,"google":"FE1DF","image":"1f43c.png","sheet_x":11,"sheet_y":48,"short_name":"panda_face","short_names":["panda_face"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":587,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PIG NOSE","unified":"1F43D","non_qualified":null,"docomo":"E755","au":"EB48","softbank":null,"google":"FE1E0","image":"1f43d.png","sheet_x":11,"sheet_y":49,"short_name":"pig_nose","short_names":["pig_nose"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":562,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PAW PRINTS","unified":"1F43E","non_qualified":null,"docomo":"E698","au":"E4EE","softbank":null,"google":"FE1DB","image":"1f43e.png","sheet_x":11,"sheet_y":50,"short_name":"feet","short_names":["feet","paw_prints"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":593,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CHIPMUNK","unified":"1F43F-FE0F","non_qualified":"1F43F","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f43f-fe0f.png","sheet_x":11,"sheet_y":51,"short_name":"chipmunk","short_names":["chipmunk"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":580,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EYES","unified":"1F440","non_qualified":null,"docomo":"E691","au":"E5A4","softbank":"E419","google":"FE190","image":"1f440.png","sheet_x":11,"sheet_y":52,"short_name":"eyes","short_names":["eyes"],"text":null,"texts":null,"category":"People & Body","subcategory":"body-parts","sort_order":218,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EYE IN SPEECH BUBBLE","unified":"1F441-FE0F-200D-1F5E8-FE0F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f441-fe0f-200d-1f5e8-fe0f.png","sheet_x":11,"sheet_y":53,"short_name":"eye-in-speech-bubble","short_names":["eye-in-speech-bubble"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":159,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"EYE","unified":"1F441-FE0F","non_qualified":"1F441","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f441-fe0f.png","sheet_x":11,"sheet_y":54,"short_name":"eye","short_names":["eye"],"text":null,"texts":null,"category":"People & Body","subcategory":"body-parts","sort_order":219,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EAR","unified":"1F442","non_qualified":null,"docomo":"E692","au":"E5A5","softbank":"E41B","google":"FE191","image":"1f442.png","sheet_x":11,"sheet_y":55,"short_name":"ear","short_names":["ear"],"text":null,"texts":null,"category":"People & Body","subcategory":"body-parts","sort_order":210,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F442-1F3FB","non_qualified":null,"image":"1f442-1f3fb.png","sheet_x":11,"sheet_y":56,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F442-1F3FC","non_qualified":null,"image":"1f442-1f3fc.png","sheet_x":11,"sheet_y":57,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F442-1F3FD","non_qualified":null,"image":"1f442-1f3fd.png","sheet_x":11,"sheet_y":58,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F442-1F3FE","non_qualified":null,"image":"1f442-1f3fe.png","sheet_x":11,"sheet_y":59,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F442-1F3FF","non_qualified":null,"image":"1f442-1f3ff.png","sheet_x":11,"sheet_y":60,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"NOSE","unified":"1F443","non_qualified":null,"docomo":null,"au":"EAD0","softbank":"E41A","google":"FE192","image":"1f443.png","sheet_x":12,"sheet_y":0,"short_name":"nose","short_names":["nose"],"text":null,"texts":null,"category":"People & Body","subcategory":"body-parts","sort_order":212,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F443-1F3FB","non_qualified":null,"image":"1f443-1f3fb.png","sheet_x":12,"sheet_y":1,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F443-1F3FC","non_qualified":null,"image":"1f443-1f3fc.png","sheet_x":12,"sheet_y":2,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F443-1F3FD","non_qualified":null,"image":"1f443-1f3fd.png","sheet_x":12,"sheet_y":3,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F443-1F3FE","non_qualified":null,"image":"1f443-1f3fe.png","sheet_x":12,"sheet_y":4,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F443-1F3FF","non_qualified":null,"image":"1f443-1f3ff.png","sheet_x":12,"sheet_y":5,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MOUTH","unified":"1F444","non_qualified":null,"docomo":"E6F9","au":"EAD1","softbank":"E41C","google":"FE193","image":"1f444.png","sheet_x":12,"sheet_y":6,"short_name":"lips","short_names":["lips"],"text":null,"texts":null,"category":"People & Body","subcategory":"body-parts","sort_order":221,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TONGUE","unified":"1F445","non_qualified":null,"docomo":"E728","au":"EB47","softbank":null,"google":"FE194","image":"1f445.png","sheet_x":12,"sheet_y":7,"short_name":"tongue","short_names":["tongue"],"text":null,"texts":null,"category":"People & Body","subcategory":"body-parts","sort_order":220,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WHITE UP POINTING BACKHAND INDEX","unified":"1F446","non_qualified":null,"docomo":null,"au":"EA8D","softbank":"E22E","google":"FEB99","image":"1f446.png","sheet_x":12,"sheet_y":8,"short_name":"point_up_2","short_names":["point_up_2"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-single-finger","sort_order":184,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F446-1F3FB","non_qualified":null,"image":"1f446-1f3fb.png","sheet_x":12,"sheet_y":9,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F446-1F3FC","non_qualified":null,"image":"1f446-1f3fc.png","sheet_x":12,"sheet_y":10,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F446-1F3FD","non_qualified":null,"image":"1f446-1f3fd.png","sheet_x":12,"sheet_y":11,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F446-1F3FE","non_qualified":null,"image":"1f446-1f3fe.png","sheet_x":12,"sheet_y":12,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F446-1F3FF","non_qualified":null,"image":"1f446-1f3ff.png","sheet_x":12,"sheet_y":13,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WHITE DOWN POINTING BACKHAND INDEX","unified":"1F447","non_qualified":null,"docomo":null,"au":"EA8E","softbank":"E22F","google":"FEB9A","image":"1f447.png","sheet_x":12,"sheet_y":14,"short_name":"point_down","short_names":["point_down"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-single-finger","sort_order":186,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F447-1F3FB","non_qualified":null,"image":"1f447-1f3fb.png","sheet_x":12,"sheet_y":15,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F447-1F3FC","non_qualified":null,"image":"1f447-1f3fc.png","sheet_x":12,"sheet_y":16,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F447-1F3FD","non_qualified":null,"image":"1f447-1f3fd.png","sheet_x":12,"sheet_y":17,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F447-1F3FE","non_qualified":null,"image":"1f447-1f3fe.png","sheet_x":12,"sheet_y":18,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F447-1F3FF","non_qualified":null,"image":"1f447-1f3ff.png","sheet_x":12,"sheet_y":19,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WHITE LEFT POINTING BACKHAND INDEX","unified":"1F448","non_qualified":null,"docomo":null,"au":"E4FF","softbank":"E230","google":"FEB9B","image":"1f448.png","sheet_x":12,"sheet_y":20,"short_name":"point_left","short_names":["point_left"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-single-finger","sort_order":182,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F448-1F3FB","non_qualified":null,"image":"1f448-1f3fb.png","sheet_x":12,"sheet_y":21,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F448-1F3FC","non_qualified":null,"image":"1f448-1f3fc.png","sheet_x":12,"sheet_y":22,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F448-1F3FD","non_qualified":null,"image":"1f448-1f3fd.png","sheet_x":12,"sheet_y":23,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F448-1F3FE","non_qualified":null,"image":"1f448-1f3fe.png","sheet_x":12,"sheet_y":24,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F448-1F3FF","non_qualified":null,"image":"1f448-1f3ff.png","sheet_x":12,"sheet_y":25,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WHITE RIGHT POINTING BACKHAND INDEX","unified":"1F449","non_qualified":null,"docomo":null,"au":"E500","softbank":"E231","google":"FEB9C","image":"1f449.png","sheet_x":12,"sheet_y":26,"short_name":"point_right","short_names":["point_right"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-single-finger","sort_order":183,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F449-1F3FB","non_qualified":null,"image":"1f449-1f3fb.png","sheet_x":12,"sheet_y":27,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F449-1F3FC","non_qualified":null,"image":"1f449-1f3fc.png","sheet_x":12,"sheet_y":28,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F449-1F3FD","non_qualified":null,"image":"1f449-1f3fd.png","sheet_x":12,"sheet_y":29,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F449-1F3FE","non_qualified":null,"image":"1f449-1f3fe.png","sheet_x":12,"sheet_y":30,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F449-1F3FF","non_qualified":null,"image":"1f449-1f3ff.png","sheet_x":12,"sheet_y":31,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"FISTED HAND SIGN","unified":"1F44A","non_qualified":null,"docomo":"E6FD","au":"E4F3","softbank":"E00D","google":"FEB96","image":"1f44a.png","sheet_x":12,"sheet_y":32,"short_name":"facepunch","short_names":["facepunch","punch"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-closed","sort_order":192,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F44A-1F3FB","non_qualified":null,"image":"1f44a-1f3fb.png","sheet_x":12,"sheet_y":33,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F44A-1F3FC","non_qualified":null,"image":"1f44a-1f3fc.png","sheet_x":12,"sheet_y":34,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F44A-1F3FD","non_qualified":null,"image":"1f44a-1f3fd.png","sheet_x":12,"sheet_y":35,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F44A-1F3FE","non_qualified":null,"image":"1f44a-1f3fe.png","sheet_x":12,"sheet_y":36,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F44A-1F3FF","non_qualified":null,"image":"1f44a-1f3ff.png","sheet_x":12,"sheet_y":37,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WAVING HAND SIGN","unified":"1F44B","non_qualified":null,"docomo":"E695","au":"EAD6","softbank":"E41E","google":"FEB9D","image":"1f44b.png","sheet_x":12,"sheet_y":38,"short_name":"wave","short_names":["wave"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-open","sort_order":164,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F44B-1F3FB","non_qualified":null,"image":"1f44b-1f3fb.png","sheet_x":12,"sheet_y":39,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F44B-1F3FC","non_qualified":null,"image":"1f44b-1f3fc.png","sheet_x":12,"sheet_y":40,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F44B-1F3FD","non_qualified":null,"image":"1f44b-1f3fd.png","sheet_x":12,"sheet_y":41,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F44B-1F3FE","non_qualified":null,"image":"1f44b-1f3fe.png","sheet_x":12,"sheet_y":42,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F44B-1F3FF","non_qualified":null,"image":"1f44b-1f3ff.png","sheet_x":12,"sheet_y":43,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"OK HAND SIGN","unified":"1F44C","non_qualified":null,"docomo":"E70B","au":"EAD4","softbank":"E420","google":"FEB9F","image":"1f44c.png","sheet_x":12,"sheet_y":44,"short_name":"ok_hand","short_names":["ok_hand"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-partial","sort_order":173,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F44C-1F3FB","non_qualified":null,"image":"1f44c-1f3fb.png","sheet_x":12,"sheet_y":45,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F44C-1F3FC","non_qualified":null,"image":"1f44c-1f3fc.png","sheet_x":12,"sheet_y":46,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F44C-1F3FD","non_qualified":null,"image":"1f44c-1f3fd.png","sheet_x":12,"sheet_y":47,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F44C-1F3FE","non_qualified":null,"image":"1f44c-1f3fe.png","sheet_x":12,"sheet_y":48,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F44C-1F3FF","non_qualified":null,"image":"1f44c-1f3ff.png","sheet_x":12,"sheet_y":49,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"THUMBS UP SIGN","unified":"1F44D","non_qualified":null,"docomo":"E727","au":"E4F9","softbank":"E00E","google":"FEB97","image":"1f44d.png","sheet_x":12,"sheet_y":50,"short_name":"+1","short_names":["+1","thumbsup"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-closed","sort_order":189,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F44D-1F3FB","non_qualified":null,"image":"1f44d-1f3fb.png","sheet_x":12,"sheet_y":51,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F44D-1F3FC","non_qualified":null,"image":"1f44d-1f3fc.png","sheet_x":12,"sheet_y":52,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F44D-1F3FD","non_qualified":null,"image":"1f44d-1f3fd.png","sheet_x":12,"sheet_y":53,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F44D-1F3FE","non_qualified":null,"image":"1f44d-1f3fe.png","sheet_x":12,"sheet_y":54,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F44D-1F3FF","non_qualified":null,"image":"1f44d-1f3ff.png","sheet_x":12,"sheet_y":55,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"THUMBS DOWN SIGN","unified":"1F44E","non_qualified":null,"docomo":"E700","au":"EAD5","softbank":"E421","google":"FEBA0","image":"1f44e.png","sheet_x":12,"sheet_y":56,"short_name":"-1","short_names":["-1","thumbsdown"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-closed","sort_order":190,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F44E-1F3FB","non_qualified":null,"image":"1f44e-1f3fb.png","sheet_x":12,"sheet_y":57,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F44E-1F3FC","non_qualified":null,"image":"1f44e-1f3fc.png","sheet_x":12,"sheet_y":58,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F44E-1F3FD","non_qualified":null,"image":"1f44e-1f3fd.png","sheet_x":12,"sheet_y":59,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F44E-1F3FE","non_qualified":null,"image":"1f44e-1f3fe.png","sheet_x":12,"sheet_y":60,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F44E-1F3FF","non_qualified":null,"image":"1f44e-1f3ff.png","sheet_x":13,"sheet_y":0,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"CLAPPING HANDS SIGN","unified":"1F44F","non_qualified":null,"docomo":null,"au":"EAD3","softbank":"E41F","google":"FEB9E","image":"1f44f.png","sheet_x":13,"sheet_y":1,"short_name":"clap","short_names":["clap"],"text":null,"texts":null,"category":"People & Body","subcategory":"hands","sort_order":195,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F44F-1F3FB","non_qualified":null,"image":"1f44f-1f3fb.png","sheet_x":13,"sheet_y":2,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F44F-1F3FC","non_qualified":null,"image":"1f44f-1f3fc.png","sheet_x":13,"sheet_y":3,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F44F-1F3FD","non_qualified":null,"image":"1f44f-1f3fd.png","sheet_x":13,"sheet_y":4,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F44F-1F3FE","non_qualified":null,"image":"1f44f-1f3fe.png","sheet_x":13,"sheet_y":5,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F44F-1F3FF","non_qualified":null,"image":"1f44f-1f3ff.png","sheet_x":13,"sheet_y":6,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"OPEN HANDS SIGN","unified":"1F450","non_qualified":null,"docomo":"E695","au":"EAD6","softbank":"E422","google":"FEBA1","image":"1f450.png","sheet_x":13,"sheet_y":7,"short_name":"open_hands","short_names":["open_hands"],"text":null,"texts":null,"category":"People & Body","subcategory":"hands","sort_order":198,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F450-1F3FB","non_qualified":null,"image":"1f450-1f3fb.png","sheet_x":13,"sheet_y":8,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F450-1F3FC","non_qualified":null,"image":"1f450-1f3fc.png","sheet_x":13,"sheet_y":9,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F450-1F3FD","non_qualified":null,"image":"1f450-1f3fd.png","sheet_x":13,"sheet_y":10,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F450-1F3FE","non_qualified":null,"image":"1f450-1f3fe.png","sheet_x":13,"sheet_y":11,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F450-1F3FF","non_qualified":null,"image":"1f450-1f3ff.png","sheet_x":13,"sheet_y":12,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"CROWN","unified":"1F451","non_qualified":null,"docomo":"E71A","au":"E5C9","softbank":"E10E","google":"FE4D1","image":"1f451.png","sheet_x":13,"sheet_y":13,"short_name":"crown","short_names":["crown"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1144,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WOMANS HAT","unified":"1F452","non_qualified":null,"docomo":null,"au":"EA9E","softbank":"E318","google":"FE4D4","image":"1f452.png","sheet_x":13,"sheet_y":14,"short_name":"womans_hat","short_names":["womans_hat"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1145,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EYEGLASSES","unified":"1F453","non_qualified":null,"docomo":"E69A","au":"E4FE","softbank":null,"google":"FE4CE","image":"1f453.png","sheet_x":13,"sheet_y":15,"short_name":"eyeglasses","short_names":["eyeglasses"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1110,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NECKTIE","unified":"1F454","non_qualified":null,"docomo":null,"au":"EA93","softbank":"E302","google":"FE4D3","image":"1f454.png","sheet_x":13,"sheet_y":16,"short_name":"necktie","short_names":["necktie"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1115,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"T-SHIRT","unified":"1F455","non_qualified":null,"docomo":"E70E","au":"E5B6","softbank":"E006","google":"FE4CF","image":"1f455.png","sheet_x":13,"sheet_y":17,"short_name":"shirt","short_names":["shirt","tshirt"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1116,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"JEANS","unified":"1F456","non_qualified":null,"docomo":"E711","au":"EB77","softbank":null,"google":"FE4D0","image":"1f456.png","sheet_x":13,"sheet_y":18,"short_name":"jeans","short_names":["jeans"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1117,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DRESS","unified":"1F457","non_qualified":null,"docomo":null,"au":"EB6B","softbank":"E319","google":"FE4D5","image":"1f457.png","sheet_x":13,"sheet_y":19,"short_name":"dress","short_names":["dress"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1122,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"KIMONO","unified":"1F458","non_qualified":null,"docomo":null,"au":"EAA3","softbank":"E321","google":"FE4D9","image":"1f458.png","sheet_x":13,"sheet_y":20,"short_name":"kimono","short_names":["kimono"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1123,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BIKINI","unified":"1F459","non_qualified":null,"docomo":null,"au":"EAA4","softbank":"E322","google":"FE4DA","image":"1f459.png","sheet_x":13,"sheet_y":21,"short_name":"bikini","short_names":["bikini"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1128,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WOMANS CLOTHES","unified":"1F45A","non_qualified":null,"docomo":"E70E","au":"E50D","softbank":null,"google":"FE4DB","image":"1f45a.png","sheet_x":13,"sheet_y":22,"short_name":"womans_clothes","short_names":["womans_clothes"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1129,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PURSE","unified":"1F45B","non_qualified":null,"docomo":"E70F","au":"E504","softbank":null,"google":"FE4DC","image":"1f45b.png","sheet_x":13,"sheet_y":23,"short_name":"purse","short_names":["purse"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1130,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HANDBAG","unified":"1F45C","non_qualified":null,"docomo":"E682","au":"E49C","softbank":"E323","google":"FE4F0","image":"1f45c.png","sheet_x":13,"sheet_y":24,"short_name":"handbag","short_names":["handbag"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1131,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"POUCH","unified":"1F45D","non_qualified":null,"docomo":"E6AD","au":null,"softbank":null,"google":"FE4F1","image":"1f45d.png","sheet_x":13,"sheet_y":25,"short_name":"pouch","short_names":["pouch"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1132,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MANS SHOE","unified":"1F45E","non_qualified":null,"docomo":"E699","au":"E5B7","softbank":null,"google":"FE4CC","image":"1f45e.png","sheet_x":13,"sheet_y":26,"short_name":"mans_shoe","short_names":["mans_shoe","shoe"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1136,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ATHLETIC SHOE","unified":"1F45F","non_qualified":null,"docomo":"E699","au":"EB2B","softbank":"E007","google":"FE4CD","image":"1f45f.png","sheet_x":13,"sheet_y":27,"short_name":"athletic_shoe","short_names":["athletic_shoe"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1137,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HIGH-HEELED SHOE","unified":"1F460","non_qualified":null,"docomo":"E674","au":"E51A","softbank":"E13E","google":"FE4D6","image":"1f460.png","sheet_x":13,"sheet_y":28,"short_name":"high_heel","short_names":["high_heel"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1140,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WOMANS SANDAL","unified":"1F461","non_qualified":null,"docomo":"E674","au":"E51A","softbank":"E31A","google":"FE4D7","image":"1f461.png","sheet_x":13,"sheet_y":29,"short_name":"sandal","short_names":["sandal"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1141,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WOMANS BOOTS","unified":"1F462","non_qualified":null,"docomo":null,"au":"EA9F","softbank":"E31B","google":"FE4D8","image":"1f462.png","sheet_x":13,"sheet_y":30,"short_name":"boot","short_names":["boot"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1143,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FOOTPRINTS","unified":"1F463","non_qualified":null,"docomo":"E698","au":"EB2A","softbank":"E536","google":"FE553","image":"1f463.png","sheet_x":13,"sheet_y":31,"short_name":"footprints","short_names":["footprints"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-symbol","sort_order":524,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BUST IN SILHOUETTE","unified":"1F464","non_qualified":null,"docomo":"E6B1","au":null,"softbank":null,"google":"FE19A","image":"1f464.png","sheet_x":13,"sheet_y":32,"short_name":"bust_in_silhouette","short_names":["bust_in_silhouette"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-symbol","sort_order":521,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BUSTS IN SILHOUETTE","unified":"1F465","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f465.png","sheet_x":13,"sheet_y":33,"short_name":"busts_in_silhouette","short_names":["busts_in_silhouette"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-symbol","sort_order":522,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BOY","unified":"1F466","non_qualified":null,"docomo":"E6F0","au":"E4FC","softbank":"E001","google":"FE19B","image":"1f466.png","sheet_x":13,"sheet_y":34,"short_name":"boy","short_names":["boy"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":225,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F466-1F3FB","non_qualified":null,"image":"1f466-1f3fb.png","sheet_x":13,"sheet_y":35,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F466-1F3FC","non_qualified":null,"image":"1f466-1f3fc.png","sheet_x":13,"sheet_y":36,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F466-1F3FD","non_qualified":null,"image":"1f466-1f3fd.png","sheet_x":13,"sheet_y":37,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F466-1F3FE","non_qualified":null,"image":"1f466-1f3fe.png","sheet_x":13,"sheet_y":38,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F466-1F3FF","non_qualified":null,"image":"1f466-1f3ff.png","sheet_x":13,"sheet_y":39,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"GIRL","unified":"1F467","non_qualified":null,"docomo":"E6F0","au":"E4FA","softbank":"E002","google":"FE19C","image":"1f467.png","sheet_x":13,"sheet_y":40,"short_name":"girl","short_names":["girl"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":226,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F467-1F3FB","non_qualified":null,"image":"1f467-1f3fb.png","sheet_x":13,"sheet_y":41,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F467-1F3FC","non_qualified":null,"image":"1f467-1f3fc.png","sheet_x":13,"sheet_y":42,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F467-1F3FD","non_qualified":null,"image":"1f467-1f3fd.png","sheet_x":13,"sheet_y":43,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F467-1F3FE","non_qualified":null,"image":"1f467-1f3fe.png","sheet_x":13,"sheet_y":44,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F467-1F3FF","non_qualified":null,"image":"1f467-1f3ff.png","sheet_x":13,"sheet_y":45,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN FARMER","unified":"1F468-200D-1F33E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f33e.png","sheet_x":13,"sheet_y":46,"short_name":"male-farmer","short_names":["male-farmer"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":294,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F33E","non_qualified":null,"image":"1f468-1f3fb-200d-1f33e.png","sheet_x":13,"sheet_y":47,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-1F33E","non_qualified":null,"image":"1f468-1f3fc-200d-1f33e.png","sheet_x":13,"sheet_y":48,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-1F33E","non_qualified":null,"image":"1f468-1f3fd-200d-1f33e.png","sheet_x":13,"sheet_y":49,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-1F33E","non_qualified":null,"image":"1f468-1f3fe-200d-1f33e.png","sheet_x":13,"sheet_y":50,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-1F33E","non_qualified":null,"image":"1f468-1f3ff-200d-1f33e.png","sheet_x":13,"sheet_y":51,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN COOK","unified":"1F468-200D-1F373","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f373.png","sheet_x":13,"sheet_y":52,"short_name":"male-cook","short_names":["male-cook"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":297,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F373","non_qualified":null,"image":"1f468-1f3fb-200d-1f373.png","sheet_x":13,"sheet_y":53,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-1F373","non_qualified":null,"image":"1f468-1f3fc-200d-1f373.png","sheet_x":13,"sheet_y":54,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-1F373","non_qualified":null,"image":"1f468-1f3fd-200d-1f373.png","sheet_x":13,"sheet_y":55,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-1F373","non_qualified":null,"image":"1f468-1f3fe-200d-1f373.png","sheet_x":13,"sheet_y":56,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-1F373","non_qualified":null,"image":"1f468-1f3ff-200d-1f373.png","sheet_x":13,"sheet_y":57,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN FEEDING BABY","unified":"1F468-200D-1F37C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f37c.png","sheet_x":13,"sheet_y":58,"short_name":"man_feeding_baby","short_names":["man_feeding_baby"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":361,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F37C","non_qualified":null,"image":"1f468-1f3fb-200d-1f37c.png","sheet_x":13,"sheet_y":59,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-1F37C","non_qualified":null,"image":"1f468-1f3fc-200d-1f37c.png","sheet_x":13,"sheet_y":60,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-1F37C","non_qualified":null,"image":"1f468-1f3fd-200d-1f37c.png","sheet_x":14,"sheet_y":0,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-1F37C","non_qualified":null,"image":"1f468-1f3fe-200d-1f37c.png","sheet_x":14,"sheet_y":1,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-1F37C","non_qualified":null,"image":"1f468-1f3ff-200d-1f37c.png","sheet_x":14,"sheet_y":2,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN STUDENT","unified":"1F468-200D-1F393","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f393.png","sheet_x":14,"sheet_y":3,"short_name":"male-student","short_names":["male-student"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":285,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F393","non_qualified":null,"image":"1f468-1f3fb-200d-1f393.png","sheet_x":14,"sheet_y":4,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-1F393","non_qualified":null,"image":"1f468-1f3fc-200d-1f393.png","sheet_x":14,"sheet_y":5,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-1F393","non_qualified":null,"image":"1f468-1f3fd-200d-1f393.png","sheet_x":14,"sheet_y":6,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-1F393","non_qualified":null,"image":"1f468-1f3fe-200d-1f393.png","sheet_x":14,"sheet_y":7,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-1F393","non_qualified":null,"image":"1f468-1f3ff-200d-1f393.png","sheet_x":14,"sheet_y":8,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN SINGER","unified":"1F468-200D-1F3A4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f3a4.png","sheet_x":14,"sheet_y":9,"short_name":"male-singer","short_names":["male-singer"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":315,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F3A4","non_qualified":null,"image":"1f468-1f3fb-200d-1f3a4.png","sheet_x":14,"sheet_y":10,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-1F3A4","non_qualified":null,"image":"1f468-1f3fc-200d-1f3a4.png","sheet_x":14,"sheet_y":11,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-1F3A4","non_qualified":null,"image":"1f468-1f3fd-200d-1f3a4.png","sheet_x":14,"sheet_y":12,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-1F3A4","non_qualified":null,"image":"1f468-1f3fe-200d-1f3a4.png","sheet_x":14,"sheet_y":13,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-1F3A4","non_qualified":null,"image":"1f468-1f3ff-200d-1f3a4.png","sheet_x":14,"sheet_y":14,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN ARTIST","unified":"1F468-200D-1F3A8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f3a8.png","sheet_x":14,"sheet_y":15,"short_name":"male-artist","short_names":["male-artist"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":318,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F3A8","non_qualified":null,"image":"1f468-1f3fb-200d-1f3a8.png","sheet_x":14,"sheet_y":16,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-1F3A8","non_qualified":null,"image":"1f468-1f3fc-200d-1f3a8.png","sheet_x":14,"sheet_y":17,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-1F3A8","non_qualified":null,"image":"1f468-1f3fd-200d-1f3a8.png","sheet_x":14,"sheet_y":18,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-1F3A8","non_qualified":null,"image":"1f468-1f3fe-200d-1f3a8.png","sheet_x":14,"sheet_y":19,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-1F3A8","non_qualified":null,"image":"1f468-1f3ff-200d-1f3a8.png","sheet_x":14,"sheet_y":20,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN TEACHER","unified":"1F468-200D-1F3EB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f3eb.png","sheet_x":14,"sheet_y":21,"short_name":"male-teacher","short_names":["male-teacher"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":288,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F3EB","non_qualified":null,"image":"1f468-1f3fb-200d-1f3eb.png","sheet_x":14,"sheet_y":22,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-1F3EB","non_qualified":null,"image":"1f468-1f3fc-200d-1f3eb.png","sheet_x":14,"sheet_y":23,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-1F3EB","non_qualified":null,"image":"1f468-1f3fd-200d-1f3eb.png","sheet_x":14,"sheet_y":24,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-1F3EB","non_qualified":null,"image":"1f468-1f3fe-200d-1f3eb.png","sheet_x":14,"sheet_y":25,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-1F3EB","non_qualified":null,"image":"1f468-1f3ff-200d-1f3eb.png","sheet_x":14,"sheet_y":26,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN FACTORY WORKER","unified":"1F468-200D-1F3ED","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f3ed.png","sheet_x":14,"sheet_y":27,"short_name":"male-factory-worker","short_names":["male-factory-worker"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":303,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F3ED","non_qualified":null,"image":"1f468-1f3fb-200d-1f3ed.png","sheet_x":14,"sheet_y":28,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-1F3ED","non_qualified":null,"image":"1f468-1f3fc-200d-1f3ed.png","sheet_x":14,"sheet_y":29,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-1F3ED","non_qualified":null,"image":"1f468-1f3fd-200d-1f3ed.png","sheet_x":14,"sheet_y":30,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-1F3ED","non_qualified":null,"image":"1f468-1f3fe-200d-1f3ed.png","sheet_x":14,"sheet_y":31,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-1F3ED","non_qualified":null,"image":"1f468-1f3ff-200d-1f3ed.png","sheet_x":14,"sheet_y":32,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"FAMILY: MAN, BOY, BOY","unified":"1F468-200D-1F466-200D-1F466","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f466-200d-1f466.png","sheet_x":14,"sheet_y":33,"short_name":"man-boy-boy","short_names":["man-boy-boy"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":511,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAMILY: MAN, BOY","unified":"1F468-200D-1F466","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f466.png","sheet_x":14,"sheet_y":34,"short_name":"man-boy","short_names":["man-boy"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":510,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAMILY: MAN, GIRL, BOY","unified":"1F468-200D-1F467-200D-1F466","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f467-200d-1f466.png","sheet_x":14,"sheet_y":35,"short_name":"man-girl-boy","short_names":["man-girl-boy"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":513,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAMILY: MAN, GIRL, GIRL","unified":"1F468-200D-1F467-200D-1F467","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f467-200d-1f467.png","sheet_x":14,"sheet_y":36,"short_name":"man-girl-girl","short_names":["man-girl-girl"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":514,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAMILY: MAN, GIRL","unified":"1F468-200D-1F467","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f467.png","sheet_x":14,"sheet_y":37,"short_name":"man-girl","short_names":["man-girl"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":512,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAMILY: MAN, MAN, BOY","unified":"1F468-200D-1F468-200D-1F466","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f468-200d-1f466.png","sheet_x":14,"sheet_y":38,"short_name":"man-man-boy","short_names":["man-man-boy"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":500,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAMILY: MAN, MAN, BOY, BOY","unified":"1F468-200D-1F468-200D-1F466-200D-1F466","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f468-200d-1f466-200d-1f466.png","sheet_x":14,"sheet_y":39,"short_name":"man-man-boy-boy","short_names":["man-man-boy-boy"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":503,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAMILY: MAN, MAN, GIRL","unified":"1F468-200D-1F468-200D-1F467","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f468-200d-1f467.png","sheet_x":14,"sheet_y":40,"short_name":"man-man-girl","short_names":["man-man-girl"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":501,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAMILY: MAN, MAN, GIRL, BOY","unified":"1F468-200D-1F468-200D-1F467-200D-1F466","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f468-200d-1f467-200d-1f466.png","sheet_x":14,"sheet_y":41,"short_name":"man-man-girl-boy","short_names":["man-man-girl-boy"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":502,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAMILY: MAN, MAN, GIRL, GIRL","unified":"1F468-200D-1F468-200D-1F467-200D-1F467","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f468-200d-1f467-200d-1f467.png","sheet_x":14,"sheet_y":42,"short_name":"man-man-girl-girl","short_names":["man-man-girl-girl"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":504,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAMILY: MAN, WOMAN, BOY","unified":"1F468-200D-1F469-200D-1F466","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f469-200d-1f466.png","sheet_x":14,"sheet_y":43,"short_name":"man-woman-boy","short_names":["man-woman-boy"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":495,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F46A"},{"name":"FAMILY: MAN, WOMAN, BOY, BOY","unified":"1F468-200D-1F469-200D-1F466-200D-1F466","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f469-200d-1f466-200d-1f466.png","sheet_x":14,"sheet_y":44,"short_name":"man-woman-boy-boy","short_names":["man-woman-boy-boy"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":498,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAMILY: MAN, WOMAN, GIRL","unified":"1F468-200D-1F469-200D-1F467","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f469-200d-1f467.png","sheet_x":14,"sheet_y":45,"short_name":"man-woman-girl","short_names":["man-woman-girl"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":496,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAMILY: MAN, WOMAN, GIRL, BOY","unified":"1F468-200D-1F469-200D-1F467-200D-1F466","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f469-200d-1f467-200d-1f466.png","sheet_x":14,"sheet_y":46,"short_name":"man-woman-girl-boy","short_names":["man-woman-girl-boy"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":497,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAMILY: MAN, WOMAN, GIRL, GIRL","unified":"1F468-200D-1F469-200D-1F467-200D-1F467","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f469-200d-1f467-200d-1f467.png","sheet_x":14,"sheet_y":47,"short_name":"man-woman-girl-girl","short_names":["man-woman-girl-girl"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":499,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MAN TECHNOLOGIST","unified":"1F468-200D-1F4BB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f4bb.png","sheet_x":14,"sheet_y":48,"short_name":"male-technologist","short_names":["male-technologist"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":312,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F4BB","non_qualified":null,"image":"1f468-1f3fb-200d-1f4bb.png","sheet_x":14,"sheet_y":49,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-1F4BB","non_qualified":null,"image":"1f468-1f3fc-200d-1f4bb.png","sheet_x":14,"sheet_y":50,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-1F4BB","non_qualified":null,"image":"1f468-1f3fd-200d-1f4bb.png","sheet_x":14,"sheet_y":51,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-1F4BB","non_qualified":null,"image":"1f468-1f3fe-200d-1f4bb.png","sheet_x":14,"sheet_y":52,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-1F4BB","non_qualified":null,"image":"1f468-1f3ff-200d-1f4bb.png","sheet_x":14,"sheet_y":53,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN OFFICE WORKER","unified":"1F468-200D-1F4BC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f4bc.png","sheet_x":14,"sheet_y":54,"short_name":"male-office-worker","short_names":["male-office-worker"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":306,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F4BC","non_qualified":null,"image":"1f468-1f3fb-200d-1f4bc.png","sheet_x":14,"sheet_y":55,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-1F4BC","non_qualified":null,"image":"1f468-1f3fc-200d-1f4bc.png","sheet_x":14,"sheet_y":56,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-1F4BC","non_qualified":null,"image":"1f468-1f3fd-200d-1f4bc.png","sheet_x":14,"sheet_y":57,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-1F4BC","non_qualified":null,"image":"1f468-1f3fe-200d-1f4bc.png","sheet_x":14,"sheet_y":58,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-1F4BC","non_qualified":null,"image":"1f468-1f3ff-200d-1f4bc.png","sheet_x":14,"sheet_y":59,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN MECHANIC","unified":"1F468-200D-1F527","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f527.png","sheet_x":14,"sheet_y":60,"short_name":"male-mechanic","short_names":["male-mechanic"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":300,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F527","non_qualified":null,"image":"1f468-1f3fb-200d-1f527.png","sheet_x":15,"sheet_y":0,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-1F527","non_qualified":null,"image":"1f468-1f3fc-200d-1f527.png","sheet_x":15,"sheet_y":1,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-1F527","non_qualified":null,"image":"1f468-1f3fd-200d-1f527.png","sheet_x":15,"sheet_y":2,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-1F527","non_qualified":null,"image":"1f468-1f3fe-200d-1f527.png","sheet_x":15,"sheet_y":3,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-1F527","non_qualified":null,"image":"1f468-1f3ff-200d-1f527.png","sheet_x":15,"sheet_y":4,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN SCIENTIST","unified":"1F468-200D-1F52C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f52c.png","sheet_x":15,"sheet_y":5,"short_name":"male-scientist","short_names":["male-scientist"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":309,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F52C","non_qualified":null,"image":"1f468-1f3fb-200d-1f52c.png","sheet_x":15,"sheet_y":6,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-1F52C","non_qualified":null,"image":"1f468-1f3fc-200d-1f52c.png","sheet_x":15,"sheet_y":7,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-1F52C","non_qualified":null,"image":"1f468-1f3fd-200d-1f52c.png","sheet_x":15,"sheet_y":8,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-1F52C","non_qualified":null,"image":"1f468-1f3fe-200d-1f52c.png","sheet_x":15,"sheet_y":9,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-1F52C","non_qualified":null,"image":"1f468-1f3ff-200d-1f52c.png","sheet_x":15,"sheet_y":10,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN ASTRONAUT","unified":"1F468-200D-1F680","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f680.png","sheet_x":15,"sheet_y":11,"short_name":"male-astronaut","short_names":["male-astronaut"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":324,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F680","non_qualified":null,"image":"1f468-1f3fb-200d-1f680.png","sheet_x":15,"sheet_y":12,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-1F680","non_qualified":null,"image":"1f468-1f3fc-200d-1f680.png","sheet_x":15,"sheet_y":13,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-1F680","non_qualified":null,"image":"1f468-1f3fd-200d-1f680.png","sheet_x":15,"sheet_y":14,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-1F680","non_qualified":null,"image":"1f468-1f3fe-200d-1f680.png","sheet_x":15,"sheet_y":15,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-1F680","non_qualified":null,"image":"1f468-1f3ff-200d-1f680.png","sheet_x":15,"sheet_y":16,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN FIREFIGHTER","unified":"1F468-200D-1F692","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f692.png","sheet_x":15,"sheet_y":17,"short_name":"male-firefighter","short_names":["male-firefighter"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":327,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F692","non_qualified":null,"image":"1f468-1f3fb-200d-1f692.png","sheet_x":15,"sheet_y":18,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-1F692","non_qualified":null,"image":"1f468-1f3fc-200d-1f692.png","sheet_x":15,"sheet_y":19,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-1F692","non_qualified":null,"image":"1f468-1f3fd-200d-1f692.png","sheet_x":15,"sheet_y":20,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-1F692","non_qualified":null,"image":"1f468-1f3fe-200d-1f692.png","sheet_x":15,"sheet_y":21,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-1F692","non_qualified":null,"image":"1f468-1f3ff-200d-1f692.png","sheet_x":15,"sheet_y":22,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN WITH WHITE CANE","unified":"1F468-200D-1F9AF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f9af.png","sheet_x":15,"sheet_y":23,"short_name":"man_with_probing_cane","short_names":["man_with_probing_cane"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":411,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F9AF","non_qualified":null,"image":"1f468-1f3fb-200d-1f9af.png","sheet_x":15,"sheet_y":24,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-1F9AF","non_qualified":null,"image":"1f468-1f3fc-200d-1f9af.png","sheet_x":15,"sheet_y":25,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-1F9AF","non_qualified":null,"image":"1f468-1f3fd-200d-1f9af.png","sheet_x":15,"sheet_y":26,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-1F9AF","non_qualified":null,"image":"1f468-1f3fe-200d-1f9af.png","sheet_x":15,"sheet_y":27,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-1F9AF","non_qualified":null,"image":"1f468-1f3ff-200d-1f9af.png","sheet_x":15,"sheet_y":28,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN: RED HAIR","unified":"1F468-200D-1F9B0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f9b0.png","sheet_x":15,"sheet_y":29,"short_name":"red_haired_man","short_names":["red_haired_man"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":233,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F9B0","non_qualified":null,"image":"1f468-1f3fb-200d-1f9b0.png","sheet_x":15,"sheet_y":30,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-1F9B0","non_qualified":null,"image":"1f468-1f3fc-200d-1f9b0.png","sheet_x":15,"sheet_y":31,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-1F9B0","non_qualified":null,"image":"1f468-1f3fd-200d-1f9b0.png","sheet_x":15,"sheet_y":32,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-1F9B0","non_qualified":null,"image":"1f468-1f3fe-200d-1f9b0.png","sheet_x":15,"sheet_y":33,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-1F9B0","non_qualified":null,"image":"1f468-1f3ff-200d-1f9b0.png","sheet_x":15,"sheet_y":34,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN: CURLY HAIR","unified":"1F468-200D-1F9B1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f9b1.png","sheet_x":15,"sheet_y":35,"short_name":"curly_haired_man","short_names":["curly_haired_man"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":234,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F9B1","non_qualified":null,"image":"1f468-1f3fb-200d-1f9b1.png","sheet_x":15,"sheet_y":36,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-1F9B1","non_qualified":null,"image":"1f468-1f3fc-200d-1f9b1.png","sheet_x":15,"sheet_y":37,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-1F9B1","non_qualified":null,"image":"1f468-1f3fd-200d-1f9b1.png","sheet_x":15,"sheet_y":38,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-1F9B1","non_qualified":null,"image":"1f468-1f3fe-200d-1f9b1.png","sheet_x":15,"sheet_y":39,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-1F9B1","non_qualified":null,"image":"1f468-1f3ff-200d-1f9b1.png","sheet_x":15,"sheet_y":40,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN: BALD","unified":"1F468-200D-1F9B2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f9b2.png","sheet_x":15,"sheet_y":41,"short_name":"bald_man","short_names":["bald_man"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":236,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F9B2","non_qualified":null,"image":"1f468-1f3fb-200d-1f9b2.png","sheet_x":15,"sheet_y":42,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-1F9B2","non_qualified":null,"image":"1f468-1f3fc-200d-1f9b2.png","sheet_x":15,"sheet_y":43,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-1F9B2","non_qualified":null,"image":"1f468-1f3fd-200d-1f9b2.png","sheet_x":15,"sheet_y":44,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-1F9B2","non_qualified":null,"image":"1f468-1f3fe-200d-1f9b2.png","sheet_x":15,"sheet_y":45,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-1F9B2","non_qualified":null,"image":"1f468-1f3ff-200d-1f9b2.png","sheet_x":15,"sheet_y":46,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN: WHITE HAIR","unified":"1F468-200D-1F9B3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f9b3.png","sheet_x":15,"sheet_y":47,"short_name":"white_haired_man","short_names":["white_haired_man"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":235,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F9B3","non_qualified":null,"image":"1f468-1f3fb-200d-1f9b3.png","sheet_x":15,"sheet_y":48,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-1F9B3","non_qualified":null,"image":"1f468-1f3fc-200d-1f9b3.png","sheet_x":15,"sheet_y":49,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-1F9B3","non_qualified":null,"image":"1f468-1f3fd-200d-1f9b3.png","sheet_x":15,"sheet_y":50,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-1F9B3","non_qualified":null,"image":"1f468-1f3fe-200d-1f9b3.png","sheet_x":15,"sheet_y":51,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-1F9B3","non_qualified":null,"image":"1f468-1f3ff-200d-1f9b3.png","sheet_x":15,"sheet_y":52,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN IN MOTORIZED WHEELCHAIR","unified":"1F468-200D-1F9BC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f9bc.png","sheet_x":15,"sheet_y":53,"short_name":"man_in_motorized_wheelchair","short_names":["man_in_motorized_wheelchair"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":414,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F9BC","non_qualified":null,"image":"1f468-1f3fb-200d-1f9bc.png","sheet_x":15,"sheet_y":54,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-1F9BC","non_qualified":null,"image":"1f468-1f3fc-200d-1f9bc.png","sheet_x":15,"sheet_y":55,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-1F9BC","non_qualified":null,"image":"1f468-1f3fd-200d-1f9bc.png","sheet_x":15,"sheet_y":56,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-1F9BC","non_qualified":null,"image":"1f468-1f3fe-200d-1f9bc.png","sheet_x":15,"sheet_y":57,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-1F9BC","non_qualified":null,"image":"1f468-1f3ff-200d-1f9bc.png","sheet_x":15,"sheet_y":58,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN IN MANUAL WHEELCHAIR","unified":"1F468-200D-1F9BD","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f9bd.png","sheet_x":15,"sheet_y":59,"short_name":"man_in_manual_wheelchair","short_names":["man_in_manual_wheelchair"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":417,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F9BD","non_qualified":null,"image":"1f468-1f3fb-200d-1f9bd.png","sheet_x":15,"sheet_y":60,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-1F9BD","non_qualified":null,"image":"1f468-1f3fc-200d-1f9bd.png","sheet_x":16,"sheet_y":0,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-1F9BD","non_qualified":null,"image":"1f468-1f3fd-200d-1f9bd.png","sheet_x":16,"sheet_y":1,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-1F9BD","non_qualified":null,"image":"1f468-1f3fe-200d-1f9bd.png","sheet_x":16,"sheet_y":2,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-1F9BD","non_qualified":null,"image":"1f468-1f3ff-200d-1f9bd.png","sheet_x":16,"sheet_y":3,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN HEALTH WORKER","unified":"1F468-200D-2695-FE0F","non_qualified":"1F468-200D-2695","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-2695-fe0f.png","sheet_x":16,"sheet_y":4,"short_name":"male-doctor","short_names":["male-doctor"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":282,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-2695-FE0F","non_qualified":"1F468-1F3FB-200D-2695","image":"1f468-1f3fb-200d-2695-fe0f.png","sheet_x":16,"sheet_y":5,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-2695-FE0F","non_qualified":"1F468-1F3FC-200D-2695","image":"1f468-1f3fc-200d-2695-fe0f.png","sheet_x":16,"sheet_y":6,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-2695-FE0F","non_qualified":"1F468-1F3FD-200D-2695","image":"1f468-1f3fd-200d-2695-fe0f.png","sheet_x":16,"sheet_y":7,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-2695-FE0F","non_qualified":"1F468-1F3FE-200D-2695","image":"1f468-1f3fe-200d-2695-fe0f.png","sheet_x":16,"sheet_y":8,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-2695-FE0F","non_qualified":"1F468-1F3FF-200D-2695","image":"1f468-1f3ff-200d-2695-fe0f.png","sheet_x":16,"sheet_y":9,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN JUDGE","unified":"1F468-200D-2696-FE0F","non_qualified":"1F468-200D-2696","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-2696-fe0f.png","sheet_x":16,"sheet_y":10,"short_name":"male-judge","short_names":["male-judge"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":291,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-2696-FE0F","non_qualified":"1F468-1F3FB-200D-2696","image":"1f468-1f3fb-200d-2696-fe0f.png","sheet_x":16,"sheet_y":11,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-2696-FE0F","non_qualified":"1F468-1F3FC-200D-2696","image":"1f468-1f3fc-200d-2696-fe0f.png","sheet_x":16,"sheet_y":12,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-2696-FE0F","non_qualified":"1F468-1F3FD-200D-2696","image":"1f468-1f3fd-200d-2696-fe0f.png","sheet_x":16,"sheet_y":13,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-2696-FE0F","non_qualified":"1F468-1F3FE-200D-2696","image":"1f468-1f3fe-200d-2696-fe0f.png","sheet_x":16,"sheet_y":14,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-2696-FE0F","non_qualified":"1F468-1F3FF-200D-2696","image":"1f468-1f3ff-200d-2696-fe0f.png","sheet_x":16,"sheet_y":15,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN PILOT","unified":"1F468-200D-2708-FE0F","non_qualified":"1F468-200D-2708","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-2708-fe0f.png","sheet_x":16,"sheet_y":16,"short_name":"male-pilot","short_names":["male-pilot"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":321,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-2708-FE0F","non_qualified":"1F468-1F3FB-200D-2708","image":"1f468-1f3fb-200d-2708-fe0f.png","sheet_x":16,"sheet_y":17,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-2708-FE0F","non_qualified":"1F468-1F3FC-200D-2708","image":"1f468-1f3fc-200d-2708-fe0f.png","sheet_x":16,"sheet_y":18,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-2708-FE0F","non_qualified":"1F468-1F3FD-200D-2708","image":"1f468-1f3fd-200d-2708-fe0f.png","sheet_x":16,"sheet_y":19,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-2708-FE0F","non_qualified":"1F468-1F3FE-200D-2708","image":"1f468-1f3fe-200d-2708-fe0f.png","sheet_x":16,"sheet_y":20,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-2708-FE0F","non_qualified":"1F468-1F3FF-200D-2708","image":"1f468-1f3ff-200d-2708-fe0f.png","sheet_x":16,"sheet_y":21,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"COUPLE WITH HEART: MAN, MAN","unified":"1F468-200D-2764-FE0F-200D-1F468","non_qualified":"1F468-200D-2764-200D-1F468","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-2764-fe0f-200d-1f468.png","sheet_x":16,"sheet_y":22,"short_name":"man-heart-man","short_names":["man-heart-man"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":492,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB-1F3FB":{"unified":"1F468-1F3FB-200D-2764-FE0F-200D-1F468-1F3FB","non_qualified":"1F468-1F3FB-200D-2764-200D-1F468-1F3FB","image":"1f468-1f3fb-200d-2764-fe0f-200d-1f468-1f3fb.png","sheet_x":16,"sheet_y":23,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FB-1F3FC":{"unified":"1F468-1F3FB-200D-2764-FE0F-200D-1F468-1F3FC","non_qualified":"1F468-1F3FB-200D-2764-200D-1F468-1F3FC","image":"1f468-1f3fb-200d-2764-fe0f-200d-1f468-1f3fc.png","sheet_x":16,"sheet_y":24,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FB-1F3FD":{"unified":"1F468-1F3FB-200D-2764-FE0F-200D-1F468-1F3FD","non_qualified":"1F468-1F3FB-200D-2764-200D-1F468-1F3FD","image":"1f468-1f3fb-200d-2764-fe0f-200d-1f468-1f3fd.png","sheet_x":16,"sheet_y":25,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FB-1F3FE":{"unified":"1F468-1F3FB-200D-2764-FE0F-200D-1F468-1F3FE","non_qualified":"1F468-1F3FB-200D-2764-200D-1F468-1F3FE","image":"1f468-1f3fb-200d-2764-fe0f-200d-1f468-1f3fe.png","sheet_x":16,"sheet_y":26,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FB-1F3FF":{"unified":"1F468-1F3FB-200D-2764-FE0F-200D-1F468-1F3FF","non_qualified":"1F468-1F3FB-200D-2764-200D-1F468-1F3FF","image":"1f468-1f3fb-200d-2764-fe0f-200d-1f468-1f3ff.png","sheet_x":16,"sheet_y":27,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FB":{"unified":"1F468-1F3FC-200D-2764-FE0F-200D-1F468-1F3FB","non_qualified":"1F468-1F3FC-200D-2764-200D-1F468-1F3FB","image":"1f468-1f3fc-200d-2764-fe0f-200d-1f468-1f3fb.png","sheet_x":16,"sheet_y":28,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FC":{"unified":"1F468-1F3FC-200D-2764-FE0F-200D-1F468-1F3FC","non_qualified":"1F468-1F3FC-200D-2764-200D-1F468-1F3FC","image":"1f468-1f3fc-200d-2764-fe0f-200d-1f468-1f3fc.png","sheet_x":16,"sheet_y":29,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FD":{"unified":"1F468-1F3FC-200D-2764-FE0F-200D-1F468-1F3FD","non_qualified":"1F468-1F3FC-200D-2764-200D-1F468-1F3FD","image":"1f468-1f3fc-200d-2764-fe0f-200d-1f468-1f3fd.png","sheet_x":16,"sheet_y":30,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FE":{"unified":"1F468-1F3FC-200D-2764-FE0F-200D-1F468-1F3FE","non_qualified":"1F468-1F3FC-200D-2764-200D-1F468-1F3FE","image":"1f468-1f3fc-200d-2764-fe0f-200d-1f468-1f3fe.png","sheet_x":16,"sheet_y":31,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FF":{"unified":"1F468-1F3FC-200D-2764-FE0F-200D-1F468-1F3FF","non_qualified":"1F468-1F3FC-200D-2764-200D-1F468-1F3FF","image":"1f468-1f3fc-200d-2764-fe0f-200d-1f468-1f3ff.png","sheet_x":16,"sheet_y":32,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FB":{"unified":"1F468-1F3FD-200D-2764-FE0F-200D-1F468-1F3FB","non_qualified":"1F468-1F3FD-200D-2764-200D-1F468-1F3FB","image":"1f468-1f3fd-200d-2764-fe0f-200d-1f468-1f3fb.png","sheet_x":16,"sheet_y":33,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FC":{"unified":"1F468-1F3FD-200D-2764-FE0F-200D-1F468-1F3FC","non_qualified":"1F468-1F3FD-200D-2764-200D-1F468-1F3FC","image":"1f468-1f3fd-200d-2764-fe0f-200d-1f468-1f3fc.png","sheet_x":16,"sheet_y":34,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FD":{"unified":"1F468-1F3FD-200D-2764-FE0F-200D-1F468-1F3FD","non_qualified":"1F468-1F3FD-200D-2764-200D-1F468-1F3FD","image":"1f468-1f3fd-200d-2764-fe0f-200d-1f468-1f3fd.png","sheet_x":16,"sheet_y":35,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FE":{"unified":"1F468-1F3FD-200D-2764-FE0F-200D-1F468-1F3FE","non_qualified":"1F468-1F3FD-200D-2764-200D-1F468-1F3FE","image":"1f468-1f3fd-200d-2764-fe0f-200d-1f468-1f3fe.png","sheet_x":16,"sheet_y":36,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FF":{"unified":"1F468-1F3FD-200D-2764-FE0F-200D-1F468-1F3FF","non_qualified":"1F468-1F3FD-200D-2764-200D-1F468-1F3FF","image":"1f468-1f3fd-200d-2764-fe0f-200d-1f468-1f3ff.png","sheet_x":16,"sheet_y":37,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE-1F3FB":{"unified":"1F468-1F3FE-200D-2764-FE0F-200D-1F468-1F3FB","non_qualified":"1F468-1F3FE-200D-2764-200D-1F468-1F3FB","image":"1f468-1f3fe-200d-2764-fe0f-200d-1f468-1f3fb.png","sheet_x":16,"sheet_y":38,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE-1F3FC":{"unified":"1F468-1F3FE-200D-2764-FE0F-200D-1F468-1F3FC","non_qualified":"1F468-1F3FE-200D-2764-200D-1F468-1F3FC","image":"1f468-1f3fe-200d-2764-fe0f-200d-1f468-1f3fc.png","sheet_x":16,"sheet_y":39,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE-1F3FD":{"unified":"1F468-1F3FE-200D-2764-FE0F-200D-1F468-1F3FD","non_qualified":"1F468-1F3FE-200D-2764-200D-1F468-1F3FD","image":"1f468-1f3fe-200d-2764-fe0f-200d-1f468-1f3fd.png","sheet_x":16,"sheet_y":40,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE-1F3FE":{"unified":"1F468-1F3FE-200D-2764-FE0F-200D-1F468-1F3FE","non_qualified":"1F468-1F3FE-200D-2764-200D-1F468-1F3FE","image":"1f468-1f3fe-200d-2764-fe0f-200d-1f468-1f3fe.png","sheet_x":16,"sheet_y":41,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE-1F3FF":{"unified":"1F468-1F3FE-200D-2764-FE0F-200D-1F468-1F3FF","non_qualified":"1F468-1F3FE-200D-2764-200D-1F468-1F3FF","image":"1f468-1f3fe-200d-2764-fe0f-200d-1f468-1f3ff.png","sheet_x":16,"sheet_y":42,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF-1F3FB":{"unified":"1F468-1F3FF-200D-2764-FE0F-200D-1F468-1F3FB","non_qualified":"1F468-1F3FF-200D-2764-200D-1F468-1F3FB","image":"1f468-1f3ff-200d-2764-fe0f-200d-1f468-1f3fb.png","sheet_x":16,"sheet_y":43,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF-1F3FC":{"unified":"1F468-1F3FF-200D-2764-FE0F-200D-1F468-1F3FC","non_qualified":"1F468-1F3FF-200D-2764-200D-1F468-1F3FC","image":"1f468-1f3ff-200d-2764-fe0f-200d-1f468-1f3fc.png","sheet_x":16,"sheet_y":44,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF-1F3FD":{"unified":"1F468-1F3FF-200D-2764-FE0F-200D-1F468-1F3FD","non_qualified":"1F468-1F3FF-200D-2764-200D-1F468-1F3FD","image":"1f468-1f3ff-200d-2764-fe0f-200d-1f468-1f3fd.png","sheet_x":16,"sheet_y":45,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF-1F3FE":{"unified":"1F468-1F3FF-200D-2764-FE0F-200D-1F468-1F3FE","non_qualified":"1F468-1F3FF-200D-2764-200D-1F468-1F3FE","image":"1f468-1f3ff-200d-2764-fe0f-200d-1f468-1f3fe.png","sheet_x":16,"sheet_y":46,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF-1F3FF":{"unified":"1F468-1F3FF-200D-2764-FE0F-200D-1F468-1F3FF","non_qualified":"1F468-1F3FF-200D-2764-200D-1F468-1F3FF","image":"1f468-1f3ff-200d-2764-fe0f-200d-1f468-1f3ff.png","sheet_x":16,"sheet_y":47,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false}}},{"name":"KISS: MAN, MAN","unified":"1F468-200D-2764-FE0F-200D-1F48B-200D-1F468","non_qualified":"1F468-200D-2764-200D-1F48B-200D-1F468","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-2764-fe0f-200d-1f48b-200d-1f468.png","sheet_x":16,"sheet_y":48,"short_name":"man-kiss-man","short_names":["man-kiss-man"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":488,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB-1F3FB":{"unified":"1F468-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FB","non_qualified":"1F468-1F3FB-200D-2764-200D-1F48B-200D-1F468-1F3FB","image":"1f468-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb.png","sheet_x":16,"sheet_y":49,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FB-1F3FC":{"unified":"1F468-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FC","non_qualified":"1F468-1F3FB-200D-2764-200D-1F48B-200D-1F468-1F3FC","image":"1f468-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc.png","sheet_x":16,"sheet_y":50,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FB-1F3FD":{"unified":"1F468-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FD","non_qualified":"1F468-1F3FB-200D-2764-200D-1F48B-200D-1F468-1F3FD","image":"1f468-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd.png","sheet_x":16,"sheet_y":51,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FB-1F3FE":{"unified":"1F468-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FE","non_qualified":"1F468-1F3FB-200D-2764-200D-1F48B-200D-1F468-1F3FE","image":"1f468-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe.png","sheet_x":16,"sheet_y":52,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FB-1F3FF":{"unified":"1F468-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FF","non_qualified":"1F468-1F3FB-200D-2764-200D-1F48B-200D-1F468-1F3FF","image":"1f468-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff.png","sheet_x":16,"sheet_y":53,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FB":{"unified":"1F468-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FB","non_qualified":"1F468-1F3FC-200D-2764-200D-1F48B-200D-1F468-1F3FB","image":"1f468-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb.png","sheet_x":16,"sheet_y":54,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FC":{"unified":"1F468-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FC","non_qualified":"1F468-1F3FC-200D-2764-200D-1F48B-200D-1F468-1F3FC","image":"1f468-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc.png","sheet_x":16,"sheet_y":55,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FD":{"unified":"1F468-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FD","non_qualified":"1F468-1F3FC-200D-2764-200D-1F48B-200D-1F468-1F3FD","image":"1f468-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd.png","sheet_x":16,"sheet_y":56,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FE":{"unified":"1F468-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FE","non_qualified":"1F468-1F3FC-200D-2764-200D-1F48B-200D-1F468-1F3FE","image":"1f468-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe.png","sheet_x":16,"sheet_y":57,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FF":{"unified":"1F468-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FF","non_qualified":"1F468-1F3FC-200D-2764-200D-1F48B-200D-1F468-1F3FF","image":"1f468-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff.png","sheet_x":16,"sheet_y":58,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FB":{"unified":"1F468-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FB","non_qualified":"1F468-1F3FD-200D-2764-200D-1F48B-200D-1F468-1F3FB","image":"1f468-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb.png","sheet_x":16,"sheet_y":59,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FC":{"unified":"1F468-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FC","non_qualified":"1F468-1F3FD-200D-2764-200D-1F48B-200D-1F468-1F3FC","image":"1f468-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc.png","sheet_x":16,"sheet_y":60,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FD":{"unified":"1F468-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FD","non_qualified":"1F468-1F3FD-200D-2764-200D-1F48B-200D-1F468-1F3FD","image":"1f468-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd.png","sheet_x":17,"sheet_y":0,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FE":{"unified":"1F468-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FE","non_qualified":"1F468-1F3FD-200D-2764-200D-1F48B-200D-1F468-1F3FE","image":"1f468-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe.png","sheet_x":17,"sheet_y":1,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FF":{"unified":"1F468-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FF","non_qualified":"1F468-1F3FD-200D-2764-200D-1F48B-200D-1F468-1F3FF","image":"1f468-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff.png","sheet_x":17,"sheet_y":2,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE-1F3FB":{"unified":"1F468-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FB","non_qualified":"1F468-1F3FE-200D-2764-200D-1F48B-200D-1F468-1F3FB","image":"1f468-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb.png","sheet_x":17,"sheet_y":3,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE-1F3FC":{"unified":"1F468-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FC","non_qualified":"1F468-1F3FE-200D-2764-200D-1F48B-200D-1F468-1F3FC","image":"1f468-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc.png","sheet_x":17,"sheet_y":4,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE-1F3FD":{"unified":"1F468-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FD","non_qualified":"1F468-1F3FE-200D-2764-200D-1F48B-200D-1F468-1F3FD","image":"1f468-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd.png","sheet_x":17,"sheet_y":5,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE-1F3FE":{"unified":"1F468-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FE","non_qualified":"1F468-1F3FE-200D-2764-200D-1F48B-200D-1F468-1F3FE","image":"1f468-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe.png","sheet_x":17,"sheet_y":6,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE-1F3FF":{"unified":"1F468-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FF","non_qualified":"1F468-1F3FE-200D-2764-200D-1F48B-200D-1F468-1F3FF","image":"1f468-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff.png","sheet_x":17,"sheet_y":7,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF-1F3FB":{"unified":"1F468-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FB","non_qualified":"1F468-1F3FF-200D-2764-200D-1F48B-200D-1F468-1F3FB","image":"1f468-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb.png","sheet_x":17,"sheet_y":8,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF-1F3FC":{"unified":"1F468-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FC","non_qualified":"1F468-1F3FF-200D-2764-200D-1F48B-200D-1F468-1F3FC","image":"1f468-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc.png","sheet_x":17,"sheet_y":9,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF-1F3FD":{"unified":"1F468-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FD","non_qualified":"1F468-1F3FF-200D-2764-200D-1F48B-200D-1F468-1F3FD","image":"1f468-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd.png","sheet_x":17,"sheet_y":10,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF-1F3FE":{"unified":"1F468-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FE","non_qualified":"1F468-1F3FF-200D-2764-200D-1F48B-200D-1F468-1F3FE","image":"1f468-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe.png","sheet_x":17,"sheet_y":11,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF-1F3FF":{"unified":"1F468-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FF","non_qualified":"1F468-1F3FF-200D-2764-200D-1F48B-200D-1F468-1F3FF","image":"1f468-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff.png","sheet_x":17,"sheet_y":12,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false}}},{"name":"MAN","unified":"1F468","non_qualified":null,"docomo":"E6F0","au":"E4FC","softbank":"E004","google":"FE19D","image":"1f468.png","sheet_x":17,"sheet_y":13,"short_name":"man","short_names":["man"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":229,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB","non_qualified":null,"image":"1f468-1f3fb.png","sheet_x":17,"sheet_y":14,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC","non_qualified":null,"image":"1f468-1f3fc.png","sheet_x":17,"sheet_y":15,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD","non_qualified":null,"image":"1f468-1f3fd.png","sheet_x":17,"sheet_y":16,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE","non_qualified":null,"image":"1f468-1f3fe.png","sheet_x":17,"sheet_y":17,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF","non_qualified":null,"image":"1f468-1f3ff.png","sheet_x":17,"sheet_y":18,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN FARMER","unified":"1F469-200D-1F33E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f33e.png","sheet_x":17,"sheet_y":19,"short_name":"female-farmer","short_names":["female-farmer"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":295,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F33E","non_qualified":null,"image":"1f469-1f3fb-200d-1f33e.png","sheet_x":17,"sheet_y":20,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-1F33E","non_qualified":null,"image":"1f469-1f3fc-200d-1f33e.png","sheet_x":17,"sheet_y":21,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-1F33E","non_qualified":null,"image":"1f469-1f3fd-200d-1f33e.png","sheet_x":17,"sheet_y":22,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-1F33E","non_qualified":null,"image":"1f469-1f3fe-200d-1f33e.png","sheet_x":17,"sheet_y":23,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-1F33E","non_qualified":null,"image":"1f469-1f3ff-200d-1f33e.png","sheet_x":17,"sheet_y":24,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN COOK","unified":"1F469-200D-1F373","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f373.png","sheet_x":17,"sheet_y":25,"short_name":"female-cook","short_names":["female-cook"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":298,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F373","non_qualified":null,"image":"1f469-1f3fb-200d-1f373.png","sheet_x":17,"sheet_y":26,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-1F373","non_qualified":null,"image":"1f469-1f3fc-200d-1f373.png","sheet_x":17,"sheet_y":27,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-1F373","non_qualified":null,"image":"1f469-1f3fd-200d-1f373.png","sheet_x":17,"sheet_y":28,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-1F373","non_qualified":null,"image":"1f469-1f3fe-200d-1f373.png","sheet_x":17,"sheet_y":29,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-1F373","non_qualified":null,"image":"1f469-1f3ff-200d-1f373.png","sheet_x":17,"sheet_y":30,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN FEEDING BABY","unified":"1F469-200D-1F37C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f37c.png","sheet_x":17,"sheet_y":31,"short_name":"woman_feeding_baby","short_names":["woman_feeding_baby"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":360,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F37C","non_qualified":null,"image":"1f469-1f3fb-200d-1f37c.png","sheet_x":17,"sheet_y":32,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-1F37C","non_qualified":null,"image":"1f469-1f3fc-200d-1f37c.png","sheet_x":17,"sheet_y":33,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-1F37C","non_qualified":null,"image":"1f469-1f3fd-200d-1f37c.png","sheet_x":17,"sheet_y":34,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-1F37C","non_qualified":null,"image":"1f469-1f3fe-200d-1f37c.png","sheet_x":17,"sheet_y":35,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-1F37C","non_qualified":null,"image":"1f469-1f3ff-200d-1f37c.png","sheet_x":17,"sheet_y":36,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN STUDENT","unified":"1F469-200D-1F393","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f393.png","sheet_x":17,"sheet_y":37,"short_name":"female-student","short_names":["female-student"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":286,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F393","non_qualified":null,"image":"1f469-1f3fb-200d-1f393.png","sheet_x":17,"sheet_y":38,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-1F393","non_qualified":null,"image":"1f469-1f3fc-200d-1f393.png","sheet_x":17,"sheet_y":39,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-1F393","non_qualified":null,"image":"1f469-1f3fd-200d-1f393.png","sheet_x":17,"sheet_y":40,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-1F393","non_qualified":null,"image":"1f469-1f3fe-200d-1f393.png","sheet_x":17,"sheet_y":41,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-1F393","non_qualified":null,"image":"1f469-1f3ff-200d-1f393.png","sheet_x":17,"sheet_y":42,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN SINGER","unified":"1F469-200D-1F3A4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f3a4.png","sheet_x":17,"sheet_y":43,"short_name":"female-singer","short_names":["female-singer"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":316,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F3A4","non_qualified":null,"image":"1f469-1f3fb-200d-1f3a4.png","sheet_x":17,"sheet_y":44,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-1F3A4","non_qualified":null,"image":"1f469-1f3fc-200d-1f3a4.png","sheet_x":17,"sheet_y":45,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-1F3A4","non_qualified":null,"image":"1f469-1f3fd-200d-1f3a4.png","sheet_x":17,"sheet_y":46,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-1F3A4","non_qualified":null,"image":"1f469-1f3fe-200d-1f3a4.png","sheet_x":17,"sheet_y":47,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-1F3A4","non_qualified":null,"image":"1f469-1f3ff-200d-1f3a4.png","sheet_x":17,"sheet_y":48,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN ARTIST","unified":"1F469-200D-1F3A8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f3a8.png","sheet_x":17,"sheet_y":49,"short_name":"female-artist","short_names":["female-artist"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":319,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F3A8","non_qualified":null,"image":"1f469-1f3fb-200d-1f3a8.png","sheet_x":17,"sheet_y":50,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-1F3A8","non_qualified":null,"image":"1f469-1f3fc-200d-1f3a8.png","sheet_x":17,"sheet_y":51,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-1F3A8","non_qualified":null,"image":"1f469-1f3fd-200d-1f3a8.png","sheet_x":17,"sheet_y":52,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-1F3A8","non_qualified":null,"image":"1f469-1f3fe-200d-1f3a8.png","sheet_x":17,"sheet_y":53,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-1F3A8","non_qualified":null,"image":"1f469-1f3ff-200d-1f3a8.png","sheet_x":17,"sheet_y":54,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN TEACHER","unified":"1F469-200D-1F3EB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f3eb.png","sheet_x":17,"sheet_y":55,"short_name":"female-teacher","short_names":["female-teacher"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":289,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F3EB","non_qualified":null,"image":"1f469-1f3fb-200d-1f3eb.png","sheet_x":17,"sheet_y":56,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-1F3EB","non_qualified":null,"image":"1f469-1f3fc-200d-1f3eb.png","sheet_x":17,"sheet_y":57,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-1F3EB","non_qualified":null,"image":"1f469-1f3fd-200d-1f3eb.png","sheet_x":17,"sheet_y":58,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-1F3EB","non_qualified":null,"image":"1f469-1f3fe-200d-1f3eb.png","sheet_x":17,"sheet_y":59,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-1F3EB","non_qualified":null,"image":"1f469-1f3ff-200d-1f3eb.png","sheet_x":17,"sheet_y":60,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN FACTORY WORKER","unified":"1F469-200D-1F3ED","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f3ed.png","sheet_x":18,"sheet_y":0,"short_name":"female-factory-worker","short_names":["female-factory-worker"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":304,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F3ED","non_qualified":null,"image":"1f469-1f3fb-200d-1f3ed.png","sheet_x":18,"sheet_y":1,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-1F3ED","non_qualified":null,"image":"1f469-1f3fc-200d-1f3ed.png","sheet_x":18,"sheet_y":2,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-1F3ED","non_qualified":null,"image":"1f469-1f3fd-200d-1f3ed.png","sheet_x":18,"sheet_y":3,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-1F3ED","non_qualified":null,"image":"1f469-1f3fe-200d-1f3ed.png","sheet_x":18,"sheet_y":4,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-1F3ED","non_qualified":null,"image":"1f469-1f3ff-200d-1f3ed.png","sheet_x":18,"sheet_y":5,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"FAMILY: WOMAN, BOY, BOY","unified":"1F469-200D-1F466-200D-1F466","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f466-200d-1f466.png","sheet_x":18,"sheet_y":6,"short_name":"woman-boy-boy","short_names":["woman-boy-boy"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":516,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAMILY: WOMAN, BOY","unified":"1F469-200D-1F466","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f466.png","sheet_x":18,"sheet_y":7,"short_name":"woman-boy","short_names":["woman-boy"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":515,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAMILY: WOMAN, GIRL, BOY","unified":"1F469-200D-1F467-200D-1F466","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f467-200d-1f466.png","sheet_x":18,"sheet_y":8,"short_name":"woman-girl-boy","short_names":["woman-girl-boy"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":518,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAMILY: WOMAN, GIRL, GIRL","unified":"1F469-200D-1F467-200D-1F467","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f467-200d-1f467.png","sheet_x":18,"sheet_y":9,"short_name":"woman-girl-girl","short_names":["woman-girl-girl"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":519,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAMILY: WOMAN, GIRL","unified":"1F469-200D-1F467","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f467.png","sheet_x":18,"sheet_y":10,"short_name":"woman-girl","short_names":["woman-girl"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":517,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAMILY: WOMAN, WOMAN, BOY","unified":"1F469-200D-1F469-200D-1F466","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f469-200d-1f466.png","sheet_x":18,"sheet_y":11,"short_name":"woman-woman-boy","short_names":["woman-woman-boy"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":505,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAMILY: WOMAN, WOMAN, BOY, BOY","unified":"1F469-200D-1F469-200D-1F466-200D-1F466","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f469-200d-1f466-200d-1f466.png","sheet_x":18,"sheet_y":12,"short_name":"woman-woman-boy-boy","short_names":["woman-woman-boy-boy"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":508,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAMILY: WOMAN, WOMAN, GIRL","unified":"1F469-200D-1F469-200D-1F467","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f469-200d-1f467.png","sheet_x":18,"sheet_y":13,"short_name":"woman-woman-girl","short_names":["woman-woman-girl"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":506,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAMILY: WOMAN, WOMAN, GIRL, BOY","unified":"1F469-200D-1F469-200D-1F467-200D-1F466","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f469-200d-1f467-200d-1f466.png","sheet_x":18,"sheet_y":14,"short_name":"woman-woman-girl-boy","short_names":["woman-woman-girl-boy"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":507,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAMILY: WOMAN, WOMAN, GIRL, GIRL","unified":"1F469-200D-1F469-200D-1F467-200D-1F467","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f469-200d-1f467-200d-1f467.png","sheet_x":18,"sheet_y":15,"short_name":"woman-woman-girl-girl","short_names":["woman-woman-girl-girl"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":509,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WOMAN TECHNOLOGIST","unified":"1F469-200D-1F4BB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f4bb.png","sheet_x":18,"sheet_y":16,"short_name":"female-technologist","short_names":["female-technologist"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":313,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F4BB","non_qualified":null,"image":"1f469-1f3fb-200d-1f4bb.png","sheet_x":18,"sheet_y":17,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-1F4BB","non_qualified":null,"image":"1f469-1f3fc-200d-1f4bb.png","sheet_x":18,"sheet_y":18,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-1F4BB","non_qualified":null,"image":"1f469-1f3fd-200d-1f4bb.png","sheet_x":18,"sheet_y":19,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-1F4BB","non_qualified":null,"image":"1f469-1f3fe-200d-1f4bb.png","sheet_x":18,"sheet_y":20,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-1F4BB","non_qualified":null,"image":"1f469-1f3ff-200d-1f4bb.png","sheet_x":18,"sheet_y":21,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN OFFICE WORKER","unified":"1F469-200D-1F4BC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f4bc.png","sheet_x":18,"sheet_y":22,"short_name":"female-office-worker","short_names":["female-office-worker"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":307,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F4BC","non_qualified":null,"image":"1f469-1f3fb-200d-1f4bc.png","sheet_x":18,"sheet_y":23,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-1F4BC","non_qualified":null,"image":"1f469-1f3fc-200d-1f4bc.png","sheet_x":18,"sheet_y":24,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-1F4BC","non_qualified":null,"image":"1f469-1f3fd-200d-1f4bc.png","sheet_x":18,"sheet_y":25,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-1F4BC","non_qualified":null,"image":"1f469-1f3fe-200d-1f4bc.png","sheet_x":18,"sheet_y":26,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-1F4BC","non_qualified":null,"image":"1f469-1f3ff-200d-1f4bc.png","sheet_x":18,"sheet_y":27,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN MECHANIC","unified":"1F469-200D-1F527","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f527.png","sheet_x":18,"sheet_y":28,"short_name":"female-mechanic","short_names":["female-mechanic"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":301,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F527","non_qualified":null,"image":"1f469-1f3fb-200d-1f527.png","sheet_x":18,"sheet_y":29,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-1F527","non_qualified":null,"image":"1f469-1f3fc-200d-1f527.png","sheet_x":18,"sheet_y":30,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-1F527","non_qualified":null,"image":"1f469-1f3fd-200d-1f527.png","sheet_x":18,"sheet_y":31,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-1F527","non_qualified":null,"image":"1f469-1f3fe-200d-1f527.png","sheet_x":18,"sheet_y":32,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-1F527","non_qualified":null,"image":"1f469-1f3ff-200d-1f527.png","sheet_x":18,"sheet_y":33,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN SCIENTIST","unified":"1F469-200D-1F52C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f52c.png","sheet_x":18,"sheet_y":34,"short_name":"female-scientist","short_names":["female-scientist"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":310,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F52C","non_qualified":null,"image":"1f469-1f3fb-200d-1f52c.png","sheet_x":18,"sheet_y":35,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-1F52C","non_qualified":null,"image":"1f469-1f3fc-200d-1f52c.png","sheet_x":18,"sheet_y":36,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-1F52C","non_qualified":null,"image":"1f469-1f3fd-200d-1f52c.png","sheet_x":18,"sheet_y":37,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-1F52C","non_qualified":null,"image":"1f469-1f3fe-200d-1f52c.png","sheet_x":18,"sheet_y":38,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-1F52C","non_qualified":null,"image":"1f469-1f3ff-200d-1f52c.png","sheet_x":18,"sheet_y":39,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN ASTRONAUT","unified":"1F469-200D-1F680","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f680.png","sheet_x":18,"sheet_y":40,"short_name":"female-astronaut","short_names":["female-astronaut"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":325,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F680","non_qualified":null,"image":"1f469-1f3fb-200d-1f680.png","sheet_x":18,"sheet_y":41,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-1F680","non_qualified":null,"image":"1f469-1f3fc-200d-1f680.png","sheet_x":18,"sheet_y":42,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-1F680","non_qualified":null,"image":"1f469-1f3fd-200d-1f680.png","sheet_x":18,"sheet_y":43,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-1F680","non_qualified":null,"image":"1f469-1f3fe-200d-1f680.png","sheet_x":18,"sheet_y":44,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-1F680","non_qualified":null,"image":"1f469-1f3ff-200d-1f680.png","sheet_x":18,"sheet_y":45,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN FIREFIGHTER","unified":"1F469-200D-1F692","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f692.png","sheet_x":18,"sheet_y":46,"short_name":"female-firefighter","short_names":["female-firefighter"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":328,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F692","non_qualified":null,"image":"1f469-1f3fb-200d-1f692.png","sheet_x":18,"sheet_y":47,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-1F692","non_qualified":null,"image":"1f469-1f3fc-200d-1f692.png","sheet_x":18,"sheet_y":48,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-1F692","non_qualified":null,"image":"1f469-1f3fd-200d-1f692.png","sheet_x":18,"sheet_y":49,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-1F692","non_qualified":null,"image":"1f469-1f3fe-200d-1f692.png","sheet_x":18,"sheet_y":50,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-1F692","non_qualified":null,"image":"1f469-1f3ff-200d-1f692.png","sheet_x":18,"sheet_y":51,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN WITH WHITE CANE","unified":"1F469-200D-1F9AF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f9af.png","sheet_x":18,"sheet_y":52,"short_name":"woman_with_probing_cane","short_names":["woman_with_probing_cane"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":412,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F9AF","non_qualified":null,"image":"1f469-1f3fb-200d-1f9af.png","sheet_x":18,"sheet_y":53,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-1F9AF","non_qualified":null,"image":"1f469-1f3fc-200d-1f9af.png","sheet_x":18,"sheet_y":54,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-1F9AF","non_qualified":null,"image":"1f469-1f3fd-200d-1f9af.png","sheet_x":18,"sheet_y":55,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-1F9AF","non_qualified":null,"image":"1f469-1f3fe-200d-1f9af.png","sheet_x":18,"sheet_y":56,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-1F9AF","non_qualified":null,"image":"1f469-1f3ff-200d-1f9af.png","sheet_x":18,"sheet_y":57,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN: RED HAIR","unified":"1F469-200D-1F9B0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f9b0.png","sheet_x":18,"sheet_y":58,"short_name":"red_haired_woman","short_names":["red_haired_woman"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":238,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F9B0","non_qualified":null,"image":"1f469-1f3fb-200d-1f9b0.png","sheet_x":18,"sheet_y":59,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-1F9B0","non_qualified":null,"image":"1f469-1f3fc-200d-1f9b0.png","sheet_x":18,"sheet_y":60,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-1F9B0","non_qualified":null,"image":"1f469-1f3fd-200d-1f9b0.png","sheet_x":19,"sheet_y":0,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-1F9B0","non_qualified":null,"image":"1f469-1f3fe-200d-1f9b0.png","sheet_x":19,"sheet_y":1,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-1F9B0","non_qualified":null,"image":"1f469-1f3ff-200d-1f9b0.png","sheet_x":19,"sheet_y":2,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN: CURLY HAIR","unified":"1F469-200D-1F9B1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f9b1.png","sheet_x":19,"sheet_y":3,"short_name":"curly_haired_woman","short_names":["curly_haired_woman"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":240,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F9B1","non_qualified":null,"image":"1f469-1f3fb-200d-1f9b1.png","sheet_x":19,"sheet_y":4,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-1F9B1","non_qualified":null,"image":"1f469-1f3fc-200d-1f9b1.png","sheet_x":19,"sheet_y":5,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-1F9B1","non_qualified":null,"image":"1f469-1f3fd-200d-1f9b1.png","sheet_x":19,"sheet_y":6,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-1F9B1","non_qualified":null,"image":"1f469-1f3fe-200d-1f9b1.png","sheet_x":19,"sheet_y":7,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-1F9B1","non_qualified":null,"image":"1f469-1f3ff-200d-1f9b1.png","sheet_x":19,"sheet_y":8,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN: BALD","unified":"1F469-200D-1F9B2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f9b2.png","sheet_x":19,"sheet_y":9,"short_name":"bald_woman","short_names":["bald_woman"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":244,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F9B2","non_qualified":null,"image":"1f469-1f3fb-200d-1f9b2.png","sheet_x":19,"sheet_y":10,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-1F9B2","non_qualified":null,"image":"1f469-1f3fc-200d-1f9b2.png","sheet_x":19,"sheet_y":11,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-1F9B2","non_qualified":null,"image":"1f469-1f3fd-200d-1f9b2.png","sheet_x":19,"sheet_y":12,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-1F9B2","non_qualified":null,"image":"1f469-1f3fe-200d-1f9b2.png","sheet_x":19,"sheet_y":13,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-1F9B2","non_qualified":null,"image":"1f469-1f3ff-200d-1f9b2.png","sheet_x":19,"sheet_y":14,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN: WHITE HAIR","unified":"1F469-200D-1F9B3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f9b3.png","sheet_x":19,"sheet_y":15,"short_name":"white_haired_woman","short_names":["white_haired_woman"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":242,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F9B3","non_qualified":null,"image":"1f469-1f3fb-200d-1f9b3.png","sheet_x":19,"sheet_y":16,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-1F9B3","non_qualified":null,"image":"1f469-1f3fc-200d-1f9b3.png","sheet_x":19,"sheet_y":17,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-1F9B3","non_qualified":null,"image":"1f469-1f3fd-200d-1f9b3.png","sheet_x":19,"sheet_y":18,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-1F9B3","non_qualified":null,"image":"1f469-1f3fe-200d-1f9b3.png","sheet_x":19,"sheet_y":19,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-1F9B3","non_qualified":null,"image":"1f469-1f3ff-200d-1f9b3.png","sheet_x":19,"sheet_y":20,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN IN MOTORIZED WHEELCHAIR","unified":"1F469-200D-1F9BC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f9bc.png","sheet_x":19,"sheet_y":21,"short_name":"woman_in_motorized_wheelchair","short_names":["woman_in_motorized_wheelchair"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":415,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F9BC","non_qualified":null,"image":"1f469-1f3fb-200d-1f9bc.png","sheet_x":19,"sheet_y":22,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-1F9BC","non_qualified":null,"image":"1f469-1f3fc-200d-1f9bc.png","sheet_x":19,"sheet_y":23,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-1F9BC","non_qualified":null,"image":"1f469-1f3fd-200d-1f9bc.png","sheet_x":19,"sheet_y":24,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-1F9BC","non_qualified":null,"image":"1f469-1f3fe-200d-1f9bc.png","sheet_x":19,"sheet_y":25,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-1F9BC","non_qualified":null,"image":"1f469-1f3ff-200d-1f9bc.png","sheet_x":19,"sheet_y":26,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN IN MANUAL WHEELCHAIR","unified":"1F469-200D-1F9BD","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f9bd.png","sheet_x":19,"sheet_y":27,"short_name":"woman_in_manual_wheelchair","short_names":["woman_in_manual_wheelchair"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":418,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F9BD","non_qualified":null,"image":"1f469-1f3fb-200d-1f9bd.png","sheet_x":19,"sheet_y":28,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-1F9BD","non_qualified":null,"image":"1f469-1f3fc-200d-1f9bd.png","sheet_x":19,"sheet_y":29,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-1F9BD","non_qualified":null,"image":"1f469-1f3fd-200d-1f9bd.png","sheet_x":19,"sheet_y":30,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-1F9BD","non_qualified":null,"image":"1f469-1f3fe-200d-1f9bd.png","sheet_x":19,"sheet_y":31,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-1F9BD","non_qualified":null,"image":"1f469-1f3ff-200d-1f9bd.png","sheet_x":19,"sheet_y":32,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN HEALTH WORKER","unified":"1F469-200D-2695-FE0F","non_qualified":"1F469-200D-2695","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-2695-fe0f.png","sheet_x":19,"sheet_y":33,"short_name":"female-doctor","short_names":["female-doctor"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":283,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-2695-FE0F","non_qualified":"1F469-1F3FB-200D-2695","image":"1f469-1f3fb-200d-2695-fe0f.png","sheet_x":19,"sheet_y":34,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-2695-FE0F","non_qualified":"1F469-1F3FC-200D-2695","image":"1f469-1f3fc-200d-2695-fe0f.png","sheet_x":19,"sheet_y":35,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-2695-FE0F","non_qualified":"1F469-1F3FD-200D-2695","image":"1f469-1f3fd-200d-2695-fe0f.png","sheet_x":19,"sheet_y":36,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-2695-FE0F","non_qualified":"1F469-1F3FE-200D-2695","image":"1f469-1f3fe-200d-2695-fe0f.png","sheet_x":19,"sheet_y":37,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-2695-FE0F","non_qualified":"1F469-1F3FF-200D-2695","image":"1f469-1f3ff-200d-2695-fe0f.png","sheet_x":19,"sheet_y":38,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN JUDGE","unified":"1F469-200D-2696-FE0F","non_qualified":"1F469-200D-2696","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-2696-fe0f.png","sheet_x":19,"sheet_y":39,"short_name":"female-judge","short_names":["female-judge"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":292,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-2696-FE0F","non_qualified":"1F469-1F3FB-200D-2696","image":"1f469-1f3fb-200d-2696-fe0f.png","sheet_x":19,"sheet_y":40,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-2696-FE0F","non_qualified":"1F469-1F3FC-200D-2696","image":"1f469-1f3fc-200d-2696-fe0f.png","sheet_x":19,"sheet_y":41,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-2696-FE0F","non_qualified":"1F469-1F3FD-200D-2696","image":"1f469-1f3fd-200d-2696-fe0f.png","sheet_x":19,"sheet_y":42,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-2696-FE0F","non_qualified":"1F469-1F3FE-200D-2696","image":"1f469-1f3fe-200d-2696-fe0f.png","sheet_x":19,"sheet_y":43,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-2696-FE0F","non_qualified":"1F469-1F3FF-200D-2696","image":"1f469-1f3ff-200d-2696-fe0f.png","sheet_x":19,"sheet_y":44,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN PILOT","unified":"1F469-200D-2708-FE0F","non_qualified":"1F469-200D-2708","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-2708-fe0f.png","sheet_x":19,"sheet_y":45,"short_name":"female-pilot","short_names":["female-pilot"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":322,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-2708-FE0F","non_qualified":"1F469-1F3FB-200D-2708","image":"1f469-1f3fb-200d-2708-fe0f.png","sheet_x":19,"sheet_y":46,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-2708-FE0F","non_qualified":"1F469-1F3FC-200D-2708","image":"1f469-1f3fc-200d-2708-fe0f.png","sheet_x":19,"sheet_y":47,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-2708-FE0F","non_qualified":"1F469-1F3FD-200D-2708","image":"1f469-1f3fd-200d-2708-fe0f.png","sheet_x":19,"sheet_y":48,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-2708-FE0F","non_qualified":"1F469-1F3FE-200D-2708","image":"1f469-1f3fe-200d-2708-fe0f.png","sheet_x":19,"sheet_y":49,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-2708-FE0F","non_qualified":"1F469-1F3FF-200D-2708","image":"1f469-1f3ff-200d-2708-fe0f.png","sheet_x":19,"sheet_y":50,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"COUPLE WITH HEART: WOMAN, MAN","unified":"1F469-200D-2764-FE0F-200D-1F468","non_qualified":"1F469-200D-2764-200D-1F468","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-2764-fe0f-200d-1f468.png","sheet_x":19,"sheet_y":51,"short_name":"woman-heart-man","short_names":["woman-heart-man"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":491,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB-1F3FB":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F468-1F3FB","non_qualified":"1F469-1F3FB-200D-2764-200D-1F468-1F3FB","image":"1f469-1f3fb-200d-2764-fe0f-200d-1f468-1f3fb.png","sheet_x":19,"sheet_y":52,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FB-1F3FC":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F468-1F3FC","non_qualified":"1F469-1F3FB-200D-2764-200D-1F468-1F3FC","image":"1f469-1f3fb-200d-2764-fe0f-200d-1f468-1f3fc.png","sheet_x":19,"sheet_y":53,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FB-1F3FD":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F468-1F3FD","non_qualified":"1F469-1F3FB-200D-2764-200D-1F468-1F3FD","image":"1f469-1f3fb-200d-2764-fe0f-200d-1f468-1f3fd.png","sheet_x":19,"sheet_y":54,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FB-1F3FE":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F468-1F3FE","non_qualified":"1F469-1F3FB-200D-2764-200D-1F468-1F3FE","image":"1f469-1f3fb-200d-2764-fe0f-200d-1f468-1f3fe.png","sheet_x":19,"sheet_y":55,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FB-1F3FF":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F468-1F3FF","non_qualified":"1F469-1F3FB-200D-2764-200D-1F468-1F3FF","image":"1f469-1f3fb-200d-2764-fe0f-200d-1f468-1f3ff.png","sheet_x":19,"sheet_y":56,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FB":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F468-1F3FB","non_qualified":"1F469-1F3FC-200D-2764-200D-1F468-1F3FB","image":"1f469-1f3fc-200d-2764-fe0f-200d-1f468-1f3fb.png","sheet_x":19,"sheet_y":57,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FC":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F468-1F3FC","non_qualified":"1F469-1F3FC-200D-2764-200D-1F468-1F3FC","image":"1f469-1f3fc-200d-2764-fe0f-200d-1f468-1f3fc.png","sheet_x":19,"sheet_y":58,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FD":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F468-1F3FD","non_qualified":"1F469-1F3FC-200D-2764-200D-1F468-1F3FD","image":"1f469-1f3fc-200d-2764-fe0f-200d-1f468-1f3fd.png","sheet_x":19,"sheet_y":59,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FE":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F468-1F3FE","non_qualified":"1F469-1F3FC-200D-2764-200D-1F468-1F3FE","image":"1f469-1f3fc-200d-2764-fe0f-200d-1f468-1f3fe.png","sheet_x":19,"sheet_y":60,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FF":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F468-1F3FF","non_qualified":"1F469-1F3FC-200D-2764-200D-1F468-1F3FF","image":"1f469-1f3fc-200d-2764-fe0f-200d-1f468-1f3ff.png","sheet_x":20,"sheet_y":0,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FB":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F468-1F3FB","non_qualified":"1F469-1F3FD-200D-2764-200D-1F468-1F3FB","image":"1f469-1f3fd-200d-2764-fe0f-200d-1f468-1f3fb.png","sheet_x":20,"sheet_y":1,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FC":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F468-1F3FC","non_qualified":"1F469-1F3FD-200D-2764-200D-1F468-1F3FC","image":"1f469-1f3fd-200d-2764-fe0f-200d-1f468-1f3fc.png","sheet_x":20,"sheet_y":2,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FD":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F468-1F3FD","non_qualified":"1F469-1F3FD-200D-2764-200D-1F468-1F3FD","image":"1f469-1f3fd-200d-2764-fe0f-200d-1f468-1f3fd.png","sheet_x":20,"sheet_y":3,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FE":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F468-1F3FE","non_qualified":"1F469-1F3FD-200D-2764-200D-1F468-1F3FE","image":"1f469-1f3fd-200d-2764-fe0f-200d-1f468-1f3fe.png","sheet_x":20,"sheet_y":4,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FF":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F468-1F3FF","non_qualified":"1F469-1F3FD-200D-2764-200D-1F468-1F3FF","image":"1f469-1f3fd-200d-2764-fe0f-200d-1f468-1f3ff.png","sheet_x":20,"sheet_y":5,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE-1F3FB":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F468-1F3FB","non_qualified":"1F469-1F3FE-200D-2764-200D-1F468-1F3FB","image":"1f469-1f3fe-200d-2764-fe0f-200d-1f468-1f3fb.png","sheet_x":20,"sheet_y":6,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE-1F3FC":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F468-1F3FC","non_qualified":"1F469-1F3FE-200D-2764-200D-1F468-1F3FC","image":"1f469-1f3fe-200d-2764-fe0f-200d-1f468-1f3fc.png","sheet_x":20,"sheet_y":7,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE-1F3FD":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F468-1F3FD","non_qualified":"1F469-1F3FE-200D-2764-200D-1F468-1F3FD","image":"1f469-1f3fe-200d-2764-fe0f-200d-1f468-1f3fd.png","sheet_x":20,"sheet_y":8,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE-1F3FE":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F468-1F3FE","non_qualified":"1F469-1F3FE-200D-2764-200D-1F468-1F3FE","image":"1f469-1f3fe-200d-2764-fe0f-200d-1f468-1f3fe.png","sheet_x":20,"sheet_y":9,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE-1F3FF":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F468-1F3FF","non_qualified":"1F469-1F3FE-200D-2764-200D-1F468-1F3FF","image":"1f469-1f3fe-200d-2764-fe0f-200d-1f468-1f3ff.png","sheet_x":20,"sheet_y":10,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF-1F3FB":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F468-1F3FB","non_qualified":"1F469-1F3FF-200D-2764-200D-1F468-1F3FB","image":"1f469-1f3ff-200d-2764-fe0f-200d-1f468-1f3fb.png","sheet_x":20,"sheet_y":11,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF-1F3FC":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F468-1F3FC","non_qualified":"1F469-1F3FF-200D-2764-200D-1F468-1F3FC","image":"1f469-1f3ff-200d-2764-fe0f-200d-1f468-1f3fc.png","sheet_x":20,"sheet_y":12,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF-1F3FD":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F468-1F3FD","non_qualified":"1F469-1F3FF-200D-2764-200D-1F468-1F3FD","image":"1f469-1f3ff-200d-2764-fe0f-200d-1f468-1f3fd.png","sheet_x":20,"sheet_y":13,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF-1F3FE":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F468-1F3FE","non_qualified":"1F469-1F3FF-200D-2764-200D-1F468-1F3FE","image":"1f469-1f3ff-200d-2764-fe0f-200d-1f468-1f3fe.png","sheet_x":20,"sheet_y":14,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF-1F3FF":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F468-1F3FF","non_qualified":"1F469-1F3FF-200D-2764-200D-1F468-1F3FF","image":"1f469-1f3ff-200d-2764-fe0f-200d-1f468-1f3ff.png","sheet_x":20,"sheet_y":15,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false}}},{"name":"COUPLE WITH HEART: WOMAN, WOMAN","unified":"1F469-200D-2764-FE0F-200D-1F469","non_qualified":"1F469-200D-2764-200D-1F469","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-2764-fe0f-200d-1f469.png","sheet_x":20,"sheet_y":16,"short_name":"woman-heart-woman","short_names":["woman-heart-woman"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":493,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB-1F3FB":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F469-1F3FB","non_qualified":"1F469-1F3FB-200D-2764-200D-1F469-1F3FB","image":"1f469-1f3fb-200d-2764-fe0f-200d-1f469-1f3fb.png","sheet_x":20,"sheet_y":17,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FB-1F3FC":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F469-1F3FC","non_qualified":"1F469-1F3FB-200D-2764-200D-1F469-1F3FC","image":"1f469-1f3fb-200d-2764-fe0f-200d-1f469-1f3fc.png","sheet_x":20,"sheet_y":18,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FB-1F3FD":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F469-1F3FD","non_qualified":"1F469-1F3FB-200D-2764-200D-1F469-1F3FD","image":"1f469-1f3fb-200d-2764-fe0f-200d-1f469-1f3fd.png","sheet_x":20,"sheet_y":19,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FB-1F3FE":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F469-1F3FE","non_qualified":"1F469-1F3FB-200D-2764-200D-1F469-1F3FE","image":"1f469-1f3fb-200d-2764-fe0f-200d-1f469-1f3fe.png","sheet_x":20,"sheet_y":20,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FB-1F3FF":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F469-1F3FF","non_qualified":"1F469-1F3FB-200D-2764-200D-1F469-1F3FF","image":"1f469-1f3fb-200d-2764-fe0f-200d-1f469-1f3ff.png","sheet_x":20,"sheet_y":21,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FB":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F469-1F3FB","non_qualified":"1F469-1F3FC-200D-2764-200D-1F469-1F3FB","image":"1f469-1f3fc-200d-2764-fe0f-200d-1f469-1f3fb.png","sheet_x":20,"sheet_y":22,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FC":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F469-1F3FC","non_qualified":"1F469-1F3FC-200D-2764-200D-1F469-1F3FC","image":"1f469-1f3fc-200d-2764-fe0f-200d-1f469-1f3fc.png","sheet_x":20,"sheet_y":23,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FD":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F469-1F3FD","non_qualified":"1F469-1F3FC-200D-2764-200D-1F469-1F3FD","image":"1f469-1f3fc-200d-2764-fe0f-200d-1f469-1f3fd.png","sheet_x":20,"sheet_y":24,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FE":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F469-1F3FE","non_qualified":"1F469-1F3FC-200D-2764-200D-1F469-1F3FE","image":"1f469-1f3fc-200d-2764-fe0f-200d-1f469-1f3fe.png","sheet_x":20,"sheet_y":25,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FF":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F469-1F3FF","non_qualified":"1F469-1F3FC-200D-2764-200D-1F469-1F3FF","image":"1f469-1f3fc-200d-2764-fe0f-200d-1f469-1f3ff.png","sheet_x":20,"sheet_y":26,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FB":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F469-1F3FB","non_qualified":"1F469-1F3FD-200D-2764-200D-1F469-1F3FB","image":"1f469-1f3fd-200d-2764-fe0f-200d-1f469-1f3fb.png","sheet_x":20,"sheet_y":27,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FC":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F469-1F3FC","non_qualified":"1F469-1F3FD-200D-2764-200D-1F469-1F3FC","image":"1f469-1f3fd-200d-2764-fe0f-200d-1f469-1f3fc.png","sheet_x":20,"sheet_y":28,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FD":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F469-1F3FD","non_qualified":"1F469-1F3FD-200D-2764-200D-1F469-1F3FD","image":"1f469-1f3fd-200d-2764-fe0f-200d-1f469-1f3fd.png","sheet_x":20,"sheet_y":29,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FE":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F469-1F3FE","non_qualified":"1F469-1F3FD-200D-2764-200D-1F469-1F3FE","image":"1f469-1f3fd-200d-2764-fe0f-200d-1f469-1f3fe.png","sheet_x":20,"sheet_y":30,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FF":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F469-1F3FF","non_qualified":"1F469-1F3FD-200D-2764-200D-1F469-1F3FF","image":"1f469-1f3fd-200d-2764-fe0f-200d-1f469-1f3ff.png","sheet_x":20,"sheet_y":31,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE-1F3FB":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F469-1F3FB","non_qualified":"1F469-1F3FE-200D-2764-200D-1F469-1F3FB","image":"1f469-1f3fe-200d-2764-fe0f-200d-1f469-1f3fb.png","sheet_x":20,"sheet_y":32,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE-1F3FC":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F469-1F3FC","non_qualified":"1F469-1F3FE-200D-2764-200D-1F469-1F3FC","image":"1f469-1f3fe-200d-2764-fe0f-200d-1f469-1f3fc.png","sheet_x":20,"sheet_y":33,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE-1F3FD":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F469-1F3FD","non_qualified":"1F469-1F3FE-200D-2764-200D-1F469-1F3FD","image":"1f469-1f3fe-200d-2764-fe0f-200d-1f469-1f3fd.png","sheet_x":20,"sheet_y":34,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE-1F3FE":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F469-1F3FE","non_qualified":"1F469-1F3FE-200D-2764-200D-1F469-1F3FE","image":"1f469-1f3fe-200d-2764-fe0f-200d-1f469-1f3fe.png","sheet_x":20,"sheet_y":35,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE-1F3FF":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F469-1F3FF","non_qualified":"1F469-1F3FE-200D-2764-200D-1F469-1F3FF","image":"1f469-1f3fe-200d-2764-fe0f-200d-1f469-1f3ff.png","sheet_x":20,"sheet_y":36,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF-1F3FB":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F469-1F3FB","non_qualified":"1F469-1F3FF-200D-2764-200D-1F469-1F3FB","image":"1f469-1f3ff-200d-2764-fe0f-200d-1f469-1f3fb.png","sheet_x":20,"sheet_y":37,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF-1F3FC":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F469-1F3FC","non_qualified":"1F469-1F3FF-200D-2764-200D-1F469-1F3FC","image":"1f469-1f3ff-200d-2764-fe0f-200d-1f469-1f3fc.png","sheet_x":20,"sheet_y":38,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF-1F3FD":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F469-1F3FD","non_qualified":"1F469-1F3FF-200D-2764-200D-1F469-1F3FD","image":"1f469-1f3ff-200d-2764-fe0f-200d-1f469-1f3fd.png","sheet_x":20,"sheet_y":39,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF-1F3FE":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F469-1F3FE","non_qualified":"1F469-1F3FF-200D-2764-200D-1F469-1F3FE","image":"1f469-1f3ff-200d-2764-fe0f-200d-1f469-1f3fe.png","sheet_x":20,"sheet_y":40,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF-1F3FF":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F469-1F3FF","non_qualified":"1F469-1F3FF-200D-2764-200D-1F469-1F3FF","image":"1f469-1f3ff-200d-2764-fe0f-200d-1f469-1f3ff.png","sheet_x":20,"sheet_y":41,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false}}},{"name":"KISS: WOMAN, MAN","unified":"1F469-200D-2764-FE0F-200D-1F48B-200D-1F468","non_qualified":"1F469-200D-2764-200D-1F48B-200D-1F468","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-2764-fe0f-200d-1f48b-200d-1f468.png","sheet_x":20,"sheet_y":42,"short_name":"woman-kiss-man","short_names":["woman-kiss-man"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":487,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB-1F3FB":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FB","non_qualified":"1F469-1F3FB-200D-2764-200D-1F48B-200D-1F468-1F3FB","image":"1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb.png","sheet_x":20,"sheet_y":43,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FB-1F3FC":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FC","non_qualified":"1F469-1F3FB-200D-2764-200D-1F48B-200D-1F468-1F3FC","image":"1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc.png","sheet_x":20,"sheet_y":44,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FB-1F3FD":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FD","non_qualified":"1F469-1F3FB-200D-2764-200D-1F48B-200D-1F468-1F3FD","image":"1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd.png","sheet_x":20,"sheet_y":45,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FB-1F3FE":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FE","non_qualified":"1F469-1F3FB-200D-2764-200D-1F48B-200D-1F468-1F3FE","image":"1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe.png","sheet_x":20,"sheet_y":46,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FB-1F3FF":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FF","non_qualified":"1F469-1F3FB-200D-2764-200D-1F48B-200D-1F468-1F3FF","image":"1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff.png","sheet_x":20,"sheet_y":47,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FB":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FB","non_qualified":"1F469-1F3FC-200D-2764-200D-1F48B-200D-1F468-1F3FB","image":"1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb.png","sheet_x":20,"sheet_y":48,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FC":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FC","non_qualified":"1F469-1F3FC-200D-2764-200D-1F48B-200D-1F468-1F3FC","image":"1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc.png","sheet_x":20,"sheet_y":49,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FD":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FD","non_qualified":"1F469-1F3FC-200D-2764-200D-1F48B-200D-1F468-1F3FD","image":"1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd.png","sheet_x":20,"sheet_y":50,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FE":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FE","non_qualified":"1F469-1F3FC-200D-2764-200D-1F48B-200D-1F468-1F3FE","image":"1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe.png","sheet_x":20,"sheet_y":51,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FF":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FF","non_qualified":"1F469-1F3FC-200D-2764-200D-1F48B-200D-1F468-1F3FF","image":"1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff.png","sheet_x":20,"sheet_y":52,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FB":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FB","non_qualified":"1F469-1F3FD-200D-2764-200D-1F48B-200D-1F468-1F3FB","image":"1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb.png","sheet_x":20,"sheet_y":53,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FC":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FC","non_qualified":"1F469-1F3FD-200D-2764-200D-1F48B-200D-1F468-1F3FC","image":"1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc.png","sheet_x":20,"sheet_y":54,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FD":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FD","non_qualified":"1F469-1F3FD-200D-2764-200D-1F48B-200D-1F468-1F3FD","image":"1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd.png","sheet_x":20,"sheet_y":55,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FE":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FE","non_qualified":"1F469-1F3FD-200D-2764-200D-1F48B-200D-1F468-1F3FE","image":"1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe.png","sheet_x":20,"sheet_y":56,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FF":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FF","non_qualified":"1F469-1F3FD-200D-2764-200D-1F48B-200D-1F468-1F3FF","image":"1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff.png","sheet_x":20,"sheet_y":57,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE-1F3FB":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FB","non_qualified":"1F469-1F3FE-200D-2764-200D-1F48B-200D-1F468-1F3FB","image":"1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb.png","sheet_x":20,"sheet_y":58,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE-1F3FC":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FC","non_qualified":"1F469-1F3FE-200D-2764-200D-1F48B-200D-1F468-1F3FC","image":"1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc.png","sheet_x":20,"sheet_y":59,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE-1F3FD":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FD","non_qualified":"1F469-1F3FE-200D-2764-200D-1F48B-200D-1F468-1F3FD","image":"1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd.png","sheet_x":20,"sheet_y":60,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE-1F3FE":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FE","non_qualified":"1F469-1F3FE-200D-2764-200D-1F48B-200D-1F468-1F3FE","image":"1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe.png","sheet_x":21,"sheet_y":0,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE-1F3FF":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FF","non_qualified":"1F469-1F3FE-200D-2764-200D-1F48B-200D-1F468-1F3FF","image":"1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff.png","sheet_x":21,"sheet_y":1,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF-1F3FB":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FB","non_qualified":"1F469-1F3FF-200D-2764-200D-1F48B-200D-1F468-1F3FB","image":"1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb.png","sheet_x":21,"sheet_y":2,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF-1F3FC":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FC","non_qualified":"1F469-1F3FF-200D-2764-200D-1F48B-200D-1F468-1F3FC","image":"1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc.png","sheet_x":21,"sheet_y":3,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF-1F3FD":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FD","non_qualified":"1F469-1F3FF-200D-2764-200D-1F48B-200D-1F468-1F3FD","image":"1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd.png","sheet_x":21,"sheet_y":4,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF-1F3FE":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FE","non_qualified":"1F469-1F3FF-200D-2764-200D-1F48B-200D-1F468-1F3FE","image":"1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe.png","sheet_x":21,"sheet_y":5,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF-1F3FF":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FF","non_qualified":"1F469-1F3FF-200D-2764-200D-1F48B-200D-1F468-1F3FF","image":"1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff.png","sheet_x":21,"sheet_y":6,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false}}},{"name":"KISS: WOMAN, WOMAN","unified":"1F469-200D-2764-FE0F-200D-1F48B-200D-1F469","non_qualified":"1F469-200D-2764-200D-1F48B-200D-1F469","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-2764-fe0f-200d-1f48b-200d-1f469.png","sheet_x":21,"sheet_y":7,"short_name":"woman-kiss-woman","short_names":["woman-kiss-woman"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":489,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB-1F3FB":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FB","non_qualified":"1F469-1F3FB-200D-2764-200D-1F48B-200D-1F469-1F3FB","image":"1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fb.png","sheet_x":21,"sheet_y":8,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FB-1F3FC":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FC","non_qualified":"1F469-1F3FB-200D-2764-200D-1F48B-200D-1F469-1F3FC","image":"1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fc.png","sheet_x":21,"sheet_y":9,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FB-1F3FD":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FD","non_qualified":"1F469-1F3FB-200D-2764-200D-1F48B-200D-1F469-1F3FD","image":"1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fd.png","sheet_x":21,"sheet_y":10,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FB-1F3FE":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FE","non_qualified":"1F469-1F3FB-200D-2764-200D-1F48B-200D-1F469-1F3FE","image":"1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fe.png","sheet_x":21,"sheet_y":11,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FB-1F3FF":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FF","non_qualified":"1F469-1F3FB-200D-2764-200D-1F48B-200D-1F469-1F3FF","image":"1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3ff.png","sheet_x":21,"sheet_y":12,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FB":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FB","non_qualified":"1F469-1F3FC-200D-2764-200D-1F48B-200D-1F469-1F3FB","image":"1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fb.png","sheet_x":21,"sheet_y":13,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FC":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FC","non_qualified":"1F469-1F3FC-200D-2764-200D-1F48B-200D-1F469-1F3FC","image":"1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fc.png","sheet_x":21,"sheet_y":14,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FD":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FD","non_qualified":"1F469-1F3FC-200D-2764-200D-1F48B-200D-1F469-1F3FD","image":"1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fd.png","sheet_x":21,"sheet_y":15,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FE":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FE","non_qualified":"1F469-1F3FC-200D-2764-200D-1F48B-200D-1F469-1F3FE","image":"1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fe.png","sheet_x":21,"sheet_y":16,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FF":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FF","non_qualified":"1F469-1F3FC-200D-2764-200D-1F48B-200D-1F469-1F3FF","image":"1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3ff.png","sheet_x":21,"sheet_y":17,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FB":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FB","non_qualified":"1F469-1F3FD-200D-2764-200D-1F48B-200D-1F469-1F3FB","image":"1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fb.png","sheet_x":21,"sheet_y":18,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FC":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FC","non_qualified":"1F469-1F3FD-200D-2764-200D-1F48B-200D-1F469-1F3FC","image":"1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fc.png","sheet_x":21,"sheet_y":19,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FD":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FD","non_qualified":"1F469-1F3FD-200D-2764-200D-1F48B-200D-1F469-1F3FD","image":"1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fd.png","sheet_x":21,"sheet_y":20,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FE":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FE","non_qualified":"1F469-1F3FD-200D-2764-200D-1F48B-200D-1F469-1F3FE","image":"1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fe.png","sheet_x":21,"sheet_y":21,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FF":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FF","non_qualified":"1F469-1F3FD-200D-2764-200D-1F48B-200D-1F469-1F3FF","image":"1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3ff.png","sheet_x":21,"sheet_y":22,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE-1F3FB":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FB","non_qualified":"1F469-1F3FE-200D-2764-200D-1F48B-200D-1F469-1F3FB","image":"1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fb.png","sheet_x":21,"sheet_y":23,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE-1F3FC":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FC","non_qualified":"1F469-1F3FE-200D-2764-200D-1F48B-200D-1F469-1F3FC","image":"1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fc.png","sheet_x":21,"sheet_y":24,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE-1F3FD":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FD","non_qualified":"1F469-1F3FE-200D-2764-200D-1F48B-200D-1F469-1F3FD","image":"1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fd.png","sheet_x":21,"sheet_y":25,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE-1F3FE":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FE","non_qualified":"1F469-1F3FE-200D-2764-200D-1F48B-200D-1F469-1F3FE","image":"1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fe.png","sheet_x":21,"sheet_y":26,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE-1F3FF":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FF","non_qualified":"1F469-1F3FE-200D-2764-200D-1F48B-200D-1F469-1F3FF","image":"1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3ff.png","sheet_x":21,"sheet_y":27,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF-1F3FB":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FB","non_qualified":"1F469-1F3FF-200D-2764-200D-1F48B-200D-1F469-1F3FB","image":"1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fb.png","sheet_x":21,"sheet_y":28,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF-1F3FC":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FC","non_qualified":"1F469-1F3FF-200D-2764-200D-1F48B-200D-1F469-1F3FC","image":"1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fc.png","sheet_x":21,"sheet_y":29,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF-1F3FD":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FD","non_qualified":"1F469-1F3FF-200D-2764-200D-1F48B-200D-1F469-1F3FD","image":"1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fd.png","sheet_x":21,"sheet_y":30,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF-1F3FE":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FE","non_qualified":"1F469-1F3FF-200D-2764-200D-1F48B-200D-1F469-1F3FE","image":"1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fe.png","sheet_x":21,"sheet_y":31,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF-1F3FF":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FF","non_qualified":"1F469-1F3FF-200D-2764-200D-1F48B-200D-1F469-1F3FF","image":"1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3ff.png","sheet_x":21,"sheet_y":32,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false}}},{"name":"WOMAN","unified":"1F469","non_qualified":null,"docomo":"E6F0","au":"E4FA","softbank":"E005","google":"FE19E","image":"1f469.png","sheet_x":21,"sheet_y":33,"short_name":"woman","short_names":["woman"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":237,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB","non_qualified":null,"image":"1f469-1f3fb.png","sheet_x":21,"sheet_y":34,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC","non_qualified":null,"image":"1f469-1f3fc.png","sheet_x":21,"sheet_y":35,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD","non_qualified":null,"image":"1f469-1f3fd.png","sheet_x":21,"sheet_y":36,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE","non_qualified":null,"image":"1f469-1f3fe.png","sheet_x":21,"sheet_y":37,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF","non_qualified":null,"image":"1f469-1f3ff.png","sheet_x":21,"sheet_y":38,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"FAMILY","unified":"1F46A","non_qualified":null,"docomo":null,"au":"E501","softbank":null,"google":"FE19F","image":"1f46a.png","sheet_x":21,"sheet_y":39,"short_name":"family","short_names":["family"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":494,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F468-200D-1F469-200D-1F466"},{"name":"MAN AND WOMAN HOLDING HANDS","unified":"1F46B","non_qualified":null,"docomo":null,"au":null,"softbank":"E428","google":"FE1A0","image":"1f46b.png","sheet_x":21,"sheet_y":40,"short_name":"man_and_woman_holding_hands","short_names":["man_and_woman_holding_hands","woman_and_man_holding_hands","couple"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":484,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F46B-1F3FB","non_qualified":null,"image":"1f46b-1f3fb.png","sheet_x":21,"sheet_y":41,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F46B-1F3FC","non_qualified":null,"image":"1f46b-1f3fc.png","sheet_x":21,"sheet_y":42,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F46B-1F3FD","non_qualified":null,"image":"1f46b-1f3fd.png","sheet_x":21,"sheet_y":43,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F46B-1F3FE","non_qualified":null,"image":"1f46b-1f3fe.png","sheet_x":21,"sheet_y":44,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F46B-1F3FF","non_qualified":null,"image":"1f46b-1f3ff.png","sheet_x":21,"sheet_y":45,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FC":{"unified":"1F469-1F3FB-200D-1F91D-200D-1F468-1F3FC","non_qualified":null,"image":"1f469-1f3fb-200d-1f91d-200d-1f468-1f3fc.png","sheet_x":21,"sheet_y":46,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FD":{"unified":"1F469-1F3FB-200D-1F91D-200D-1F468-1F3FD","non_qualified":null,"image":"1f469-1f3fb-200d-1f91d-200d-1f468-1f3fd.png","sheet_x":21,"sheet_y":47,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FE":{"unified":"1F469-1F3FB-200D-1F91D-200D-1F468-1F3FE","non_qualified":null,"image":"1f469-1f3fb-200d-1f91d-200d-1f468-1f3fe.png","sheet_x":21,"sheet_y":48,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FF":{"unified":"1F469-1F3FB-200D-1F91D-200D-1F468-1F3FF","non_qualified":null,"image":"1f469-1f3fb-200d-1f91d-200d-1f468-1f3ff.png","sheet_x":21,"sheet_y":49,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FB":{"unified":"1F469-1F3FC-200D-1F91D-200D-1F468-1F3FB","non_qualified":null,"image":"1f469-1f3fc-200d-1f91d-200d-1f468-1f3fb.png","sheet_x":21,"sheet_y":50,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FD":{"unified":"1F469-1F3FC-200D-1F91D-200D-1F468-1F3FD","non_qualified":null,"image":"1f469-1f3fc-200d-1f91d-200d-1f468-1f3fd.png","sheet_x":21,"sheet_y":51,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FE":{"unified":"1F469-1F3FC-200D-1F91D-200D-1F468-1F3FE","non_qualified":null,"image":"1f469-1f3fc-200d-1f91d-200d-1f468-1f3fe.png","sheet_x":21,"sheet_y":52,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FF":{"unified":"1F469-1F3FC-200D-1F91D-200D-1F468-1F3FF","non_qualified":null,"image":"1f469-1f3fc-200d-1f91d-200d-1f468-1f3ff.png","sheet_x":21,"sheet_y":53,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FB":{"unified":"1F469-1F3FD-200D-1F91D-200D-1F468-1F3FB","non_qualified":null,"image":"1f469-1f3fd-200d-1f91d-200d-1f468-1f3fb.png","sheet_x":21,"sheet_y":54,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FC":{"unified":"1F469-1F3FD-200D-1F91D-200D-1F468-1F3FC","non_qualified":null,"image":"1f469-1f3fd-200d-1f91d-200d-1f468-1f3fc.png","sheet_x":21,"sheet_y":55,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FE":{"unified":"1F469-1F3FD-200D-1F91D-200D-1F468-1F3FE","non_qualified":null,"image":"1f469-1f3fd-200d-1f91d-200d-1f468-1f3fe.png","sheet_x":21,"sheet_y":56,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FF":{"unified":"1F469-1F3FD-200D-1F91D-200D-1F468-1F3FF","non_qualified":null,"image":"1f469-1f3fd-200d-1f91d-200d-1f468-1f3ff.png","sheet_x":21,"sheet_y":57,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FB":{"unified":"1F469-1F3FE-200D-1F91D-200D-1F468-1F3FB","non_qualified":null,"image":"1f469-1f3fe-200d-1f91d-200d-1f468-1f3fb.png","sheet_x":21,"sheet_y":58,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FC":{"unified":"1F469-1F3FE-200D-1F91D-200D-1F468-1F3FC","non_qualified":null,"image":"1f469-1f3fe-200d-1f91d-200d-1f468-1f3fc.png","sheet_x":21,"sheet_y":59,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FD":{"unified":"1F469-1F3FE-200D-1F91D-200D-1F468-1F3FD","non_qualified":null,"image":"1f469-1f3fe-200d-1f91d-200d-1f468-1f3fd.png","sheet_x":21,"sheet_y":60,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FF":{"unified":"1F469-1F3FE-200D-1F91D-200D-1F468-1F3FF","non_qualified":null,"image":"1f469-1f3fe-200d-1f91d-200d-1f468-1f3ff.png","sheet_x":22,"sheet_y":0,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FB":{"unified":"1F469-1F3FF-200D-1F91D-200D-1F468-1F3FB","non_qualified":null,"image":"1f469-1f3ff-200d-1f91d-200d-1f468-1f3fb.png","sheet_x":22,"sheet_y":1,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FC":{"unified":"1F469-1F3FF-200D-1F91D-200D-1F468-1F3FC","non_qualified":null,"image":"1f469-1f3ff-200d-1f91d-200d-1f468-1f3fc.png","sheet_x":22,"sheet_y":2,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FD":{"unified":"1F469-1F3FF-200D-1F91D-200D-1F468-1F3FD","non_qualified":null,"image":"1f469-1f3ff-200d-1f91d-200d-1f468-1f3fd.png","sheet_x":22,"sheet_y":3,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FE":{"unified":"1F469-1F3FF-200D-1F91D-200D-1F468-1F3FE","non_qualified":null,"image":"1f469-1f3ff-200d-1f91d-200d-1f468-1f3fe.png","sheet_x":22,"sheet_y":4,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"TWO MEN HOLDING HANDS","unified":"1F46C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f46c.png","sheet_x":22,"sheet_y":5,"short_name":"two_men_holding_hands","short_names":["two_men_holding_hands","men_holding_hands"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":485,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F46C-1F3FB","non_qualified":null,"image":"1f46c-1f3fb.png","sheet_x":22,"sheet_y":6,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F46C-1F3FC","non_qualified":null,"image":"1f46c-1f3fc.png","sheet_x":22,"sheet_y":7,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F46C-1F3FD","non_qualified":null,"image":"1f46c-1f3fd.png","sheet_x":22,"sheet_y":8,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F46C-1F3FE","non_qualified":null,"image":"1f46c-1f3fe.png","sheet_x":22,"sheet_y":9,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F46C-1F3FF","non_qualified":null,"image":"1f46c-1f3ff.png","sheet_x":22,"sheet_y":10,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FC":{"unified":"1F468-1F3FB-200D-1F91D-200D-1F468-1F3FC","non_qualified":null,"image":"1f468-1f3fb-200d-1f91d-200d-1f468-1f3fc.png","sheet_x":22,"sheet_y":11,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FB-1F3FD":{"unified":"1F468-1F3FB-200D-1F91D-200D-1F468-1F3FD","non_qualified":null,"image":"1f468-1f3fb-200d-1f91d-200d-1f468-1f3fd.png","sheet_x":22,"sheet_y":12,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FB-1F3FE":{"unified":"1F468-1F3FB-200D-1F91D-200D-1F468-1F3FE","non_qualified":null,"image":"1f468-1f3fb-200d-1f91d-200d-1f468-1f3fe.png","sheet_x":22,"sheet_y":13,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FB-1F3FF":{"unified":"1F468-1F3FB-200D-1F91D-200D-1F468-1F3FF","non_qualified":null,"image":"1f468-1f3fb-200d-1f91d-200d-1f468-1f3ff.png","sheet_x":22,"sheet_y":14,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FB":{"unified":"1F468-1F3FC-200D-1F91D-200D-1F468-1F3FB","non_qualified":null,"image":"1f468-1f3fc-200d-1f91d-200d-1f468-1f3fb.png","sheet_x":22,"sheet_y":15,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FD":{"unified":"1F468-1F3FC-200D-1F91D-200D-1F468-1F3FD","non_qualified":null,"image":"1f468-1f3fc-200d-1f91d-200d-1f468-1f3fd.png","sheet_x":22,"sheet_y":16,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FE":{"unified":"1F468-1F3FC-200D-1F91D-200D-1F468-1F3FE","non_qualified":null,"image":"1f468-1f3fc-200d-1f91d-200d-1f468-1f3fe.png","sheet_x":22,"sheet_y":17,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FF":{"unified":"1F468-1F3FC-200D-1F91D-200D-1F468-1F3FF","non_qualified":null,"image":"1f468-1f3fc-200d-1f91d-200d-1f468-1f3ff.png","sheet_x":22,"sheet_y":18,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FB":{"unified":"1F468-1F3FD-200D-1F91D-200D-1F468-1F3FB","non_qualified":null,"image":"1f468-1f3fd-200d-1f91d-200d-1f468-1f3fb.png","sheet_x":22,"sheet_y":19,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FC":{"unified":"1F468-1F3FD-200D-1F91D-200D-1F468-1F3FC","non_qualified":null,"image":"1f468-1f3fd-200d-1f91d-200d-1f468-1f3fc.png","sheet_x":22,"sheet_y":20,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FE":{"unified":"1F468-1F3FD-200D-1F91D-200D-1F468-1F3FE","non_qualified":null,"image":"1f468-1f3fd-200d-1f91d-200d-1f468-1f3fe.png","sheet_x":22,"sheet_y":21,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FF":{"unified":"1F468-1F3FD-200D-1F91D-200D-1F468-1F3FF","non_qualified":null,"image":"1f468-1f3fd-200d-1f91d-200d-1f468-1f3ff.png","sheet_x":22,"sheet_y":22,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE-1F3FB":{"unified":"1F468-1F3FE-200D-1F91D-200D-1F468-1F3FB","non_qualified":null,"image":"1f468-1f3fe-200d-1f91d-200d-1f468-1f3fb.png","sheet_x":22,"sheet_y":23,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FC":{"unified":"1F468-1F3FE-200D-1F91D-200D-1F468-1F3FC","non_qualified":null,"image":"1f468-1f3fe-200d-1f91d-200d-1f468-1f3fc.png","sheet_x":22,"sheet_y":24,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FD":{"unified":"1F468-1F3FE-200D-1F91D-200D-1F468-1F3FD","non_qualified":null,"image":"1f468-1f3fe-200d-1f91d-200d-1f468-1f3fd.png","sheet_x":22,"sheet_y":25,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FF":{"unified":"1F468-1F3FE-200D-1F91D-200D-1F468-1F3FF","non_qualified":null,"image":"1f468-1f3fe-200d-1f91d-200d-1f468-1f3ff.png","sheet_x":22,"sheet_y":26,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF-1F3FB":{"unified":"1F468-1F3FF-200D-1F91D-200D-1F468-1F3FB","non_qualified":null,"image":"1f468-1f3ff-200d-1f91d-200d-1f468-1f3fb.png","sheet_x":22,"sheet_y":27,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FC":{"unified":"1F468-1F3FF-200D-1F91D-200D-1F468-1F3FC","non_qualified":null,"image":"1f468-1f3ff-200d-1f91d-200d-1f468-1f3fc.png","sheet_x":22,"sheet_y":28,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FD":{"unified":"1F468-1F3FF-200D-1F91D-200D-1F468-1F3FD","non_qualified":null,"image":"1f468-1f3ff-200d-1f91d-200d-1f468-1f3fd.png","sheet_x":22,"sheet_y":29,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FE":{"unified":"1F468-1F3FF-200D-1F91D-200D-1F468-1F3FE","non_qualified":null,"image":"1f468-1f3ff-200d-1f91d-200d-1f468-1f3fe.png","sheet_x":22,"sheet_y":30,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"TWO WOMEN HOLDING HANDS","unified":"1F46D","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f46d.png","sheet_x":22,"sheet_y":31,"short_name":"two_women_holding_hands","short_names":["two_women_holding_hands","women_holding_hands"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":483,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F46D-1F3FB","non_qualified":null,"image":"1f46d-1f3fb.png","sheet_x":22,"sheet_y":32,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F46D-1F3FC","non_qualified":null,"image":"1f46d-1f3fc.png","sheet_x":22,"sheet_y":33,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F46D-1F3FD","non_qualified":null,"image":"1f46d-1f3fd.png","sheet_x":22,"sheet_y":34,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F46D-1F3FE","non_qualified":null,"image":"1f46d-1f3fe.png","sheet_x":22,"sheet_y":35,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F46D-1F3FF","non_qualified":null,"image":"1f46d-1f3ff.png","sheet_x":22,"sheet_y":36,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FC":{"unified":"1F469-1F3FB-200D-1F91D-200D-1F469-1F3FC","non_qualified":null,"image":"1f469-1f3fb-200d-1f91d-200d-1f469-1f3fc.png","sheet_x":22,"sheet_y":37,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FB-1F3FD":{"unified":"1F469-1F3FB-200D-1F91D-200D-1F469-1F3FD","non_qualified":null,"image":"1f469-1f3fb-200d-1f91d-200d-1f469-1f3fd.png","sheet_x":22,"sheet_y":38,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FB-1F3FE":{"unified":"1F469-1F3FB-200D-1F91D-200D-1F469-1F3FE","non_qualified":null,"image":"1f469-1f3fb-200d-1f91d-200d-1f469-1f3fe.png","sheet_x":22,"sheet_y":39,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FB-1F3FF":{"unified":"1F469-1F3FB-200D-1F91D-200D-1F469-1F3FF","non_qualified":null,"image":"1f469-1f3fb-200d-1f91d-200d-1f469-1f3ff.png","sheet_x":22,"sheet_y":40,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FB":{"unified":"1F469-1F3FC-200D-1F91D-200D-1F469-1F3FB","non_qualified":null,"image":"1f469-1f3fc-200d-1f91d-200d-1f469-1f3fb.png","sheet_x":22,"sheet_y":41,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FD":{"unified":"1F469-1F3FC-200D-1F91D-200D-1F469-1F3FD","non_qualified":null,"image":"1f469-1f3fc-200d-1f91d-200d-1f469-1f3fd.png","sheet_x":22,"sheet_y":42,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FE":{"unified":"1F469-1F3FC-200D-1F91D-200D-1F469-1F3FE","non_qualified":null,"image":"1f469-1f3fc-200d-1f91d-200d-1f469-1f3fe.png","sheet_x":22,"sheet_y":43,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FF":{"unified":"1F469-1F3FC-200D-1F91D-200D-1F469-1F3FF","non_qualified":null,"image":"1f469-1f3fc-200d-1f91d-200d-1f469-1f3ff.png","sheet_x":22,"sheet_y":44,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FB":{"unified":"1F469-1F3FD-200D-1F91D-200D-1F469-1F3FB","non_qualified":null,"image":"1f469-1f3fd-200d-1f91d-200d-1f469-1f3fb.png","sheet_x":22,"sheet_y":45,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FC":{"unified":"1F469-1F3FD-200D-1F91D-200D-1F469-1F3FC","non_qualified":null,"image":"1f469-1f3fd-200d-1f91d-200d-1f469-1f3fc.png","sheet_x":22,"sheet_y":46,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FE":{"unified":"1F469-1F3FD-200D-1F91D-200D-1F469-1F3FE","non_qualified":null,"image":"1f469-1f3fd-200d-1f91d-200d-1f469-1f3fe.png","sheet_x":22,"sheet_y":47,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FF":{"unified":"1F469-1F3FD-200D-1F91D-200D-1F469-1F3FF","non_qualified":null,"image":"1f469-1f3fd-200d-1f91d-200d-1f469-1f3ff.png","sheet_x":22,"sheet_y":48,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE-1F3FB":{"unified":"1F469-1F3FE-200D-1F91D-200D-1F469-1F3FB","non_qualified":null,"image":"1f469-1f3fe-200d-1f91d-200d-1f469-1f3fb.png","sheet_x":22,"sheet_y":49,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FC":{"unified":"1F469-1F3FE-200D-1F91D-200D-1F469-1F3FC","non_qualified":null,"image":"1f469-1f3fe-200d-1f91d-200d-1f469-1f3fc.png","sheet_x":22,"sheet_y":50,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FD":{"unified":"1F469-1F3FE-200D-1F91D-200D-1F469-1F3FD","non_qualified":null,"image":"1f469-1f3fe-200d-1f91d-200d-1f469-1f3fd.png","sheet_x":22,"sheet_y":51,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FF":{"unified":"1F469-1F3FE-200D-1F91D-200D-1F469-1F3FF","non_qualified":null,"image":"1f469-1f3fe-200d-1f91d-200d-1f469-1f3ff.png","sheet_x":22,"sheet_y":52,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF-1F3FB":{"unified":"1F469-1F3FF-200D-1F91D-200D-1F469-1F3FB","non_qualified":null,"image":"1f469-1f3ff-200d-1f91d-200d-1f469-1f3fb.png","sheet_x":22,"sheet_y":53,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FC":{"unified":"1F469-1F3FF-200D-1F91D-200D-1F469-1F3FC","non_qualified":null,"image":"1f469-1f3ff-200d-1f91d-200d-1f469-1f3fc.png","sheet_x":22,"sheet_y":54,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FD":{"unified":"1F469-1F3FF-200D-1F91D-200D-1F469-1F3FD","non_qualified":null,"image":"1f469-1f3ff-200d-1f91d-200d-1f469-1f3fd.png","sheet_x":22,"sheet_y":55,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FE":{"unified":"1F469-1F3FF-200D-1F91D-200D-1F469-1F3FE","non_qualified":null,"image":"1f469-1f3ff-200d-1f91d-200d-1f469-1f3fe.png","sheet_x":22,"sheet_y":56,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN POLICE OFFICER","unified":"1F46E-200D-2640-FE0F","non_qualified":"1F46E-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f46e-200d-2640-fe0f.png","sheet_x":22,"sheet_y":57,"short_name":"female-police-officer","short_names":["female-police-officer"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":331,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F46E-1F3FB-200D-2640-FE0F","non_qualified":"1F46E-1F3FB-200D-2640","image":"1f46e-1f3fb-200d-2640-fe0f.png","sheet_x":22,"sheet_y":58,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F46E-1F3FC-200D-2640-FE0F","non_qualified":"1F46E-1F3FC-200D-2640","image":"1f46e-1f3fc-200d-2640-fe0f.png","sheet_x":22,"sheet_y":59,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F46E-1F3FD-200D-2640-FE0F","non_qualified":"1F46E-1F3FD-200D-2640","image":"1f46e-1f3fd-200d-2640-fe0f.png","sheet_x":22,"sheet_y":60,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F46E-1F3FE-200D-2640-FE0F","non_qualified":"1F46E-1F3FE-200D-2640","image":"1f46e-1f3fe-200d-2640-fe0f.png","sheet_x":23,"sheet_y":0,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F46E-1F3FF-200D-2640-FE0F","non_qualified":"1F46E-1F3FF-200D-2640","image":"1f46e-1f3ff-200d-2640-fe0f.png","sheet_x":23,"sheet_y":1,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN POLICE OFFICER","unified":"1F46E-200D-2642-FE0F","non_qualified":"1F46E-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f46e-200d-2642-fe0f.png","sheet_x":23,"sheet_y":2,"short_name":"male-police-officer","short_names":["male-police-officer"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":330,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F46E-1F3FB-200D-2642-FE0F","non_qualified":"1F46E-1F3FB-200D-2642","image":"1f46e-1f3fb-200d-2642-fe0f.png","sheet_x":23,"sheet_y":3,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F46E-1F3FC-200D-2642-FE0F","non_qualified":"1F46E-1F3FC-200D-2642","image":"1f46e-1f3fc-200d-2642-fe0f.png","sheet_x":23,"sheet_y":4,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F46E-1F3FD-200D-2642-FE0F","non_qualified":"1F46E-1F3FD-200D-2642","image":"1f46e-1f3fd-200d-2642-fe0f.png","sheet_x":23,"sheet_y":5,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F46E-1F3FE-200D-2642-FE0F","non_qualified":"1F46E-1F3FE-200D-2642","image":"1f46e-1f3fe-200d-2642-fe0f.png","sheet_x":23,"sheet_y":6,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F46E-1F3FF-200D-2642-FE0F","non_qualified":"1F46E-1F3FF-200D-2642","image":"1f46e-1f3ff-200d-2642-fe0f.png","sheet_x":23,"sheet_y":7,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F46E"},{"name":"POLICE OFFICER","unified":"1F46E","non_qualified":null,"docomo":null,"au":"E5DD","softbank":"E152","google":"FE1A1","image":"1f46e.png","sheet_x":23,"sheet_y":8,"short_name":"cop","short_names":["cop"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":329,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F46E-1F3FB","non_qualified":null,"image":"1f46e-1f3fb.png","sheet_x":23,"sheet_y":9,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F46E-1F3FC","non_qualified":null,"image":"1f46e-1f3fc.png","sheet_x":23,"sheet_y":10,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F46E-1F3FD","non_qualified":null,"image":"1f46e-1f3fd.png","sheet_x":23,"sheet_y":11,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F46E-1F3FE","non_qualified":null,"image":"1f46e-1f3fe.png","sheet_x":23,"sheet_y":12,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F46E-1F3FF","non_qualified":null,"image":"1f46e-1f3ff.png","sheet_x":23,"sheet_y":13,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F46E-200D-2642-FE0F"},{"name":"WOMEN WITH BUNNY EARS","unified":"1F46F-200D-2640-FE0F","non_qualified":"1F46F-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f46f-200d-2640-fe0f.png","sheet_x":23,"sheet_y":14,"short_name":"women-with-bunny-ears-partying","short_names":["women-with-bunny-ears-partying","woman-with-bunny-ears-partying"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":427,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F46F"},{"name":"MEN WITH BUNNY EARS","unified":"1F46F-200D-2642-FE0F","non_qualified":"1F46F-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f46f-200d-2642-fe0f.png","sheet_x":23,"sheet_y":15,"short_name":"men-with-bunny-ears-partying","short_names":["men-with-bunny-ears-partying","man-with-bunny-ears-partying"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":426,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WOMAN WITH BUNNY EARS","unified":"1F46F","non_qualified":null,"docomo":null,"au":"EADB","softbank":"E429","google":"FE1A2","image":"1f46f.png","sheet_x":23,"sheet_y":16,"short_name":"dancers","short_names":["dancers"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":425,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F46F-200D-2640-FE0F"},{"name":"WOMAN WITH VEIL","unified":"1F470-200D-2640-FE0F","non_qualified":"1F470-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f470-200d-2640-fe0f.png","sheet_x":23,"sheet_y":17,"short_name":"woman_with_veil","short_names":["woman_with_veil"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":355,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F470-1F3FB-200D-2640-FE0F","non_qualified":"1F470-1F3FB-200D-2640","image":"1f470-1f3fb-200d-2640-fe0f.png","sheet_x":23,"sheet_y":18,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F470-1F3FC-200D-2640-FE0F","non_qualified":"1F470-1F3FC-200D-2640","image":"1f470-1f3fc-200d-2640-fe0f.png","sheet_x":23,"sheet_y":19,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F470-1F3FD-200D-2640-FE0F","non_qualified":"1F470-1F3FD-200D-2640","image":"1f470-1f3fd-200d-2640-fe0f.png","sheet_x":23,"sheet_y":20,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F470-1F3FE-200D-2640-FE0F","non_qualified":"1F470-1F3FE-200D-2640","image":"1f470-1f3fe-200d-2640-fe0f.png","sheet_x":23,"sheet_y":21,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F470-1F3FF-200D-2640-FE0F","non_qualified":"1F470-1F3FF-200D-2640","image":"1f470-1f3ff-200d-2640-fe0f.png","sheet_x":23,"sheet_y":22,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN WITH VEIL","unified":"1F470-200D-2642-FE0F","non_qualified":"1F470-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f470-200d-2642-fe0f.png","sheet_x":23,"sheet_y":23,"short_name":"man_with_veil","short_names":["man_with_veil"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":354,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F470-1F3FB-200D-2642-FE0F","non_qualified":"1F470-1F3FB-200D-2642","image":"1f470-1f3fb-200d-2642-fe0f.png","sheet_x":23,"sheet_y":24,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F470-1F3FC-200D-2642-FE0F","non_qualified":"1F470-1F3FC-200D-2642","image":"1f470-1f3fc-200d-2642-fe0f.png","sheet_x":23,"sheet_y":25,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F470-1F3FD-200D-2642-FE0F","non_qualified":"1F470-1F3FD-200D-2642","image":"1f470-1f3fd-200d-2642-fe0f.png","sheet_x":23,"sheet_y":26,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F470-1F3FE-200D-2642-FE0F","non_qualified":"1F470-1F3FE-200D-2642","image":"1f470-1f3fe-200d-2642-fe0f.png","sheet_x":23,"sheet_y":27,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F470-1F3FF-200D-2642-FE0F","non_qualified":"1F470-1F3FF-200D-2642","image":"1f470-1f3ff-200d-2642-fe0f.png","sheet_x":23,"sheet_y":28,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"BRIDE WITH VEIL","unified":"1F470","non_qualified":null,"docomo":null,"au":"EAE9","softbank":null,"google":"FE1A3","image":"1f470.png","sheet_x":23,"sheet_y":29,"short_name":"bride_with_veil","short_names":["bride_with_veil"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":353,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F470-1F3FB","non_qualified":null,"image":"1f470-1f3fb.png","sheet_x":23,"sheet_y":30,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F470-1F3FC","non_qualified":null,"image":"1f470-1f3fc.png","sheet_x":23,"sheet_y":31,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F470-1F3FD","non_qualified":null,"image":"1f470-1f3fd.png","sheet_x":23,"sheet_y":32,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F470-1F3FE","non_qualified":null,"image":"1f470-1f3fe.png","sheet_x":23,"sheet_y":33,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F470-1F3FF","non_qualified":null,"image":"1f470-1f3ff.png","sheet_x":23,"sheet_y":34,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN: BLOND HAIR","unified":"1F471-200D-2640-FE0F","non_qualified":"1F471-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f471-200d-2640-fe0f.png","sheet_x":23,"sheet_y":35,"short_name":"blond-haired-woman","short_names":["blond-haired-woman"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":246,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F471-1F3FB-200D-2640-FE0F","non_qualified":"1F471-1F3FB-200D-2640","image":"1f471-1f3fb-200d-2640-fe0f.png","sheet_x":23,"sheet_y":36,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F471-1F3FC-200D-2640-FE0F","non_qualified":"1F471-1F3FC-200D-2640","image":"1f471-1f3fc-200d-2640-fe0f.png","sheet_x":23,"sheet_y":37,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F471-1F3FD-200D-2640-FE0F","non_qualified":"1F471-1F3FD-200D-2640","image":"1f471-1f3fd-200d-2640-fe0f.png","sheet_x":23,"sheet_y":38,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F471-1F3FE-200D-2640-FE0F","non_qualified":"1F471-1F3FE-200D-2640","image":"1f471-1f3fe-200d-2640-fe0f.png","sheet_x":23,"sheet_y":39,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F471-1F3FF-200D-2640-FE0F","non_qualified":"1F471-1F3FF-200D-2640","image":"1f471-1f3ff-200d-2640-fe0f.png","sheet_x":23,"sheet_y":40,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN: BLOND HAIR","unified":"1F471-200D-2642-FE0F","non_qualified":"1F471-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f471-200d-2642-fe0f.png","sheet_x":23,"sheet_y":41,"short_name":"blond-haired-man","short_names":["blond-haired-man"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":247,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F471-1F3FB-200D-2642-FE0F","non_qualified":"1F471-1F3FB-200D-2642","image":"1f471-1f3fb-200d-2642-fe0f.png","sheet_x":23,"sheet_y":42,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F471-1F3FC-200D-2642-FE0F","non_qualified":"1F471-1F3FC-200D-2642","image":"1f471-1f3fc-200d-2642-fe0f.png","sheet_x":23,"sheet_y":43,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F471-1F3FD-200D-2642-FE0F","non_qualified":"1F471-1F3FD-200D-2642","image":"1f471-1f3fd-200d-2642-fe0f.png","sheet_x":23,"sheet_y":44,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F471-1F3FE-200D-2642-FE0F","non_qualified":"1F471-1F3FE-200D-2642","image":"1f471-1f3fe-200d-2642-fe0f.png","sheet_x":23,"sheet_y":45,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F471-1F3FF-200D-2642-FE0F","non_qualified":"1F471-1F3FF-200D-2642","image":"1f471-1f3ff-200d-2642-fe0f.png","sheet_x":23,"sheet_y":46,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F471"},{"name":"PERSON WITH BLOND HAIR","unified":"1F471","non_qualified":null,"docomo":null,"au":"EB13","softbank":"E515","google":"FE1A4","image":"1f471.png","sheet_x":23,"sheet_y":47,"short_name":"person_with_blond_hair","short_names":["person_with_blond_hair"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":228,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F471-1F3FB","non_qualified":null,"image":"1f471-1f3fb.png","sheet_x":23,"sheet_y":48,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F471-1F3FC","non_qualified":null,"image":"1f471-1f3fc.png","sheet_x":23,"sheet_y":49,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F471-1F3FD","non_qualified":null,"image":"1f471-1f3fd.png","sheet_x":23,"sheet_y":50,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F471-1F3FE","non_qualified":null,"image":"1f471-1f3fe.png","sheet_x":23,"sheet_y":51,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F471-1F3FF","non_qualified":null,"image":"1f471-1f3ff.png","sheet_x":23,"sheet_y":52,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F471-200D-2642-FE0F"},{"name":"MAN WITH GUA PI MAO","unified":"1F472","non_qualified":null,"docomo":null,"au":"EB14","softbank":"E516","google":"FE1A5","image":"1f472.png","sheet_x":23,"sheet_y":53,"short_name":"man_with_gua_pi_mao","short_names":["man_with_gua_pi_mao"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":348,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F472-1F3FB","non_qualified":null,"image":"1f472-1f3fb.png","sheet_x":23,"sheet_y":54,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F472-1F3FC","non_qualified":null,"image":"1f472-1f3fc.png","sheet_x":23,"sheet_y":55,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F472-1F3FD","non_qualified":null,"image":"1f472-1f3fd.png","sheet_x":23,"sheet_y":56,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F472-1F3FE","non_qualified":null,"image":"1f472-1f3fe.png","sheet_x":23,"sheet_y":57,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F472-1F3FF","non_qualified":null,"image":"1f472-1f3ff.png","sheet_x":23,"sheet_y":58,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN WEARING TURBAN","unified":"1F473-200D-2640-FE0F","non_qualified":"1F473-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f473-200d-2640-fe0f.png","sheet_x":23,"sheet_y":59,"short_name":"woman-wearing-turban","short_names":["woman-wearing-turban"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":347,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F473-1F3FB-200D-2640-FE0F","non_qualified":"1F473-1F3FB-200D-2640","image":"1f473-1f3fb-200d-2640-fe0f.png","sheet_x":23,"sheet_y":60,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F473-1F3FC-200D-2640-FE0F","non_qualified":"1F473-1F3FC-200D-2640","image":"1f473-1f3fc-200d-2640-fe0f.png","sheet_x":24,"sheet_y":0,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F473-1F3FD-200D-2640-FE0F","non_qualified":"1F473-1F3FD-200D-2640","image":"1f473-1f3fd-200d-2640-fe0f.png","sheet_x":24,"sheet_y":1,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F473-1F3FE-200D-2640-FE0F","non_qualified":"1F473-1F3FE-200D-2640","image":"1f473-1f3fe-200d-2640-fe0f.png","sheet_x":24,"sheet_y":2,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F473-1F3FF-200D-2640-FE0F","non_qualified":"1F473-1F3FF-200D-2640","image":"1f473-1f3ff-200d-2640-fe0f.png","sheet_x":24,"sheet_y":3,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN WEARING TURBAN","unified":"1F473-200D-2642-FE0F","non_qualified":"1F473-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f473-200d-2642-fe0f.png","sheet_x":24,"sheet_y":4,"short_name":"man-wearing-turban","short_names":["man-wearing-turban"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":346,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F473-1F3FB-200D-2642-FE0F","non_qualified":"1F473-1F3FB-200D-2642","image":"1f473-1f3fb-200d-2642-fe0f.png","sheet_x":24,"sheet_y":5,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F473-1F3FC-200D-2642-FE0F","non_qualified":"1F473-1F3FC-200D-2642","image":"1f473-1f3fc-200d-2642-fe0f.png","sheet_x":24,"sheet_y":6,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F473-1F3FD-200D-2642-FE0F","non_qualified":"1F473-1F3FD-200D-2642","image":"1f473-1f3fd-200d-2642-fe0f.png","sheet_x":24,"sheet_y":7,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F473-1F3FE-200D-2642-FE0F","non_qualified":"1F473-1F3FE-200D-2642","image":"1f473-1f3fe-200d-2642-fe0f.png","sheet_x":24,"sheet_y":8,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F473-1F3FF-200D-2642-FE0F","non_qualified":"1F473-1F3FF-200D-2642","image":"1f473-1f3ff-200d-2642-fe0f.png","sheet_x":24,"sheet_y":9,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F473"},{"name":"MAN WITH TURBAN","unified":"1F473","non_qualified":null,"docomo":null,"au":"EB15","softbank":"E517","google":"FE1A6","image":"1f473.png","sheet_x":24,"sheet_y":10,"short_name":"man_with_turban","short_names":["man_with_turban"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":345,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F473-1F3FB","non_qualified":null,"image":"1f473-1f3fb.png","sheet_x":24,"sheet_y":11,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F473-1F3FC","non_qualified":null,"image":"1f473-1f3fc.png","sheet_x":24,"sheet_y":12,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F473-1F3FD","non_qualified":null,"image":"1f473-1f3fd.png","sheet_x":24,"sheet_y":13,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F473-1F3FE","non_qualified":null,"image":"1f473-1f3fe.png","sheet_x":24,"sheet_y":14,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F473-1F3FF","non_qualified":null,"image":"1f473-1f3ff.png","sheet_x":24,"sheet_y":15,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F473-200D-2642-FE0F"},{"name":"OLDER MAN","unified":"1F474","non_qualified":null,"docomo":null,"au":"EB16","softbank":"E518","google":"FE1A7","image":"1f474.png","sheet_x":24,"sheet_y":16,"short_name":"older_man","short_names":["older_man"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":249,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F474-1F3FB","non_qualified":null,"image":"1f474-1f3fb.png","sheet_x":24,"sheet_y":17,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F474-1F3FC","non_qualified":null,"image":"1f474-1f3fc.png","sheet_x":24,"sheet_y":18,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F474-1F3FD","non_qualified":null,"image":"1f474-1f3fd.png","sheet_x":24,"sheet_y":19,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F474-1F3FE","non_qualified":null,"image":"1f474-1f3fe.png","sheet_x":24,"sheet_y":20,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F474-1F3FF","non_qualified":null,"image":"1f474-1f3ff.png","sheet_x":24,"sheet_y":21,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"OLDER WOMAN","unified":"1F475","non_qualified":null,"docomo":null,"au":"EB17","softbank":"E519","google":"FE1A8","image":"1f475.png","sheet_x":24,"sheet_y":22,"short_name":"older_woman","short_names":["older_woman"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":250,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F475-1F3FB","non_qualified":null,"image":"1f475-1f3fb.png","sheet_x":24,"sheet_y":23,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F475-1F3FC","non_qualified":null,"image":"1f475-1f3fc.png","sheet_x":24,"sheet_y":24,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F475-1F3FD","non_qualified":null,"image":"1f475-1f3fd.png","sheet_x":24,"sheet_y":25,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F475-1F3FE","non_qualified":null,"image":"1f475-1f3fe.png","sheet_x":24,"sheet_y":26,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F475-1F3FF","non_qualified":null,"image":"1f475-1f3ff.png","sheet_x":24,"sheet_y":27,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"BABY","unified":"1F476","non_qualified":null,"docomo":null,"au":"EB18","softbank":"E51A","google":"FE1A9","image":"1f476.png","sheet_x":24,"sheet_y":28,"short_name":"baby","short_names":["baby"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":223,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F476-1F3FB","non_qualified":null,"image":"1f476-1f3fb.png","sheet_x":24,"sheet_y":29,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F476-1F3FC","non_qualified":null,"image":"1f476-1f3fc.png","sheet_x":24,"sheet_y":30,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F476-1F3FD","non_qualified":null,"image":"1f476-1f3fd.png","sheet_x":24,"sheet_y":31,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F476-1F3FE","non_qualified":null,"image":"1f476-1f3fe.png","sheet_x":24,"sheet_y":32,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F476-1F3FF","non_qualified":null,"image":"1f476-1f3ff.png","sheet_x":24,"sheet_y":33,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN CONSTRUCTION WORKER","unified":"1F477-200D-2640-FE0F","non_qualified":"1F477-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f477-200d-2640-fe0f.png","sheet_x":24,"sheet_y":34,"short_name":"female-construction-worker","short_names":["female-construction-worker"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":341,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F477-1F3FB-200D-2640-FE0F","non_qualified":"1F477-1F3FB-200D-2640","image":"1f477-1f3fb-200d-2640-fe0f.png","sheet_x":24,"sheet_y":35,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F477-1F3FC-200D-2640-FE0F","non_qualified":"1F477-1F3FC-200D-2640","image":"1f477-1f3fc-200d-2640-fe0f.png","sheet_x":24,"sheet_y":36,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F477-1F3FD-200D-2640-FE0F","non_qualified":"1F477-1F3FD-200D-2640","image":"1f477-1f3fd-200d-2640-fe0f.png","sheet_x":24,"sheet_y":37,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F477-1F3FE-200D-2640-FE0F","non_qualified":"1F477-1F3FE-200D-2640","image":"1f477-1f3fe-200d-2640-fe0f.png","sheet_x":24,"sheet_y":38,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F477-1F3FF-200D-2640-FE0F","non_qualified":"1F477-1F3FF-200D-2640","image":"1f477-1f3ff-200d-2640-fe0f.png","sheet_x":24,"sheet_y":39,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN CONSTRUCTION WORKER","unified":"1F477-200D-2642-FE0F","non_qualified":"1F477-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f477-200d-2642-fe0f.png","sheet_x":24,"sheet_y":40,"short_name":"male-construction-worker","short_names":["male-construction-worker"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":340,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F477-1F3FB-200D-2642-FE0F","non_qualified":"1F477-1F3FB-200D-2642","image":"1f477-1f3fb-200d-2642-fe0f.png","sheet_x":24,"sheet_y":41,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F477-1F3FC-200D-2642-FE0F","non_qualified":"1F477-1F3FC-200D-2642","image":"1f477-1f3fc-200d-2642-fe0f.png","sheet_x":24,"sheet_y":42,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F477-1F3FD-200D-2642-FE0F","non_qualified":"1F477-1F3FD-200D-2642","image":"1f477-1f3fd-200d-2642-fe0f.png","sheet_x":24,"sheet_y":43,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F477-1F3FE-200D-2642-FE0F","non_qualified":"1F477-1F3FE-200D-2642","image":"1f477-1f3fe-200d-2642-fe0f.png","sheet_x":24,"sheet_y":44,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F477-1F3FF-200D-2642-FE0F","non_qualified":"1F477-1F3FF-200D-2642","image":"1f477-1f3ff-200d-2642-fe0f.png","sheet_x":24,"sheet_y":45,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F477"},{"name":"CONSTRUCTION WORKER","unified":"1F477","non_qualified":null,"docomo":null,"au":"EB19","softbank":"E51B","google":"FE1AA","image":"1f477.png","sheet_x":24,"sheet_y":46,"short_name":"construction_worker","short_names":["construction_worker"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":339,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F477-1F3FB","non_qualified":null,"image":"1f477-1f3fb.png","sheet_x":24,"sheet_y":47,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F477-1F3FC","non_qualified":null,"image":"1f477-1f3fc.png","sheet_x":24,"sheet_y":48,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F477-1F3FD","non_qualified":null,"image":"1f477-1f3fd.png","sheet_x":24,"sheet_y":49,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F477-1F3FE","non_qualified":null,"image":"1f477-1f3fe.png","sheet_x":24,"sheet_y":50,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F477-1F3FF","non_qualified":null,"image":"1f477-1f3ff.png","sheet_x":24,"sheet_y":51,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F477-200D-2642-FE0F"},{"name":"PRINCESS","unified":"1F478","non_qualified":null,"docomo":null,"au":"EB1A","softbank":"E51C","google":"FE1AB","image":"1f478.png","sheet_x":24,"sheet_y":52,"short_name":"princess","short_names":["princess"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":344,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F478-1F3FB","non_qualified":null,"image":"1f478-1f3fb.png","sheet_x":24,"sheet_y":53,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F478-1F3FC","non_qualified":null,"image":"1f478-1f3fc.png","sheet_x":24,"sheet_y":54,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F478-1F3FD","non_qualified":null,"image":"1f478-1f3fd.png","sheet_x":24,"sheet_y":55,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F478-1F3FE","non_qualified":null,"image":"1f478-1f3fe.png","sheet_x":24,"sheet_y":56,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F478-1F3FF","non_qualified":null,"image":"1f478-1f3ff.png","sheet_x":24,"sheet_y":57,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"JAPANESE OGRE","unified":"1F479","non_qualified":null,"docomo":null,"au":"EB44","softbank":null,"google":"FE1AC","image":"1f479.png","sheet_x":24,"sheet_y":58,"short_name":"japanese_ogre","short_names":["japanese_ogre"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-costume","sort_order":109,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"JAPANESE GOBLIN","unified":"1F47A","non_qualified":null,"docomo":null,"au":"EB45","softbank":null,"google":"FE1AD","image":"1f47a.png","sheet_x":24,"sheet_y":59,"short_name":"japanese_goblin","short_names":["japanese_goblin"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-costume","sort_order":110,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GHOST","unified":"1F47B","non_qualified":null,"docomo":null,"au":"E4CB","softbank":"E11B","google":"FE1AE","image":"1f47b.png","sheet_x":24,"sheet_y":60,"short_name":"ghost","short_names":["ghost"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-costume","sort_order":111,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BABY ANGEL","unified":"1F47C","non_qualified":null,"docomo":null,"au":"E5BF","softbank":"E04E","google":"FE1AF","image":"1f47c.png","sheet_x":25,"sheet_y":0,"short_name":"angel","short_names":["angel"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":363,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F47C-1F3FB","non_qualified":null,"image":"1f47c-1f3fb.png","sheet_x":25,"sheet_y":1,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F47C-1F3FC","non_qualified":null,"image":"1f47c-1f3fc.png","sheet_x":25,"sheet_y":2,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F47C-1F3FD","non_qualified":null,"image":"1f47c-1f3fd.png","sheet_x":25,"sheet_y":3,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F47C-1F3FE","non_qualified":null,"image":"1f47c-1f3fe.png","sheet_x":25,"sheet_y":4,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F47C-1F3FF","non_qualified":null,"image":"1f47c-1f3ff.png","sheet_x":25,"sheet_y":5,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"EXTRATERRESTRIAL ALIEN","unified":"1F47D","non_qualified":null,"docomo":null,"au":"E50E","softbank":"E10C","google":"FE1B0","image":"1f47d.png","sheet_x":25,"sheet_y":6,"short_name":"alien","short_names":["alien"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-costume","sort_order":112,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ALIEN MONSTER","unified":"1F47E","non_qualified":null,"docomo":null,"au":"E4EC","softbank":"E12B","google":"FE1B1","image":"1f47e.png","sheet_x":25,"sheet_y":7,"short_name":"space_invader","short_names":["space_invader"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-costume","sort_order":113,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"IMP","unified":"1F47F","non_qualified":null,"docomo":null,"au":"E4EF","softbank":"E11A","google":"FE1B2","image":"1f47f.png","sheet_x":25,"sheet_y":8,"short_name":"imp","short_names":["imp"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-negative","sort_order":104,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SKULL","unified":"1F480","non_qualified":null,"docomo":null,"au":"E4F8","softbank":"E11C","google":"FE1B3","image":"1f480.png","sheet_x":25,"sheet_y":9,"short_name":"skull","short_names":["skull"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-negative","sort_order":105,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WOMAN TIPPING HAND","unified":"1F481-200D-2640-FE0F","non_qualified":"1F481-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f481-200d-2640-fe0f.png","sheet_x":25,"sheet_y":10,"short_name":"woman-tipping-hand","short_names":["woman-tipping-hand"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":265,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F481-1F3FB-200D-2640-FE0F","non_qualified":"1F481-1F3FB-200D-2640","image":"1f481-1f3fb-200d-2640-fe0f.png","sheet_x":25,"sheet_y":11,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F481-1F3FC-200D-2640-FE0F","non_qualified":"1F481-1F3FC-200D-2640","image":"1f481-1f3fc-200d-2640-fe0f.png","sheet_x":25,"sheet_y":12,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F481-1F3FD-200D-2640-FE0F","non_qualified":"1F481-1F3FD-200D-2640","image":"1f481-1f3fd-200d-2640-fe0f.png","sheet_x":25,"sheet_y":13,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F481-1F3FE-200D-2640-FE0F","non_qualified":"1F481-1F3FE-200D-2640","image":"1f481-1f3fe-200d-2640-fe0f.png","sheet_x":25,"sheet_y":14,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F481-1F3FF-200D-2640-FE0F","non_qualified":"1F481-1F3FF-200D-2640","image":"1f481-1f3ff-200d-2640-fe0f.png","sheet_x":25,"sheet_y":15,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F481"},{"name":"MAN TIPPING HAND","unified":"1F481-200D-2642-FE0F","non_qualified":"1F481-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f481-200d-2642-fe0f.png","sheet_x":25,"sheet_y":16,"short_name":"man-tipping-hand","short_names":["man-tipping-hand"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":264,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F481-1F3FB-200D-2642-FE0F","non_qualified":"1F481-1F3FB-200D-2642","image":"1f481-1f3fb-200d-2642-fe0f.png","sheet_x":25,"sheet_y":17,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F481-1F3FC-200D-2642-FE0F","non_qualified":"1F481-1F3FC-200D-2642","image":"1f481-1f3fc-200d-2642-fe0f.png","sheet_x":25,"sheet_y":18,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F481-1F3FD-200D-2642-FE0F","non_qualified":"1F481-1F3FD-200D-2642","image":"1f481-1f3fd-200d-2642-fe0f.png","sheet_x":25,"sheet_y":19,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F481-1F3FE-200D-2642-FE0F","non_qualified":"1F481-1F3FE-200D-2642","image":"1f481-1f3fe-200d-2642-fe0f.png","sheet_x":25,"sheet_y":20,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F481-1F3FF-200D-2642-FE0F","non_qualified":"1F481-1F3FF-200D-2642","image":"1f481-1f3ff-200d-2642-fe0f.png","sheet_x":25,"sheet_y":21,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"INFORMATION DESK PERSON","unified":"1F481","non_qualified":null,"docomo":null,"au":null,"softbank":"E253","google":"FE1B4","image":"1f481.png","sheet_x":25,"sheet_y":22,"short_name":"information_desk_person","short_names":["information_desk_person"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":263,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F481-1F3FB","non_qualified":null,"image":"1f481-1f3fb.png","sheet_x":25,"sheet_y":23,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F481-1F3FC","non_qualified":null,"image":"1f481-1f3fc.png","sheet_x":25,"sheet_y":24,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F481-1F3FD","non_qualified":null,"image":"1f481-1f3fd.png","sheet_x":25,"sheet_y":25,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F481-1F3FE","non_qualified":null,"image":"1f481-1f3fe.png","sheet_x":25,"sheet_y":26,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F481-1F3FF","non_qualified":null,"image":"1f481-1f3ff.png","sheet_x":25,"sheet_y":27,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F481-200D-2640-FE0F"},{"name":"WOMAN GUARD","unified":"1F482-200D-2640-FE0F","non_qualified":"1F482-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f482-200d-2640-fe0f.png","sheet_x":25,"sheet_y":28,"short_name":"female-guard","short_names":["female-guard"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":337,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F482-1F3FB-200D-2640-FE0F","non_qualified":"1F482-1F3FB-200D-2640","image":"1f482-1f3fb-200d-2640-fe0f.png","sheet_x":25,"sheet_y":29,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F482-1F3FC-200D-2640-FE0F","non_qualified":"1F482-1F3FC-200D-2640","image":"1f482-1f3fc-200d-2640-fe0f.png","sheet_x":25,"sheet_y":30,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F482-1F3FD-200D-2640-FE0F","non_qualified":"1F482-1F3FD-200D-2640","image":"1f482-1f3fd-200d-2640-fe0f.png","sheet_x":25,"sheet_y":31,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F482-1F3FE-200D-2640-FE0F","non_qualified":"1F482-1F3FE-200D-2640","image":"1f482-1f3fe-200d-2640-fe0f.png","sheet_x":25,"sheet_y":32,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F482-1F3FF-200D-2640-FE0F","non_qualified":"1F482-1F3FF-200D-2640","image":"1f482-1f3ff-200d-2640-fe0f.png","sheet_x":25,"sheet_y":33,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN GUARD","unified":"1F482-200D-2642-FE0F","non_qualified":"1F482-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f482-200d-2642-fe0f.png","sheet_x":25,"sheet_y":34,"short_name":"male-guard","short_names":["male-guard"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":336,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F482-1F3FB-200D-2642-FE0F","non_qualified":"1F482-1F3FB-200D-2642","image":"1f482-1f3fb-200d-2642-fe0f.png","sheet_x":25,"sheet_y":35,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F482-1F3FC-200D-2642-FE0F","non_qualified":"1F482-1F3FC-200D-2642","image":"1f482-1f3fc-200d-2642-fe0f.png","sheet_x":25,"sheet_y":36,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F482-1F3FD-200D-2642-FE0F","non_qualified":"1F482-1F3FD-200D-2642","image":"1f482-1f3fd-200d-2642-fe0f.png","sheet_x":25,"sheet_y":37,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F482-1F3FE-200D-2642-FE0F","non_qualified":"1F482-1F3FE-200D-2642","image":"1f482-1f3fe-200d-2642-fe0f.png","sheet_x":25,"sheet_y":38,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F482-1F3FF-200D-2642-FE0F","non_qualified":"1F482-1F3FF-200D-2642","image":"1f482-1f3ff-200d-2642-fe0f.png","sheet_x":25,"sheet_y":39,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F482"},{"name":"GUARDSMAN","unified":"1F482","non_qualified":null,"docomo":null,"au":null,"softbank":"E51E","google":"FE1B5","image":"1f482.png","sheet_x":25,"sheet_y":40,"short_name":"guardsman","short_names":["guardsman"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":335,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F482-1F3FB","non_qualified":null,"image":"1f482-1f3fb.png","sheet_x":25,"sheet_y":41,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F482-1F3FC","non_qualified":null,"image":"1f482-1f3fc.png","sheet_x":25,"sheet_y":42,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F482-1F3FD","non_qualified":null,"image":"1f482-1f3fd.png","sheet_x":25,"sheet_y":43,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F482-1F3FE","non_qualified":null,"image":"1f482-1f3fe.png","sheet_x":25,"sheet_y":44,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F482-1F3FF","non_qualified":null,"image":"1f482-1f3ff.png","sheet_x":25,"sheet_y":45,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F482-200D-2642-FE0F"},{"name":"DANCER","unified":"1F483","non_qualified":null,"docomo":null,"au":"EB1C","softbank":"E51F","google":"FE1B6","image":"1f483.png","sheet_x":25,"sheet_y":46,"short_name":"dancer","short_names":["dancer"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":422,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F483-1F3FB","non_qualified":null,"image":"1f483-1f3fb.png","sheet_x":25,"sheet_y":47,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F483-1F3FC","non_qualified":null,"image":"1f483-1f3fc.png","sheet_x":25,"sheet_y":48,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F483-1F3FD","non_qualified":null,"image":"1f483-1f3fd.png","sheet_x":25,"sheet_y":49,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F483-1F3FE","non_qualified":null,"image":"1f483-1f3fe.png","sheet_x":25,"sheet_y":50,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F483-1F3FF","non_qualified":null,"image":"1f483-1f3ff.png","sheet_x":25,"sheet_y":51,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"LIPSTICK","unified":"1F484","non_qualified":null,"docomo":"E710","au":"E509","softbank":"E31C","google":"FE195","image":"1f484.png","sheet_x":25,"sheet_y":52,"short_name":"lipstick","short_names":["lipstick"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1152,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NAIL POLISH","unified":"1F485","non_qualified":null,"docomo":null,"au":"EAA0","softbank":"E31D","google":"FE196","image":"1f485.png","sheet_x":25,"sheet_y":53,"short_name":"nail_care","short_names":["nail_care"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-prop","sort_order":203,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F485-1F3FB","non_qualified":null,"image":"1f485-1f3fb.png","sheet_x":25,"sheet_y":54,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F485-1F3FC","non_qualified":null,"image":"1f485-1f3fc.png","sheet_x":25,"sheet_y":55,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F485-1F3FD","non_qualified":null,"image":"1f485-1f3fd.png","sheet_x":25,"sheet_y":56,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F485-1F3FE","non_qualified":null,"image":"1f485-1f3fe.png","sheet_x":25,"sheet_y":57,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F485-1F3FF","non_qualified":null,"image":"1f485-1f3ff.png","sheet_x":25,"sheet_y":58,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN GETTING MASSAGE","unified":"1F486-200D-2640-FE0F","non_qualified":"1F486-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f486-200d-2640-fe0f.png","sheet_x":25,"sheet_y":59,"short_name":"woman-getting-massage","short_names":["woman-getting-massage"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":397,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F486-1F3FB-200D-2640-FE0F","non_qualified":"1F486-1F3FB-200D-2640","image":"1f486-1f3fb-200d-2640-fe0f.png","sheet_x":25,"sheet_y":60,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F486-1F3FC-200D-2640-FE0F","non_qualified":"1F486-1F3FC-200D-2640","image":"1f486-1f3fc-200d-2640-fe0f.png","sheet_x":26,"sheet_y":0,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F486-1F3FD-200D-2640-FE0F","non_qualified":"1F486-1F3FD-200D-2640","image":"1f486-1f3fd-200d-2640-fe0f.png","sheet_x":26,"sheet_y":1,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F486-1F3FE-200D-2640-FE0F","non_qualified":"1F486-1F3FE-200D-2640","image":"1f486-1f3fe-200d-2640-fe0f.png","sheet_x":26,"sheet_y":2,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F486-1F3FF-200D-2640-FE0F","non_qualified":"1F486-1F3FF-200D-2640","image":"1f486-1f3ff-200d-2640-fe0f.png","sheet_x":26,"sheet_y":3,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F486"},{"name":"MAN GETTING MASSAGE","unified":"1F486-200D-2642-FE0F","non_qualified":"1F486-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f486-200d-2642-fe0f.png","sheet_x":26,"sheet_y":4,"short_name":"man-getting-massage","short_names":["man-getting-massage"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":396,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F486-1F3FB-200D-2642-FE0F","non_qualified":"1F486-1F3FB-200D-2642","image":"1f486-1f3fb-200d-2642-fe0f.png","sheet_x":26,"sheet_y":5,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F486-1F3FC-200D-2642-FE0F","non_qualified":"1F486-1F3FC-200D-2642","image":"1f486-1f3fc-200d-2642-fe0f.png","sheet_x":26,"sheet_y":6,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F486-1F3FD-200D-2642-FE0F","non_qualified":"1F486-1F3FD-200D-2642","image":"1f486-1f3fd-200d-2642-fe0f.png","sheet_x":26,"sheet_y":7,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F486-1F3FE-200D-2642-FE0F","non_qualified":"1F486-1F3FE-200D-2642","image":"1f486-1f3fe-200d-2642-fe0f.png","sheet_x":26,"sheet_y":8,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F486-1F3FF-200D-2642-FE0F","non_qualified":"1F486-1F3FF-200D-2642","image":"1f486-1f3ff-200d-2642-fe0f.png","sheet_x":26,"sheet_y":9,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"FACE MASSAGE","unified":"1F486","non_qualified":null,"docomo":null,"au":"E50B","softbank":"E31E","google":"FE197","image":"1f486.png","sheet_x":26,"sheet_y":10,"short_name":"massage","short_names":["massage"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":395,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F486-1F3FB","non_qualified":null,"image":"1f486-1f3fb.png","sheet_x":26,"sheet_y":11,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F486-1F3FC","non_qualified":null,"image":"1f486-1f3fc.png","sheet_x":26,"sheet_y":12,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F486-1F3FD","non_qualified":null,"image":"1f486-1f3fd.png","sheet_x":26,"sheet_y":13,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F486-1F3FE","non_qualified":null,"image":"1f486-1f3fe.png","sheet_x":26,"sheet_y":14,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F486-1F3FF","non_qualified":null,"image":"1f486-1f3ff.png","sheet_x":26,"sheet_y":15,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F486-200D-2640-FE0F"},{"name":"WOMAN GETTING HAIRCUT","unified":"1F487-200D-2640-FE0F","non_qualified":"1F487-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f487-200d-2640-fe0f.png","sheet_x":26,"sheet_y":16,"short_name":"woman-getting-haircut","short_names":["woman-getting-haircut"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":400,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F487-1F3FB-200D-2640-FE0F","non_qualified":"1F487-1F3FB-200D-2640","image":"1f487-1f3fb-200d-2640-fe0f.png","sheet_x":26,"sheet_y":17,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F487-1F3FC-200D-2640-FE0F","non_qualified":"1F487-1F3FC-200D-2640","image":"1f487-1f3fc-200d-2640-fe0f.png","sheet_x":26,"sheet_y":18,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F487-1F3FD-200D-2640-FE0F","non_qualified":"1F487-1F3FD-200D-2640","image":"1f487-1f3fd-200d-2640-fe0f.png","sheet_x":26,"sheet_y":19,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F487-1F3FE-200D-2640-FE0F","non_qualified":"1F487-1F3FE-200D-2640","image":"1f487-1f3fe-200d-2640-fe0f.png","sheet_x":26,"sheet_y":20,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F487-1F3FF-200D-2640-FE0F","non_qualified":"1F487-1F3FF-200D-2640","image":"1f487-1f3ff-200d-2640-fe0f.png","sheet_x":26,"sheet_y":21,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F487"},{"name":"MAN GETTING HAIRCUT","unified":"1F487-200D-2642-FE0F","non_qualified":"1F487-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f487-200d-2642-fe0f.png","sheet_x":26,"sheet_y":22,"short_name":"man-getting-haircut","short_names":["man-getting-haircut"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":399,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F487-1F3FB-200D-2642-FE0F","non_qualified":"1F487-1F3FB-200D-2642","image":"1f487-1f3fb-200d-2642-fe0f.png","sheet_x":26,"sheet_y":23,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F487-1F3FC-200D-2642-FE0F","non_qualified":"1F487-1F3FC-200D-2642","image":"1f487-1f3fc-200d-2642-fe0f.png","sheet_x":26,"sheet_y":24,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F487-1F3FD-200D-2642-FE0F","non_qualified":"1F487-1F3FD-200D-2642","image":"1f487-1f3fd-200d-2642-fe0f.png","sheet_x":26,"sheet_y":25,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F487-1F3FE-200D-2642-FE0F","non_qualified":"1F487-1F3FE-200D-2642","image":"1f487-1f3fe-200d-2642-fe0f.png","sheet_x":26,"sheet_y":26,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F487-1F3FF-200D-2642-FE0F","non_qualified":"1F487-1F3FF-200D-2642","image":"1f487-1f3ff-200d-2642-fe0f.png","sheet_x":26,"sheet_y":27,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"HAIRCUT","unified":"1F487","non_qualified":null,"docomo":"E675","au":"EAA1","softbank":"E31F","google":"FE198","image":"1f487.png","sheet_x":26,"sheet_y":28,"short_name":"haircut","short_names":["haircut"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":398,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F487-1F3FB","non_qualified":null,"image":"1f487-1f3fb.png","sheet_x":26,"sheet_y":29,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F487-1F3FC","non_qualified":null,"image":"1f487-1f3fc.png","sheet_x":26,"sheet_y":30,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F487-1F3FD","non_qualified":null,"image":"1f487-1f3fd.png","sheet_x":26,"sheet_y":31,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F487-1F3FE","non_qualified":null,"image":"1f487-1f3fe.png","sheet_x":26,"sheet_y":32,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F487-1F3FF","non_qualified":null,"image":"1f487-1f3ff.png","sheet_x":26,"sheet_y":33,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F487-200D-2640-FE0F"},{"name":"BARBER POLE","unified":"1F488","non_qualified":null,"docomo":null,"au":"EAA2","softbank":"E320","google":"FE199","image":"1f488.png","sheet_x":26,"sheet_y":34,"short_name":"barber","short_names":["barber"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-other","sort_order":870,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SYRINGE","unified":"1F489","non_qualified":null,"docomo":null,"au":"E510","softbank":"E13B","google":"FE509","image":"1f489.png","sheet_x":26,"sheet_y":35,"short_name":"syringe","short_names":["syringe"],"text":null,"texts":null,"category":"Objects","subcategory":"medical","sort_order":1326,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PILL","unified":"1F48A","non_qualified":null,"docomo":null,"au":"EA9A","softbank":"E30F","google":"FE50A","image":"1f48a.png","sheet_x":26,"sheet_y":36,"short_name":"pill","short_names":["pill"],"text":null,"texts":null,"category":"Objects","subcategory":"medical","sort_order":1328,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"KISS MARK","unified":"1F48B","non_qualified":null,"docomo":"E6F9","au":"E4EB","softbank":"E003","google":"FE823","image":"1f48b.png","sheet_x":26,"sheet_y":37,"short_name":"kiss","short_names":["kiss"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":127,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LOVE LETTER","unified":"1F48C","non_qualified":null,"docomo":"E717","au":"EB78","softbank":null,"google":"FE824","image":"1f48c.png","sheet_x":26,"sheet_y":38,"short_name":"love_letter","short_names":["love_letter"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":128,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RING","unified":"1F48D","non_qualified":null,"docomo":"E71B","au":"E514","softbank":"E034","google":"FE825","image":"1f48d.png","sheet_x":26,"sheet_y":39,"short_name":"ring","short_names":["ring"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1153,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GEM STONE","unified":"1F48E","non_qualified":null,"docomo":"E71B","au":"E514","softbank":"E035","google":"FE826","image":"1f48e.png","sheet_x":26,"sheet_y":40,"short_name":"gem","short_names":["gem"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1154,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"KISS","unified":"1F48F","non_qualified":null,"docomo":"E6F9","au":"E5CA","softbank":"E111","google":"FE827","image":"1f48f.png","sheet_x":26,"sheet_y":41,"short_name":"couplekiss","short_names":["couplekiss"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":486,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F48F-1F3FB","non_qualified":null,"image":"1f48f-1f3fb.png","sheet_x":26,"sheet_y":42,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F48F-1F3FC","non_qualified":null,"image":"1f48f-1f3fc.png","sheet_x":26,"sheet_y":43,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F48F-1F3FD","non_qualified":null,"image":"1f48f-1f3fd.png","sheet_x":26,"sheet_y":44,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F48F-1F3FE","non_qualified":null,"image":"1f48f-1f3fe.png","sheet_x":26,"sheet_y":45,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F48F-1F3FF","non_qualified":null,"image":"1f48f-1f3ff.png","sheet_x":26,"sheet_y":46,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FC":{"unified":"1F9D1-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FC","non_qualified":"1F9D1-1F3FB-200D-2764-200D-1F48B-200D-1F9D1-1F3FC","image":"1f9d1-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fc.png","sheet_x":26,"sheet_y":47,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FB-1F3FD":{"unified":"1F9D1-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FD","non_qualified":"1F9D1-1F3FB-200D-2764-200D-1F48B-200D-1F9D1-1F3FD","image":"1f9d1-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fd.png","sheet_x":26,"sheet_y":48,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FB-1F3FE":{"unified":"1F9D1-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FE","non_qualified":"1F9D1-1F3FB-200D-2764-200D-1F48B-200D-1F9D1-1F3FE","image":"1f9d1-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fe.png","sheet_x":26,"sheet_y":49,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FB-1F3FF":{"unified":"1F9D1-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FF","non_qualified":"1F9D1-1F3FB-200D-2764-200D-1F48B-200D-1F9D1-1F3FF","image":"1f9d1-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3ff.png","sheet_x":26,"sheet_y":50,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FB":{"unified":"1F9D1-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FB","non_qualified":"1F9D1-1F3FC-200D-2764-200D-1F48B-200D-1F9D1-1F3FB","image":"1f9d1-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fb.png","sheet_x":26,"sheet_y":51,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FD":{"unified":"1F9D1-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FD","non_qualified":"1F9D1-1F3FC-200D-2764-200D-1F48B-200D-1F9D1-1F3FD","image":"1f9d1-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fd.png","sheet_x":26,"sheet_y":52,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FE":{"unified":"1F9D1-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FE","non_qualified":"1F9D1-1F3FC-200D-2764-200D-1F48B-200D-1F9D1-1F3FE","image":"1f9d1-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fe.png","sheet_x":26,"sheet_y":53,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FF":{"unified":"1F9D1-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FF","non_qualified":"1F9D1-1F3FC-200D-2764-200D-1F48B-200D-1F9D1-1F3FF","image":"1f9d1-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3ff.png","sheet_x":26,"sheet_y":54,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FB":{"unified":"1F9D1-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FB","non_qualified":"1F9D1-1F3FD-200D-2764-200D-1F48B-200D-1F9D1-1F3FB","image":"1f9d1-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fb.png","sheet_x":26,"sheet_y":55,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FC":{"unified":"1F9D1-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FC","non_qualified":"1F9D1-1F3FD-200D-2764-200D-1F48B-200D-1F9D1-1F3FC","image":"1f9d1-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fc.png","sheet_x":26,"sheet_y":56,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FE":{"unified":"1F9D1-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FE","non_qualified":"1F9D1-1F3FD-200D-2764-200D-1F48B-200D-1F9D1-1F3FE","image":"1f9d1-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fe.png","sheet_x":26,"sheet_y":57,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FF":{"unified":"1F9D1-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FF","non_qualified":"1F9D1-1F3FD-200D-2764-200D-1F48B-200D-1F9D1-1F3FF","image":"1f9d1-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3ff.png","sheet_x":26,"sheet_y":58,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE-1F3FB":{"unified":"1F9D1-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FB","non_qualified":"1F9D1-1F3FE-200D-2764-200D-1F48B-200D-1F9D1-1F3FB","image":"1f9d1-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fb.png","sheet_x":26,"sheet_y":59,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE-1F3FC":{"unified":"1F9D1-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FC","non_qualified":"1F9D1-1F3FE-200D-2764-200D-1F48B-200D-1F9D1-1F3FC","image":"1f9d1-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fc.png","sheet_x":26,"sheet_y":60,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE-1F3FD":{"unified":"1F9D1-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FD","non_qualified":"1F9D1-1F3FE-200D-2764-200D-1F48B-200D-1F9D1-1F3FD","image":"1f9d1-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fd.png","sheet_x":27,"sheet_y":0,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE-1F3FF":{"unified":"1F9D1-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FF","non_qualified":"1F9D1-1F3FE-200D-2764-200D-1F48B-200D-1F9D1-1F3FF","image":"1f9d1-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3ff.png","sheet_x":27,"sheet_y":1,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF-1F3FB":{"unified":"1F9D1-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FB","non_qualified":"1F9D1-1F3FF-200D-2764-200D-1F48B-200D-1F9D1-1F3FB","image":"1f9d1-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fb.png","sheet_x":27,"sheet_y":2,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF-1F3FC":{"unified":"1F9D1-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FC","non_qualified":"1F9D1-1F3FF-200D-2764-200D-1F48B-200D-1F9D1-1F3FC","image":"1f9d1-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fc.png","sheet_x":27,"sheet_y":3,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF-1F3FD":{"unified":"1F9D1-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FD","non_qualified":"1F9D1-1F3FF-200D-2764-200D-1F48B-200D-1F9D1-1F3FD","image":"1f9d1-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fd.png","sheet_x":27,"sheet_y":4,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF-1F3FE":{"unified":"1F9D1-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FE","non_qualified":"1F9D1-1F3FF-200D-2764-200D-1F48B-200D-1F9D1-1F3FE","image":"1f9d1-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fe.png","sheet_x":27,"sheet_y":5,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false}}},{"name":"BOUQUET","unified":"1F490","non_qualified":null,"docomo":null,"au":"EA95","softbank":"E306","google":"FE828","image":"1f490.png","sheet_x":27,"sheet_y":6,"short_name":"bouquet","short_names":["bouquet"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-flower","sort_order":648,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"COUPLE WITH HEART","unified":"1F491","non_qualified":null,"docomo":"E6ED","au":"EADA","softbank":"E425","google":"FE829","image":"1f491.png","sheet_x":27,"sheet_y":7,"short_name":"couple_with_heart","short_names":["couple_with_heart"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":490,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F491-1F3FB","non_qualified":null,"image":"1f491-1f3fb.png","sheet_x":27,"sheet_y":8,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F491-1F3FC","non_qualified":null,"image":"1f491-1f3fc.png","sheet_x":27,"sheet_y":9,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F491-1F3FD","non_qualified":null,"image":"1f491-1f3fd.png","sheet_x":27,"sheet_y":10,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F491-1F3FE","non_qualified":null,"image":"1f491-1f3fe.png","sheet_x":27,"sheet_y":11,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F491-1F3FF","non_qualified":null,"image":"1f491-1f3ff.png","sheet_x":27,"sheet_y":12,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FC":{"unified":"1F9D1-1F3FB-200D-2764-FE0F-200D-1F9D1-1F3FC","non_qualified":"1F9D1-1F3FB-200D-2764-200D-1F9D1-1F3FC","image":"1f9d1-1f3fb-200d-2764-fe0f-200d-1f9d1-1f3fc.png","sheet_x":27,"sheet_y":13,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FB-1F3FD":{"unified":"1F9D1-1F3FB-200D-2764-FE0F-200D-1F9D1-1F3FD","non_qualified":"1F9D1-1F3FB-200D-2764-200D-1F9D1-1F3FD","image":"1f9d1-1f3fb-200d-2764-fe0f-200d-1f9d1-1f3fd.png","sheet_x":27,"sheet_y":14,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FB-1F3FE":{"unified":"1F9D1-1F3FB-200D-2764-FE0F-200D-1F9D1-1F3FE","non_qualified":"1F9D1-1F3FB-200D-2764-200D-1F9D1-1F3FE","image":"1f9d1-1f3fb-200d-2764-fe0f-200d-1f9d1-1f3fe.png","sheet_x":27,"sheet_y":15,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FB-1F3FF":{"unified":"1F9D1-1F3FB-200D-2764-FE0F-200D-1F9D1-1F3FF","non_qualified":"1F9D1-1F3FB-200D-2764-200D-1F9D1-1F3FF","image":"1f9d1-1f3fb-200d-2764-fe0f-200d-1f9d1-1f3ff.png","sheet_x":27,"sheet_y":16,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FB":{"unified":"1F9D1-1F3FC-200D-2764-FE0F-200D-1F9D1-1F3FB","non_qualified":"1F9D1-1F3FC-200D-2764-200D-1F9D1-1F3FB","image":"1f9d1-1f3fc-200d-2764-fe0f-200d-1f9d1-1f3fb.png","sheet_x":27,"sheet_y":17,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FD":{"unified":"1F9D1-1F3FC-200D-2764-FE0F-200D-1F9D1-1F3FD","non_qualified":"1F9D1-1F3FC-200D-2764-200D-1F9D1-1F3FD","image":"1f9d1-1f3fc-200d-2764-fe0f-200d-1f9d1-1f3fd.png","sheet_x":27,"sheet_y":18,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FE":{"unified":"1F9D1-1F3FC-200D-2764-FE0F-200D-1F9D1-1F3FE","non_qualified":"1F9D1-1F3FC-200D-2764-200D-1F9D1-1F3FE","image":"1f9d1-1f3fc-200d-2764-fe0f-200d-1f9d1-1f3fe.png","sheet_x":27,"sheet_y":19,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FF":{"unified":"1F9D1-1F3FC-200D-2764-FE0F-200D-1F9D1-1F3FF","non_qualified":"1F9D1-1F3FC-200D-2764-200D-1F9D1-1F3FF","image":"1f9d1-1f3fc-200d-2764-fe0f-200d-1f9d1-1f3ff.png","sheet_x":27,"sheet_y":20,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FB":{"unified":"1F9D1-1F3FD-200D-2764-FE0F-200D-1F9D1-1F3FB","non_qualified":"1F9D1-1F3FD-200D-2764-200D-1F9D1-1F3FB","image":"1f9d1-1f3fd-200d-2764-fe0f-200d-1f9d1-1f3fb.png","sheet_x":27,"sheet_y":21,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FC":{"unified":"1F9D1-1F3FD-200D-2764-FE0F-200D-1F9D1-1F3FC","non_qualified":"1F9D1-1F3FD-200D-2764-200D-1F9D1-1F3FC","image":"1f9d1-1f3fd-200d-2764-fe0f-200d-1f9d1-1f3fc.png","sheet_x":27,"sheet_y":22,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FE":{"unified":"1F9D1-1F3FD-200D-2764-FE0F-200D-1F9D1-1F3FE","non_qualified":"1F9D1-1F3FD-200D-2764-200D-1F9D1-1F3FE","image":"1f9d1-1f3fd-200d-2764-fe0f-200d-1f9d1-1f3fe.png","sheet_x":27,"sheet_y":23,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FF":{"unified":"1F9D1-1F3FD-200D-2764-FE0F-200D-1F9D1-1F3FF","non_qualified":"1F9D1-1F3FD-200D-2764-200D-1F9D1-1F3FF","image":"1f9d1-1f3fd-200d-2764-fe0f-200d-1f9d1-1f3ff.png","sheet_x":27,"sheet_y":24,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE-1F3FB":{"unified":"1F9D1-1F3FE-200D-2764-FE0F-200D-1F9D1-1F3FB","non_qualified":"1F9D1-1F3FE-200D-2764-200D-1F9D1-1F3FB","image":"1f9d1-1f3fe-200d-2764-fe0f-200d-1f9d1-1f3fb.png","sheet_x":27,"sheet_y":25,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE-1F3FC":{"unified":"1F9D1-1F3FE-200D-2764-FE0F-200D-1F9D1-1F3FC","non_qualified":"1F9D1-1F3FE-200D-2764-200D-1F9D1-1F3FC","image":"1f9d1-1f3fe-200d-2764-fe0f-200d-1f9d1-1f3fc.png","sheet_x":27,"sheet_y":26,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE-1F3FD":{"unified":"1F9D1-1F3FE-200D-2764-FE0F-200D-1F9D1-1F3FD","non_qualified":"1F9D1-1F3FE-200D-2764-200D-1F9D1-1F3FD","image":"1f9d1-1f3fe-200d-2764-fe0f-200d-1f9d1-1f3fd.png","sheet_x":27,"sheet_y":27,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE-1F3FF":{"unified":"1F9D1-1F3FE-200D-2764-FE0F-200D-1F9D1-1F3FF","non_qualified":"1F9D1-1F3FE-200D-2764-200D-1F9D1-1F3FF","image":"1f9d1-1f3fe-200d-2764-fe0f-200d-1f9d1-1f3ff.png","sheet_x":27,"sheet_y":28,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF-1F3FB":{"unified":"1F9D1-1F3FF-200D-2764-FE0F-200D-1F9D1-1F3FB","non_qualified":"1F9D1-1F3FF-200D-2764-200D-1F9D1-1F3FB","image":"1f9d1-1f3ff-200d-2764-fe0f-200d-1f9d1-1f3fb.png","sheet_x":27,"sheet_y":29,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF-1F3FC":{"unified":"1F9D1-1F3FF-200D-2764-FE0F-200D-1F9D1-1F3FC","non_qualified":"1F9D1-1F3FF-200D-2764-200D-1F9D1-1F3FC","image":"1f9d1-1f3ff-200d-2764-fe0f-200d-1f9d1-1f3fc.png","sheet_x":27,"sheet_y":30,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF-1F3FD":{"unified":"1F9D1-1F3FF-200D-2764-FE0F-200D-1F9D1-1F3FD","non_qualified":"1F9D1-1F3FF-200D-2764-200D-1F9D1-1F3FD","image":"1f9d1-1f3ff-200d-2764-fe0f-200d-1f9d1-1f3fd.png","sheet_x":27,"sheet_y":31,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF-1F3FE":{"unified":"1F9D1-1F3FF-200D-2764-FE0F-200D-1F9D1-1F3FE","non_qualified":"1F9D1-1F3FF-200D-2764-200D-1F9D1-1F3FE","image":"1f9d1-1f3ff-200d-2764-fe0f-200d-1f9d1-1f3fe.png","sheet_x":27,"sheet_y":32,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false}}},{"name":"WEDDING","unified":"1F492","non_qualified":null,"docomo":null,"au":"E5BB","softbank":"E43D","google":"FE82A","image":"1f492.png","sheet_x":27,"sheet_y":33,"short_name":"wedding","short_names":["wedding"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":846,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BEATING HEART","unified":"1F493","non_qualified":null,"docomo":"E6ED","au":"EB75","softbank":"E327","google":"FEB0D","image":"1f493.png","sheet_x":27,"sheet_y":34,"short_name":"heartbeat","short_names":["heartbeat"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":133,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BROKEN HEART","unified":"1F494","non_qualified":null,"docomo":"E6EE","au":"E477","softbank":"E023","google":"FEB0E","image":"1f494.png","sheet_x":27,"sheet_y":35,"short_name":"broken_heart","short_names":["broken_heart"],"text":"<\/3","texts":["<\/3"],"category":"Smileys & Emotion","subcategory":"emotion","sort_order":138,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TWO HEARTS","unified":"1F495","non_qualified":null,"docomo":"E6EF","au":"E478","softbank":null,"google":"FEB0F","image":"1f495.png","sheet_x":27,"sheet_y":36,"short_name":"two_hearts","short_names":["two_hearts"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":135,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SPARKLING HEART","unified":"1F496","non_qualified":null,"docomo":"E6EC","au":"EAA6","softbank":null,"google":"FEB10","image":"1f496.png","sheet_x":27,"sheet_y":37,"short_name":"sparkling_heart","short_names":["sparkling_heart"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":131,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GROWING HEART","unified":"1F497","non_qualified":null,"docomo":"E6ED","au":"EB75","softbank":"E328","google":"FEB11","image":"1f497.png","sheet_x":27,"sheet_y":38,"short_name":"heartpulse","short_names":["heartpulse"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":132,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HEART WITH ARROW","unified":"1F498","non_qualified":null,"docomo":"E6EC","au":"E4EA","softbank":"E329","google":"FEB12","image":"1f498.png","sheet_x":27,"sheet_y":39,"short_name":"cupid","short_names":["cupid"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":129,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLUE HEART","unified":"1F499","non_qualified":null,"docomo":"E6EC","au":"EAA7","softbank":"E32A","google":"FEB13","image":"1f499.png","sheet_x":27,"sheet_y":40,"short_name":"blue_heart","short_names":["blue_heart"],"text":"<3","texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":145,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GREEN HEART","unified":"1F49A","non_qualified":null,"docomo":"E6EC","au":"EAA8","softbank":"E32B","google":"FEB14","image":"1f49a.png","sheet_x":27,"sheet_y":41,"short_name":"green_heart","short_names":["green_heart"],"text":"<3","texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":144,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"YELLOW HEART","unified":"1F49B","non_qualified":null,"docomo":"E6EC","au":"EAA9","softbank":"E32C","google":"FEB15","image":"1f49b.png","sheet_x":27,"sheet_y":42,"short_name":"yellow_heart","short_names":["yellow_heart"],"text":"<3","texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":143,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PURPLE HEART","unified":"1F49C","non_qualified":null,"docomo":"E6EC","au":"EAAA","softbank":"E32D","google":"FEB16","image":"1f49c.png","sheet_x":27,"sheet_y":43,"short_name":"purple_heart","short_names":["purple_heart"],"text":"<3","texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":146,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HEART WITH RIBBON","unified":"1F49D","non_qualified":null,"docomo":"E6EC","au":"EB54","softbank":"E437","google":"FEB17","image":"1f49d.png","sheet_x":27,"sheet_y":44,"short_name":"gift_heart","short_names":["gift_heart"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":130,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"REVOLVING HEARTS","unified":"1F49E","non_qualified":null,"docomo":"E6ED","au":"E5AF","softbank":null,"google":"FEB18","image":"1f49e.png","sheet_x":27,"sheet_y":45,"short_name":"revolving_hearts","short_names":["revolving_hearts"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":134,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HEART DECORATION","unified":"1F49F","non_qualified":null,"docomo":"E6F8","au":"E595","softbank":"E204","google":"FEB19","image":"1f49f.png","sheet_x":27,"sheet_y":46,"short_name":"heart_decoration","short_names":["heart_decoration"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":136,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DIAMOND SHAPE WITH A DOT INSIDE","unified":"1F4A0","non_qualified":null,"docomo":"E6F8","au":null,"softbank":null,"google":"FEB55","image":"1f4a0.png","sheet_x":27,"sheet_y":47,"short_name":"diamond_shape_with_a_dot_inside","short_names":["diamond_shape_with_a_dot_inside"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1582,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ELECTRIC LIGHT BULB","unified":"1F4A1","non_qualified":null,"docomo":"E6FB","au":"E476","softbank":"E10F","google":"FEB56","image":"1f4a1.png","sheet_x":27,"sheet_y":48,"short_name":"bulb","short_names":["bulb"],"text":null,"texts":null,"category":"Objects","subcategory":"light & video","sort_order":1214,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ANGER SYMBOL","unified":"1F4A2","non_qualified":null,"docomo":"E6FC","au":"E4E5","softbank":"E334","google":"FEB57","image":"1f4a2.png","sheet_x":27,"sheet_y":49,"short_name":"anger","short_names":["anger"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":151,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BOMB","unified":"1F4A3","non_qualified":null,"docomo":"E6FE","au":"E47A","softbank":"E311","google":"FEB58","image":"1f4a3.png","sheet_x":27,"sheet_y":50,"short_name":"bomb","short_names":["bomb"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":157,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SLEEPING SYMBOL","unified":"1F4A4","non_qualified":null,"docomo":"E701","au":"E475","softbank":"E13C","google":"FEB59","image":"1f4a4.png","sheet_x":27,"sheet_y":51,"short_name":"zzz","short_names":["zzz"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":163,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"COLLISION SYMBOL","unified":"1F4A5","non_qualified":null,"docomo":"E705","au":"E5B0","softbank":null,"google":"FEB5A","image":"1f4a5.png","sheet_x":27,"sheet_y":52,"short_name":"boom","short_names":["boom","collision"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":152,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SPLASHING SWEAT SYMBOL","unified":"1F4A6","non_qualified":null,"docomo":"E706","au":"E5B1","softbank":"E331","google":"FEB5B","image":"1f4a6.png","sheet_x":27,"sheet_y":53,"short_name":"sweat_drops","short_names":["sweat_drops"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":154,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DROPLET","unified":"1F4A7","non_qualified":null,"docomo":"E707","au":"E4E6","softbank":null,"google":"FEB5C","image":"1f4a7.png","sheet_x":27,"sheet_y":54,"short_name":"droplet","short_names":["droplet"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1022,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DASH SYMBOL","unified":"1F4A8","non_qualified":null,"docomo":"E708","au":"E4F4","softbank":"E330","google":"FEB5D","image":"1f4a8.png","sheet_x":27,"sheet_y":55,"short_name":"dash","short_names":["dash"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":155,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PILE OF POO","unified":"1F4A9","non_qualified":null,"docomo":null,"au":"E4F5","softbank":"E05A","google":"FE4F4","image":"1f4a9.png","sheet_x":27,"sheet_y":56,"short_name":"hankey","short_names":["hankey","poop","shit"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-costume","sort_order":107,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FLEXED BICEPS","unified":"1F4AA","non_qualified":null,"docomo":null,"au":"E4E9","softbank":"E14C","google":"FEB5E","image":"1f4aa.png","sheet_x":27,"sheet_y":57,"short_name":"muscle","short_names":["muscle"],"text":null,"texts":null,"category":"People & Body","subcategory":"body-parts","sort_order":205,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F4AA-1F3FB","non_qualified":null,"image":"1f4aa-1f3fb.png","sheet_x":27,"sheet_y":58,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F4AA-1F3FC","non_qualified":null,"image":"1f4aa-1f3fc.png","sheet_x":27,"sheet_y":59,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F4AA-1F3FD","non_qualified":null,"image":"1f4aa-1f3fd.png","sheet_x":27,"sheet_y":60,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F4AA-1F3FE","non_qualified":null,"image":"1f4aa-1f3fe.png","sheet_x":28,"sheet_y":0,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F4AA-1F3FF","non_qualified":null,"image":"1f4aa-1f3ff.png","sheet_x":28,"sheet_y":1,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"DIZZY SYMBOL","unified":"1F4AB","non_qualified":null,"docomo":null,"au":"EB5C","softbank":null,"google":"FEB5F","image":"1f4ab.png","sheet_x":28,"sheet_y":2,"short_name":"dizzy","short_names":["dizzy"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":153,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SPEECH BALLOON","unified":"1F4AC","non_qualified":null,"docomo":null,"au":"E4FD","softbank":null,"google":"FE532","image":"1f4ac.png","sheet_x":28,"sheet_y":3,"short_name":"speech_balloon","short_names":["speech_balloon"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":158,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"THOUGHT BALLOON","unified":"1F4AD","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f4ad.png","sheet_x":28,"sheet_y":4,"short_name":"thought_balloon","short_names":["thought_balloon"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":162,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WHITE FLOWER","unified":"1F4AE","non_qualified":null,"docomo":null,"au":"E4F0","softbank":null,"google":"FEB7A","image":"1f4ae.png","sheet_x":28,"sheet_y":5,"short_name":"white_flower","short_names":["white_flower"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-flower","sort_order":650,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HUNDRED POINTS SYMBOL","unified":"1F4AF","non_qualified":null,"docomo":null,"au":"E4F2","softbank":null,"google":"FEB7B","image":"1f4af.png","sheet_x":28,"sheet_y":6,"short_name":"100","short_names":["100"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":150,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MONEY BAG","unified":"1F4B0","non_qualified":null,"docomo":"E715","au":"E4C7","softbank":"E12F","google":"FE4DD","image":"1f4b0.png","sheet_x":28,"sheet_y":7,"short_name":"moneybag","short_names":["moneybag"],"text":null,"texts":null,"category":"Objects","subcategory":"money","sort_order":1235,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CURRENCY EXCHANGE","unified":"1F4B1","non_qualified":null,"docomo":null,"au":null,"softbank":"E149","google":"FE4DE","image":"1f4b1.png","sheet_x":28,"sheet_y":8,"short_name":"currency_exchange","short_names":["currency_exchange"],"text":null,"texts":null,"category":"Symbols","subcategory":"currency","sort_order":1477,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HEAVY DOLLAR SIGN","unified":"1F4B2","non_qualified":null,"docomo":"E715","au":"E579","softbank":null,"google":"FE4E0","image":"1f4b2.png","sheet_x":28,"sheet_y":9,"short_name":"heavy_dollar_sign","short_names":["heavy_dollar_sign"],"text":null,"texts":null,"category":"Symbols","subcategory":"currency","sort_order":1478,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CREDIT CARD","unified":"1F4B3","non_qualified":null,"docomo":null,"au":"E57C","softbank":null,"google":"FE4E1","image":"1f4b3.png","sheet_x":28,"sheet_y":10,"short_name":"credit_card","short_names":["credit_card"],"text":null,"texts":null,"category":"Objects","subcategory":"money","sort_order":1242,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BANKNOTE WITH YEN SIGN","unified":"1F4B4","non_qualified":null,"docomo":"E6D6","au":"E57D","softbank":null,"google":"FE4E2","image":"1f4b4.png","sheet_x":28,"sheet_y":11,"short_name":"yen","short_names":["yen"],"text":null,"texts":null,"category":"Objects","subcategory":"money","sort_order":1237,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BANKNOTE WITH DOLLAR SIGN","unified":"1F4B5","non_qualified":null,"docomo":"E715","au":"E585","softbank":null,"google":"FE4E3","image":"1f4b5.png","sheet_x":28,"sheet_y":12,"short_name":"dollar","short_names":["dollar"],"text":null,"texts":null,"category":"Objects","subcategory":"money","sort_order":1238,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BANKNOTE WITH EURO SIGN","unified":"1F4B6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f4b6.png","sheet_x":28,"sheet_y":13,"short_name":"euro","short_names":["euro"],"text":null,"texts":null,"category":"Objects","subcategory":"money","sort_order":1239,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BANKNOTE WITH POUND SIGN","unified":"1F4B7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f4b7.png","sheet_x":28,"sheet_y":14,"short_name":"pound","short_names":["pound"],"text":null,"texts":null,"category":"Objects","subcategory":"money","sort_order":1240,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MONEY WITH WINGS","unified":"1F4B8","non_qualified":null,"docomo":null,"au":"EB5B","softbank":null,"google":"FE4E4","image":"1f4b8.png","sheet_x":28,"sheet_y":15,"short_name":"money_with_wings","short_names":["money_with_wings"],"text":null,"texts":null,"category":"Objects","subcategory":"money","sort_order":1241,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CHART WITH UPWARDS TREND AND YEN SIGN","unified":"1F4B9","non_qualified":null,"docomo":null,"au":"E5DC","softbank":"E14A","google":"FE4DF","image":"1f4b9.png","sheet_x":28,"sheet_y":16,"short_name":"chart","short_names":["chart"],"text":null,"texts":null,"category":"Objects","subcategory":"money","sort_order":1244,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SEAT","unified":"1F4BA","non_qualified":null,"docomo":"E6B2","au":null,"softbank":"E11F","google":"FE537","image":"1f4ba.png","sheet_x":28,"sheet_y":17,"short_name":"seat","short_names":["seat"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-air","sort_order":936,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PERSONAL COMPUTER","unified":"1F4BB","non_qualified":null,"docomo":"E716","au":"E5B8","softbank":"E00C","google":"FE538","image":"1f4bb.png","sheet_x":28,"sheet_y":18,"short_name":"computer","short_names":["computer"],"text":null,"texts":null,"category":"Objects","subcategory":"computer","sort_order":1191,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BRIEFCASE","unified":"1F4BC","non_qualified":null,"docomo":"E682","au":"E5CE","softbank":"E11E","google":"FE53B","image":"1f4bc.png","sheet_x":28,"sheet_y":19,"short_name":"briefcase","short_names":["briefcase"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1265,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MINIDISC","unified":"1F4BD","non_qualified":null,"docomo":null,"au":"E582","softbank":"E316","google":"FE53C","image":"1f4bd.png","sheet_x":28,"sheet_y":20,"short_name":"minidisc","short_names":["minidisc"],"text":null,"texts":null,"category":"Objects","subcategory":"computer","sort_order":1197,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FLOPPY DISK","unified":"1F4BE","non_qualified":null,"docomo":null,"au":"E562","softbank":null,"google":"FE53D","image":"1f4be.png","sheet_x":28,"sheet_y":21,"short_name":"floppy_disk","short_names":["floppy_disk"],"text":null,"texts":null,"category":"Objects","subcategory":"computer","sort_order":1198,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"OPTICAL DISC","unified":"1F4BF","non_qualified":null,"docomo":"E68C","au":"E50C","softbank":"E126","google":"FE81D","image":"1f4bf.png","sheet_x":28,"sheet_y":22,"short_name":"cd","short_names":["cd"],"text":null,"texts":null,"category":"Objects","subcategory":"computer","sort_order":1199,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DVD","unified":"1F4C0","non_qualified":null,"docomo":"E68C","au":"E50C","softbank":"E127","google":"FE81E","image":"1f4c0.png","sheet_x":28,"sheet_y":23,"short_name":"dvd","short_names":["dvd"],"text":null,"texts":null,"category":"Objects","subcategory":"computer","sort_order":1200,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FILE FOLDER","unified":"1F4C1","non_qualified":null,"docomo":null,"au":"E58F","softbank":null,"google":"FE543","image":"1f4c1.png","sheet_x":28,"sheet_y":24,"short_name":"file_folder","short_names":["file_folder"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1266,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"OPEN FILE FOLDER","unified":"1F4C2","non_qualified":null,"docomo":null,"au":"E590","softbank":null,"google":"FE544","image":"1f4c2.png","sheet_x":28,"sheet_y":25,"short_name":"open_file_folder","short_names":["open_file_folder"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1267,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PAGE WITH CURL","unified":"1F4C3","non_qualified":null,"docomo":"E689","au":"E561","softbank":null,"google":"FE540","image":"1f4c3.png","sheet_x":28,"sheet_y":26,"short_name":"page_with_curl","short_names":["page_with_curl"],"text":null,"texts":null,"category":"Objects","subcategory":"book-paper","sort_order":1227,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PAGE FACING UP","unified":"1F4C4","non_qualified":null,"docomo":"E689","au":"E569","softbank":null,"google":"FE541","image":"1f4c4.png","sheet_x":28,"sheet_y":27,"short_name":"page_facing_up","short_names":["page_facing_up"],"text":null,"texts":null,"category":"Objects","subcategory":"book-paper","sort_order":1229,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CALENDAR","unified":"1F4C5","non_qualified":null,"docomo":null,"au":"E563","softbank":null,"google":"FE542","image":"1f4c5.png","sheet_x":28,"sheet_y":28,"short_name":"date","short_names":["date"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1269,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TEAR-OFF CALENDAR","unified":"1F4C6","non_qualified":null,"docomo":null,"au":"E56A","softbank":null,"google":"FE549","image":"1f4c6.png","sheet_x":28,"sheet_y":29,"short_name":"calendar","short_names":["calendar"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1270,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CARD INDEX","unified":"1F4C7","non_qualified":null,"docomo":"E683","au":"E56C","softbank":null,"google":"FE54D","image":"1f4c7.png","sheet_x":28,"sheet_y":30,"short_name":"card_index","short_names":["card_index"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1273,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CHART WITH UPWARDS TREND","unified":"1F4C8","non_qualified":null,"docomo":null,"au":"E575","softbank":null,"google":"FE54B","image":"1f4c8.png","sheet_x":28,"sheet_y":31,"short_name":"chart_with_upwards_trend","short_names":["chart_with_upwards_trend"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1274,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CHART WITH DOWNWARDS TREND","unified":"1F4C9","non_qualified":null,"docomo":null,"au":"E576","softbank":null,"google":"FE54C","image":"1f4c9.png","sheet_x":28,"sheet_y":32,"short_name":"chart_with_downwards_trend","short_names":["chart_with_downwards_trend"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1275,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BAR CHART","unified":"1F4CA","non_qualified":null,"docomo":null,"au":"E574","softbank":null,"google":"FE54A","image":"1f4ca.png","sheet_x":28,"sheet_y":33,"short_name":"bar_chart","short_names":["bar_chart"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1276,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLIPBOARD","unified":"1F4CB","non_qualified":null,"docomo":"E689","au":"E564","softbank":null,"google":"FE548","image":"1f4cb.png","sheet_x":28,"sheet_y":34,"short_name":"clipboard","short_names":["clipboard"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1277,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PUSHPIN","unified":"1F4CC","non_qualified":null,"docomo":null,"au":"E56D","softbank":null,"google":"FE54E","image":"1f4cc.png","sheet_x":28,"sheet_y":35,"short_name":"pushpin","short_names":["pushpin"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1278,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ROUND PUSHPIN","unified":"1F4CD","non_qualified":null,"docomo":null,"au":"E560","softbank":null,"google":"FE53F","image":"1f4cd.png","sheet_x":28,"sheet_y":36,"short_name":"round_pushpin","short_names":["round_pushpin"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1279,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PAPERCLIP","unified":"1F4CE","non_qualified":null,"docomo":"E730","au":"E4A0","softbank":null,"google":"FE53A","image":"1f4ce.png","sheet_x":28,"sheet_y":37,"short_name":"paperclip","short_names":["paperclip"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1280,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"STRAIGHT RULER","unified":"1F4CF","non_qualified":null,"docomo":null,"au":"E570","softbank":null,"google":"FE550","image":"1f4cf.png","sheet_x":28,"sheet_y":38,"short_name":"straight_ruler","short_names":["straight_ruler"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1282,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TRIANGULAR RULER","unified":"1F4D0","non_qualified":null,"docomo":null,"au":"E4A2","softbank":null,"google":"FE551","image":"1f4d0.png","sheet_x":28,"sheet_y":39,"short_name":"triangular_ruler","short_names":["triangular_ruler"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1283,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BOOKMARK TABS","unified":"1F4D1","non_qualified":null,"docomo":"E689","au":"EB0B","softbank":null,"google":"FE552","image":"1f4d1.png","sheet_x":28,"sheet_y":40,"short_name":"bookmark_tabs","short_names":["bookmark_tabs"],"text":null,"texts":null,"category":"Objects","subcategory":"book-paper","sort_order":1232,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LEDGER","unified":"1F4D2","non_qualified":null,"docomo":"E683","au":"E56E","softbank":null,"google":"FE54F","image":"1f4d2.png","sheet_x":28,"sheet_y":41,"short_name":"ledger","short_names":["ledger"],"text":null,"texts":null,"category":"Objects","subcategory":"book-paper","sort_order":1226,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NOTEBOOK","unified":"1F4D3","non_qualified":null,"docomo":"E683","au":"E56B","softbank":null,"google":"FE545","image":"1f4d3.png","sheet_x":28,"sheet_y":42,"short_name":"notebook","short_names":["notebook"],"text":null,"texts":null,"category":"Objects","subcategory":"book-paper","sort_order":1225,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NOTEBOOK WITH DECORATIVE COVER","unified":"1F4D4","non_qualified":null,"docomo":"E683","au":"E49D","softbank":null,"google":"FE547","image":"1f4d4.png","sheet_x":28,"sheet_y":43,"short_name":"notebook_with_decorative_cover","short_names":["notebook_with_decorative_cover"],"text":null,"texts":null,"category":"Objects","subcategory":"book-paper","sort_order":1218,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOSED BOOK","unified":"1F4D5","non_qualified":null,"docomo":"E683","au":"E568","softbank":null,"google":"FE502","image":"1f4d5.png","sheet_x":28,"sheet_y":44,"short_name":"closed_book","short_names":["closed_book"],"text":null,"texts":null,"category":"Objects","subcategory":"book-paper","sort_order":1219,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"OPEN BOOK","unified":"1F4D6","non_qualified":null,"docomo":"E683","au":"E49F","softbank":"E148","google":"FE546","image":"1f4d6.png","sheet_x":28,"sheet_y":45,"short_name":"book","short_names":["book","open_book"],"text":null,"texts":null,"category":"Objects","subcategory":"book-paper","sort_order":1220,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GREEN BOOK","unified":"1F4D7","non_qualified":null,"docomo":"E683","au":"E565","softbank":null,"google":"FE4FF","image":"1f4d7.png","sheet_x":28,"sheet_y":46,"short_name":"green_book","short_names":["green_book"],"text":null,"texts":null,"category":"Objects","subcategory":"book-paper","sort_order":1221,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLUE BOOK","unified":"1F4D8","non_qualified":null,"docomo":"E683","au":"E566","softbank":null,"google":"FE500","image":"1f4d8.png","sheet_x":28,"sheet_y":47,"short_name":"blue_book","short_names":["blue_book"],"text":null,"texts":null,"category":"Objects","subcategory":"book-paper","sort_order":1222,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ORANGE BOOK","unified":"1F4D9","non_qualified":null,"docomo":"E683","au":"E567","softbank":null,"google":"FE501","image":"1f4d9.png","sheet_x":28,"sheet_y":48,"short_name":"orange_book","short_names":["orange_book"],"text":null,"texts":null,"category":"Objects","subcategory":"book-paper","sort_order":1223,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BOOKS","unified":"1F4DA","non_qualified":null,"docomo":"E683","au":"E56F","softbank":null,"google":"FE503","image":"1f4da.png","sheet_x":28,"sheet_y":49,"short_name":"books","short_names":["books"],"text":null,"texts":null,"category":"Objects","subcategory":"book-paper","sort_order":1224,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NAME BADGE","unified":"1F4DB","non_qualified":null,"docomo":null,"au":"E51D","softbank":null,"google":"FE504","image":"1f4db.png","sheet_x":28,"sheet_y":50,"short_name":"name_badge","short_names":["name_badge"],"text":null,"texts":null,"category":"Symbols","subcategory":"other-symbol","sort_order":1483,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SCROLL","unified":"1F4DC","non_qualified":null,"docomo":"E70A","au":"E55F","softbank":null,"google":"FE4FD","image":"1f4dc.png","sheet_x":28,"sheet_y":51,"short_name":"scroll","short_names":["scroll"],"text":null,"texts":null,"category":"Objects","subcategory":"book-paper","sort_order":1228,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MEMO","unified":"1F4DD","non_qualified":null,"docomo":"E689","au":"EA92","softbank":"E301","google":"FE527","image":"1f4dd.png","sheet_x":28,"sheet_y":52,"short_name":"memo","short_names":["memo","pencil"],"text":null,"texts":null,"category":"Objects","subcategory":"writing","sort_order":1264,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TELEPHONE RECEIVER","unified":"1F4DE","non_qualified":null,"docomo":"E687","au":"E51E","softbank":null,"google":"FE524","image":"1f4de.png","sheet_x":28,"sheet_y":53,"short_name":"telephone_receiver","short_names":["telephone_receiver"],"text":null,"texts":null,"category":"Objects","subcategory":"phone","sort_order":1185,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PAGER","unified":"1F4DF","non_qualified":null,"docomo":"E65A","au":"E59B","softbank":null,"google":"FE522","image":"1f4df.png","sheet_x":28,"sheet_y":54,"short_name":"pager","short_names":["pager"],"text":null,"texts":null,"category":"Objects","subcategory":"phone","sort_order":1186,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAX MACHINE","unified":"1F4E0","non_qualified":null,"docomo":"E6D0","au":"E520","softbank":"E00B","google":"FE528","image":"1f4e0.png","sheet_x":28,"sheet_y":55,"short_name":"fax","short_names":["fax"],"text":null,"texts":null,"category":"Objects","subcategory":"phone","sort_order":1187,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SATELLITE ANTENNA","unified":"1F4E1","non_qualified":null,"docomo":null,"au":"E4A8","softbank":"E14B","google":"FE531","image":"1f4e1.png","sheet_x":28,"sheet_y":56,"short_name":"satellite_antenna","short_names":["satellite_antenna"],"text":null,"texts":null,"category":"Objects","subcategory":"science","sort_order":1325,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PUBLIC ADDRESS LOUDSPEAKER","unified":"1F4E2","non_qualified":null,"docomo":null,"au":"E511","softbank":"E142","google":"FE52F","image":"1f4e2.png","sheet_x":28,"sheet_y":57,"short_name":"loudspeaker","short_names":["loudspeaker"],"text":null,"texts":null,"category":"Objects","subcategory":"sound","sort_order":1159,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CHEERING MEGAPHONE","unified":"1F4E3","non_qualified":null,"docomo":null,"au":"E511","softbank":"E317","google":"FE530","image":"1f4e3.png","sheet_x":28,"sheet_y":58,"short_name":"mega","short_names":["mega"],"text":null,"texts":null,"category":"Objects","subcategory":"sound","sort_order":1160,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"OUTBOX TRAY","unified":"1F4E4","non_qualified":null,"docomo":null,"au":"E592","softbank":null,"google":"FE533","image":"1f4e4.png","sheet_x":28,"sheet_y":59,"short_name":"outbox_tray","short_names":["outbox_tray"],"text":null,"texts":null,"category":"Objects","subcategory":"mail","sort_order":1249,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"INBOX TRAY","unified":"1F4E5","non_qualified":null,"docomo":null,"au":"E593","softbank":null,"google":"FE534","image":"1f4e5.png","sheet_x":28,"sheet_y":60,"short_name":"inbox_tray","short_names":["inbox_tray"],"text":null,"texts":null,"category":"Objects","subcategory":"mail","sort_order":1250,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PACKAGE","unified":"1F4E6","non_qualified":null,"docomo":"E685","au":"E51F","softbank":null,"google":"FE535","image":"1f4e6.png","sheet_x":29,"sheet_y":0,"short_name":"package","short_names":["package"],"text":null,"texts":null,"category":"Objects","subcategory":"mail","sort_order":1251,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"E-MAIL SYMBOL","unified":"1F4E7","non_qualified":null,"docomo":"E6D3","au":"EB71","softbank":null,"google":"FEB92","image":"1f4e7.png","sheet_x":29,"sheet_y":1,"short_name":"e-mail","short_names":["e-mail"],"text":null,"texts":null,"category":"Objects","subcategory":"mail","sort_order":1246,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"INCOMING ENVELOPE","unified":"1F4E8","non_qualified":null,"docomo":"E6CF","au":"E591","softbank":null,"google":"FE52A","image":"1f4e8.png","sheet_x":29,"sheet_y":2,"short_name":"incoming_envelope","short_names":["incoming_envelope"],"text":null,"texts":null,"category":"Objects","subcategory":"mail","sort_order":1247,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ENVELOPE WITH DOWNWARDS ARROW ABOVE","unified":"1F4E9","non_qualified":null,"docomo":"E6CF","au":"EB62","softbank":"E103","google":"FE52B","image":"1f4e9.png","sheet_x":29,"sheet_y":3,"short_name":"envelope_with_arrow","short_names":["envelope_with_arrow"],"text":null,"texts":null,"category":"Objects","subcategory":"mail","sort_order":1248,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOSED MAILBOX WITH LOWERED FLAG","unified":"1F4EA","non_qualified":null,"docomo":"E665","au":"E51B","softbank":null,"google":"FE52C","image":"1f4ea.png","sheet_x":29,"sheet_y":4,"short_name":"mailbox_closed","short_names":["mailbox_closed"],"text":null,"texts":null,"category":"Objects","subcategory":"mail","sort_order":1253,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOSED MAILBOX WITH RAISED FLAG","unified":"1F4EB","non_qualified":null,"docomo":"E665","au":"EB0A","softbank":"E101","google":"FE52D","image":"1f4eb.png","sheet_x":29,"sheet_y":5,"short_name":"mailbox","short_names":["mailbox"],"text":null,"texts":null,"category":"Objects","subcategory":"mail","sort_order":1252,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"OPEN MAILBOX WITH RAISED FLAG","unified":"1F4EC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f4ec.png","sheet_x":29,"sheet_y":6,"short_name":"mailbox_with_mail","short_names":["mailbox_with_mail"],"text":null,"texts":null,"category":"Objects","subcategory":"mail","sort_order":1254,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"OPEN MAILBOX WITH LOWERED FLAG","unified":"1F4ED","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f4ed.png","sheet_x":29,"sheet_y":7,"short_name":"mailbox_with_no_mail","short_names":["mailbox_with_no_mail"],"text":null,"texts":null,"category":"Objects","subcategory":"mail","sort_order":1255,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"POSTBOX","unified":"1F4EE","non_qualified":null,"docomo":"E665","au":"E51B","softbank":"E102","google":"FE52E","image":"1f4ee.png","sheet_x":29,"sheet_y":8,"short_name":"postbox","short_names":["postbox"],"text":null,"texts":null,"category":"Objects","subcategory":"mail","sort_order":1256,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"POSTAL HORN","unified":"1F4EF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f4ef.png","sheet_x":29,"sheet_y":9,"short_name":"postal_horn","short_names":["postal_horn"],"text":null,"texts":null,"category":"Objects","subcategory":"sound","sort_order":1161,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NEWSPAPER","unified":"1F4F0","non_qualified":null,"docomo":null,"au":"E58B","softbank":null,"google":"FE822","image":"1f4f0.png","sheet_x":29,"sheet_y":10,"short_name":"newspaper","short_names":["newspaper"],"text":null,"texts":null,"category":"Objects","subcategory":"book-paper","sort_order":1230,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MOBILE PHONE","unified":"1F4F1","non_qualified":null,"docomo":"E688","au":"E588","softbank":"E00A","google":"FE525","image":"1f4f1.png","sheet_x":29,"sheet_y":11,"short_name":"iphone","short_names":["iphone"],"text":null,"texts":null,"category":"Objects","subcategory":"phone","sort_order":1182,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MOBILE PHONE WITH RIGHTWARDS ARROW AT LEFT","unified":"1F4F2","non_qualified":null,"docomo":"E6CE","au":"EB08","softbank":"E104","google":"FE526","image":"1f4f2.png","sheet_x":29,"sheet_y":12,"short_name":"calling","short_names":["calling"],"text":null,"texts":null,"category":"Objects","subcategory":"phone","sort_order":1183,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"VIBRATION MODE","unified":"1F4F3","non_qualified":null,"docomo":null,"au":"EA90","softbank":"E250","google":"FE839","image":"1f4f3.png","sheet_x":29,"sheet_y":13,"short_name":"vibration_mode","short_names":["vibration_mode"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1459,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MOBILE PHONE OFF","unified":"1F4F4","non_qualified":null,"docomo":null,"au":"EA91","softbank":"E251","google":"FE83A","image":"1f4f4.png","sheet_x":29,"sheet_y":14,"short_name":"mobile_phone_off","short_names":["mobile_phone_off"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1460,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NO MOBILE PHONES","unified":"1F4F5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f4f5.png","sheet_x":29,"sheet_y":15,"short_name":"no_mobile_phones","short_names":["no_mobile_phones"],"text":null,"texts":null,"category":"Symbols","subcategory":"warning","sort_order":1387,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ANTENNA WITH BARS","unified":"1F4F6","non_qualified":null,"docomo":null,"au":"EA84","softbank":"E20B","google":"FE838","image":"1f4f6.png","sheet_x":29,"sheet_y":16,"short_name":"signal_strength","short_names":["signal_strength"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1458,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CAMERA","unified":"1F4F7","non_qualified":null,"docomo":"E681","au":"E515","softbank":"E008","google":"FE4EF","image":"1f4f7.png","sheet_x":29,"sheet_y":17,"short_name":"camera","short_names":["camera"],"text":null,"texts":null,"category":"Objects","subcategory":"light & video","sort_order":1207,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CAMERA WITH FLASH","unified":"1F4F8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f4f8.png","sheet_x":29,"sheet_y":18,"short_name":"camera_with_flash","short_names":["camera_with_flash"],"text":null,"texts":null,"category":"Objects","subcategory":"light & video","sort_order":1208,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"VIDEO CAMERA","unified":"1F4F9","non_qualified":null,"docomo":"E677","au":"E57E","softbank":null,"google":"FE4F9","image":"1f4f9.png","sheet_x":29,"sheet_y":19,"short_name":"video_camera","short_names":["video_camera"],"text":null,"texts":null,"category":"Objects","subcategory":"light & video","sort_order":1209,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TELEVISION","unified":"1F4FA","non_qualified":null,"docomo":"E68A","au":"E502","softbank":"E12A","google":"FE81C","image":"1f4fa.png","sheet_x":29,"sheet_y":20,"short_name":"tv","short_names":["tv"],"text":null,"texts":null,"category":"Objects","subcategory":"light & video","sort_order":1206,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RADIO","unified":"1F4FB","non_qualified":null,"docomo":null,"au":"E5B9","softbank":"E128","google":"FE81F","image":"1f4fb.png","sheet_x":29,"sheet_y":21,"short_name":"radio","short_names":["radio"],"text":null,"texts":null,"category":"Objects","subcategory":"music","sort_order":1172,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"VIDEOCASSETTE","unified":"1F4FC","non_qualified":null,"docomo":null,"au":"E580","softbank":"E129","google":"FE820","image":"1f4fc.png","sheet_x":29,"sheet_y":22,"short_name":"vhs","short_names":["vhs"],"text":null,"texts":null,"category":"Objects","subcategory":"light & video","sort_order":1210,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FILM PROJECTOR","unified":"1F4FD-FE0F","non_qualified":"1F4FD","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f4fd-fe0f.png","sheet_x":29,"sheet_y":23,"short_name":"film_projector","short_names":["film_projector"],"text":null,"texts":null,"category":"Objects","subcategory":"light & video","sort_order":1204,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PRAYER BEADS","unified":"1F4FF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f4ff.png","sheet_x":29,"sheet_y":24,"short_name":"prayer_beads","short_names":["prayer_beads"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1151,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TWISTED RIGHTWARDS ARROWS","unified":"1F500","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f500.png","sheet_x":29,"sheet_y":25,"short_name":"twisted_rightwards_arrows","short_names":["twisted_rightwards_arrows"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1437,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCKWISE RIGHTWARDS AND LEFTWARDS OPEN CIRCLE ARROWS","unified":"1F501","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f501.png","sheet_x":29,"sheet_y":26,"short_name":"repeat","short_names":["repeat"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1438,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCKWISE RIGHTWARDS AND LEFTWARDS OPEN CIRCLE ARROWS WITH CIRCLED ONE OVERLAY","unified":"1F502","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f502.png","sheet_x":29,"sheet_y":27,"short_name":"repeat_one","short_names":["repeat_one"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1439,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCKWISE DOWNWARDS AND UPWARDS OPEN CIRCLE ARROWS","unified":"1F503","non_qualified":null,"docomo":"E735","au":"EB0D","softbank":null,"google":"FEB91","image":"1f503.png","sheet_x":29,"sheet_y":28,"short_name":"arrows_clockwise","short_names":["arrows_clockwise"],"text":null,"texts":null,"category":"Symbols","subcategory":"arrow","sort_order":1405,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ANTICLOCKWISE DOWNWARDS AND UPWARDS OPEN CIRCLE ARROWS","unified":"1F504","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f504.png","sheet_x":29,"sheet_y":29,"short_name":"arrows_counterclockwise","short_names":["arrows_counterclockwise"],"text":null,"texts":null,"category":"Symbols","subcategory":"arrow","sort_order":1406,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LOW BRIGHTNESS SYMBOL","unified":"1F505","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f505.png","sheet_x":29,"sheet_y":30,"short_name":"low_brightness","short_names":["low_brightness"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1456,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HIGH BRIGHTNESS SYMBOL","unified":"1F506","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f506.png","sheet_x":29,"sheet_y":31,"short_name":"high_brightness","short_names":["high_brightness"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1457,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SPEAKER WITH CANCELLATION STROKE","unified":"1F507","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f507.png","sheet_x":29,"sheet_y":32,"short_name":"mute","short_names":["mute"],"text":null,"texts":null,"category":"Objects","subcategory":"sound","sort_order":1155,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SPEAKER","unified":"1F508","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f508.png","sheet_x":29,"sheet_y":33,"short_name":"speaker","short_names":["speaker"],"text":null,"texts":null,"category":"Objects","subcategory":"sound","sort_order":1156,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SPEAKER WITH ONE SOUND WAVE","unified":"1F509","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f509.png","sheet_x":29,"sheet_y":34,"short_name":"sound","short_names":["sound"],"text":null,"texts":null,"category":"Objects","subcategory":"sound","sort_order":1157,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SPEAKER WITH THREE SOUND WAVES","unified":"1F50A","non_qualified":null,"docomo":null,"au":"E511","softbank":"E141","google":"FE821","image":"1f50a.png","sheet_x":29,"sheet_y":35,"short_name":"loud_sound","short_names":["loud_sound"],"text":null,"texts":null,"category":"Objects","subcategory":"sound","sort_order":1158,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BATTERY","unified":"1F50B","non_qualified":null,"docomo":null,"au":"E584","softbank":null,"google":"FE4FC","image":"1f50b.png","sheet_x":29,"sheet_y":36,"short_name":"battery","short_names":["battery"],"text":null,"texts":null,"category":"Objects","subcategory":"computer","sort_order":1188,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ELECTRIC PLUG","unified":"1F50C","non_qualified":null,"docomo":null,"au":"E589","softbank":null,"google":"FE4FE","image":"1f50c.png","sheet_x":29,"sheet_y":37,"short_name":"electric_plug","short_names":["electric_plug"],"text":null,"texts":null,"category":"Objects","subcategory":"computer","sort_order":1190,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LEFT-POINTING MAGNIFYING GLASS","unified":"1F50D","non_qualified":null,"docomo":"E6DC","au":"E518","softbank":"E114","google":"FEB85","image":"1f50d.png","sheet_x":29,"sheet_y":38,"short_name":"mag","short_names":["mag"],"text":null,"texts":null,"category":"Objects","subcategory":"light & video","sort_order":1211,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RIGHT-POINTING MAGNIFYING GLASS","unified":"1F50E","non_qualified":null,"docomo":"E6DC","au":"EB05","softbank":null,"google":"FEB8D","image":"1f50e.png","sheet_x":29,"sheet_y":39,"short_name":"mag_right","short_names":["mag_right"],"text":null,"texts":null,"category":"Objects","subcategory":"light & video","sort_order":1212,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LOCK WITH INK PEN","unified":"1F50F","non_qualified":null,"docomo":"E6D9","au":"EB0C","softbank":null,"google":"FEB90","image":"1f50f.png","sheet_x":29,"sheet_y":40,"short_name":"lock_with_ink_pen","short_names":["lock_with_ink_pen"],"text":null,"texts":null,"category":"Objects","subcategory":"lock","sort_order":1290,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOSED LOCK WITH KEY","unified":"1F510","non_qualified":null,"docomo":"E6D9","au":"EAFC","softbank":null,"google":"FEB8A","image":"1f510.png","sheet_x":29,"sheet_y":41,"short_name":"closed_lock_with_key","short_names":["closed_lock_with_key"],"text":null,"texts":null,"category":"Objects","subcategory":"lock","sort_order":1291,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"KEY","unified":"1F511","non_qualified":null,"docomo":"E6D9","au":"E519","softbank":"E03F","google":"FEB82","image":"1f511.png","sheet_x":29,"sheet_y":42,"short_name":"key","short_names":["key"],"text":null,"texts":null,"category":"Objects","subcategory":"lock","sort_order":1292,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LOCK","unified":"1F512","non_qualified":null,"docomo":"E6D9","au":"E51C","softbank":"E144","google":"FEB86","image":"1f512.png","sheet_x":29,"sheet_y":43,"short_name":"lock","short_names":["lock"],"text":null,"texts":null,"category":"Objects","subcategory":"lock","sort_order":1288,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"OPEN LOCK","unified":"1F513","non_qualified":null,"docomo":"E6D9","au":"E51C","softbank":"E145","google":"FEB87","image":"1f513.png","sheet_x":29,"sheet_y":44,"short_name":"unlock","short_names":["unlock"],"text":null,"texts":null,"category":"Objects","subcategory":"lock","sort_order":1289,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BELL","unified":"1F514","non_qualified":null,"docomo":"E713","au":"E512","softbank":"E325","google":"FE4F2","image":"1f514.png","sheet_x":29,"sheet_y":45,"short_name":"bell","short_names":["bell"],"text":null,"texts":null,"category":"Objects","subcategory":"sound","sort_order":1162,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BELL WITH CANCELLATION STROKE","unified":"1F515","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f515.png","sheet_x":29,"sheet_y":46,"short_name":"no_bell","short_names":["no_bell"],"text":null,"texts":null,"category":"Objects","subcategory":"sound","sort_order":1163,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BOOKMARK","unified":"1F516","non_qualified":null,"docomo":null,"au":"EB07","softbank":null,"google":"FEB8F","image":"1f516.png","sheet_x":29,"sheet_y":47,"short_name":"bookmark","short_names":["bookmark"],"text":null,"texts":null,"category":"Objects","subcategory":"book-paper","sort_order":1233,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LINK SYMBOL","unified":"1F517","non_qualified":null,"docomo":null,"au":"E58A","softbank":null,"google":"FEB4B","image":"1f517.png","sheet_x":29,"sheet_y":48,"short_name":"link","short_names":["link"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1313,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RADIO BUTTON","unified":"1F518","non_qualified":null,"docomo":null,"au":"EB04","softbank":null,"google":"FEB8C","image":"1f518.png","sheet_x":29,"sheet_y":49,"short_name":"radio_button","short_names":["radio_button"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1583,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BACK WITH LEFTWARDS ARROW ABOVE","unified":"1F519","non_qualified":null,"docomo":null,"au":"EB06","softbank":null,"google":"FEB8E","image":"1f519.png","sheet_x":29,"sheet_y":50,"short_name":"back","short_names":["back"],"text":null,"texts":null,"category":"Symbols","subcategory":"arrow","sort_order":1407,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"END WITH LEFTWARDS ARROW ABOVE","unified":"1F51A","non_qualified":null,"docomo":"E6B9","au":null,"softbank":null,"google":"FE01A","image":"1f51a.png","sheet_x":29,"sheet_y":51,"short_name":"end","short_names":["end"],"text":null,"texts":null,"category":"Symbols","subcategory":"arrow","sort_order":1408,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ON WITH EXCLAMATION MARK WITH LEFT RIGHT ARROW ABOVE","unified":"1F51B","non_qualified":null,"docomo":"E6B8","au":null,"softbank":null,"google":"FE019","image":"1f51b.png","sheet_x":29,"sheet_y":52,"short_name":"on","short_names":["on"],"text":null,"texts":null,"category":"Symbols","subcategory":"arrow","sort_order":1409,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SOON WITH RIGHTWARDS ARROW ABOVE","unified":"1F51C","non_qualified":null,"docomo":"E6B7","au":null,"softbank":null,"google":"FE018","image":"1f51c.png","sheet_x":29,"sheet_y":53,"short_name":"soon","short_names":["soon"],"text":null,"texts":null,"category":"Symbols","subcategory":"arrow","sort_order":1410,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TOP WITH UPWARDS ARROW ABOVE","unified":"1F51D","non_qualified":null,"docomo":null,"au":null,"softbank":"E24C","google":"FEB42","image":"1f51d.png","sheet_x":29,"sheet_y":54,"short_name":"top","short_names":["top"],"text":null,"texts":null,"category":"Symbols","subcategory":"arrow","sort_order":1411,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NO ONE UNDER EIGHTEEN SYMBOL","unified":"1F51E","non_qualified":null,"docomo":null,"au":"EA83","softbank":"E207","google":"FEB25","image":"1f51e.png","sheet_x":29,"sheet_y":55,"short_name":"underage","short_names":["underage"],"text":null,"texts":null,"category":"Symbols","subcategory":"warning","sort_order":1388,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"KEYCAP TEN","unified":"1F51F","non_qualified":null,"docomo":null,"au":"E52B","softbank":null,"google":"FE83B","image":"1f51f.png","sheet_x":29,"sheet_y":56,"short_name":"keycap_ten","short_names":["keycap_ten"],"text":null,"texts":null,"category":"Symbols","subcategory":"keycap","sort_order":1512,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"INPUT SYMBOL FOR LATIN CAPITAL LETTERS","unified":"1F520","non_qualified":null,"docomo":null,"au":"EAFD","softbank":null,"google":"FEB7C","image":"1f520.png","sheet_x":29,"sheet_y":57,"short_name":"capital_abcd","short_names":["capital_abcd"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1513,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"INPUT SYMBOL FOR LATIN SMALL LETTERS","unified":"1F521","non_qualified":null,"docomo":null,"au":"EAFE","softbank":null,"google":"FEB7D","image":"1f521.png","sheet_x":29,"sheet_y":58,"short_name":"abcd","short_names":["abcd"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1514,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"INPUT SYMBOL FOR NUMBERS","unified":"1F522","non_qualified":null,"docomo":null,"au":"EAFF","softbank":null,"google":"FEB7E","image":"1f522.png","sheet_x":29,"sheet_y":59,"short_name":"1234","short_names":["1234"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1515,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"INPUT SYMBOL FOR SYMBOLS","unified":"1F523","non_qualified":null,"docomo":null,"au":"EB00","softbank":null,"google":"FEB7F","image":"1f523.png","sheet_x":29,"sheet_y":60,"short_name":"symbols","short_names":["symbols"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1516,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"INPUT SYMBOL FOR LATIN LETTERS","unified":"1F524","non_qualified":null,"docomo":null,"au":"EB55","softbank":null,"google":"FEB80","image":"1f524.png","sheet_x":30,"sheet_y":0,"short_name":"abc","short_names":["abc"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1517,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FIRE","unified":"1F525","non_qualified":null,"docomo":null,"au":"E47B","softbank":"E11D","google":"FE4F6","image":"1f525.png","sheet_x":30,"sheet_y":1,"short_name":"fire","short_names":["fire"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1021,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ELECTRIC TORCH","unified":"1F526","non_qualified":null,"docomo":"E6FB","au":"E583","softbank":null,"google":"FE4FB","image":"1f526.png","sheet_x":30,"sheet_y":2,"short_name":"flashlight","short_names":["flashlight"],"text":null,"texts":null,"category":"Objects","subcategory":"light & video","sort_order":1215,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WRENCH","unified":"1F527","non_qualified":null,"docomo":"E718","au":"E587","softbank":null,"google":"FE4C9","image":"1f527.png","sheet_x":30,"sheet_y":3,"short_name":"wrench","short_names":["wrench"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1306,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HAMMER","unified":"1F528","non_qualified":null,"docomo":null,"au":"E5CB","softbank":"E116","google":"FE4CA","image":"1f528.png","sheet_x":30,"sheet_y":4,"short_name":"hammer","short_names":["hammer"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1294,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NUT AND BOLT","unified":"1F529","non_qualified":null,"docomo":null,"au":"E581","softbank":null,"google":"FE4CB","image":"1f529.png","sheet_x":30,"sheet_y":5,"short_name":"nut_and_bolt","short_names":["nut_and_bolt"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1308,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HOCHO","unified":"1F52A","non_qualified":null,"docomo":null,"au":"E57F","softbank":null,"google":"FE4FA","image":"1f52a.png","sheet_x":30,"sheet_y":6,"short_name":"hocho","short_names":["hocho","knife"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"dishware","sort_order":803,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PISTOL","unified":"1F52B","non_qualified":null,"docomo":null,"au":"E50A","softbank":"E113","google":"FE4F5","image":"1f52b.png","sheet_x":30,"sheet_y":7,"short_name":"gun","short_names":["gun"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1301,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MICROSCOPE","unified":"1F52C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f52c.png","sheet_x":30,"sheet_y":8,"short_name":"microscope","short_names":["microscope"],"text":null,"texts":null,"category":"Objects","subcategory":"science","sort_order":1323,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TELESCOPE","unified":"1F52D","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f52d.png","sheet_x":30,"sheet_y":9,"short_name":"telescope","short_names":["telescope"],"text":null,"texts":null,"category":"Objects","subcategory":"science","sort_order":1324,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CRYSTAL BALL","unified":"1F52E","non_qualified":null,"docomo":null,"au":"EA8F","softbank":null,"google":"FE4F7","image":"1f52e.png","sheet_x":30,"sheet_y":10,"short_name":"crystal_ball","short_names":["crystal_ball"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1082,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SIX POINTED STAR WITH MIDDLE DOT","unified":"1F52F","non_qualified":null,"docomo":null,"au":"EA8F","softbank":"E23E","google":"FE4F8","image":"1f52f.png","sheet_x":30,"sheet_y":11,"short_name":"six_pointed_star","short_names":["six_pointed_star"],"text":null,"texts":null,"category":"Symbols","subcategory":"religion","sort_order":1423,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"JAPANESE SYMBOL FOR BEGINNER","unified":"1F530","non_qualified":null,"docomo":null,"au":"E480","softbank":"E209","google":"FE044","image":"1f530.png","sheet_x":30,"sheet_y":12,"short_name":"beginner","short_names":["beginner"],"text":null,"texts":null,"category":"Symbols","subcategory":"other-symbol","sort_order":1484,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TRIDENT EMBLEM","unified":"1F531","non_qualified":null,"docomo":"E71A","au":"E5C9","softbank":"E031","google":"FE4D2","image":"1f531.png","sheet_x":30,"sheet_y":13,"short_name":"trident","short_names":["trident"],"text":null,"texts":null,"category":"Symbols","subcategory":"other-symbol","sort_order":1482,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK SQUARE BUTTON","unified":"1F532","non_qualified":null,"docomo":"E69C","au":"E54B","softbank":"E21A","google":"FEB64","image":"1f532.png","sheet_x":30,"sheet_y":14,"short_name":"black_square_button","short_names":["black_square_button"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1585,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WHITE SQUARE BUTTON","unified":"1F533","non_qualified":null,"docomo":"E69C","au":"E54B","softbank":"E21B","google":"FEB67","image":"1f533.png","sheet_x":30,"sheet_y":15,"short_name":"white_square_button","short_names":["white_square_button"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1584,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LARGE RED CIRCLE","unified":"1F534","non_qualified":null,"docomo":"E69C","au":"E54A","softbank":"E219","google":"FEB63","image":"1f534.png","sheet_x":30,"sheet_y":16,"short_name":"red_circle","short_names":["red_circle"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1552,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LARGE BLUE CIRCLE","unified":"1F535","non_qualified":null,"docomo":"E69C","au":"E54B","softbank":null,"google":"FEB64","image":"1f535.png","sheet_x":30,"sheet_y":17,"short_name":"large_blue_circle","short_names":["large_blue_circle"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1556,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LARGE ORANGE DIAMOND","unified":"1F536","non_qualified":null,"docomo":null,"au":"E546","softbank":null,"google":"FEB73","image":"1f536.png","sheet_x":30,"sheet_y":18,"short_name":"large_orange_diamond","short_names":["large_orange_diamond"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1576,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LARGE BLUE DIAMOND","unified":"1F537","non_qualified":null,"docomo":null,"au":"E547","softbank":null,"google":"FEB74","image":"1f537.png","sheet_x":30,"sheet_y":19,"short_name":"large_blue_diamond","short_names":["large_blue_diamond"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1577,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SMALL ORANGE DIAMOND","unified":"1F538","non_qualified":null,"docomo":null,"au":"E536","softbank":null,"google":"FEB75","image":"1f538.png","sheet_x":30,"sheet_y":20,"short_name":"small_orange_diamond","short_names":["small_orange_diamond"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1578,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SMALL BLUE DIAMOND","unified":"1F539","non_qualified":null,"docomo":null,"au":"E537","softbank":null,"google":"FEB76","image":"1f539.png","sheet_x":30,"sheet_y":21,"short_name":"small_blue_diamond","short_names":["small_blue_diamond"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1579,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"UP-POINTING RED TRIANGLE","unified":"1F53A","non_qualified":null,"docomo":null,"au":"E55A","softbank":null,"google":"FEB78","image":"1f53a.png","sheet_x":30,"sheet_y":22,"short_name":"small_red_triangle","short_names":["small_red_triangle"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1580,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DOWN-POINTING RED TRIANGLE","unified":"1F53B","non_qualified":null,"docomo":null,"au":"E55B","softbank":null,"google":"FEB79","image":"1f53b.png","sheet_x":30,"sheet_y":23,"short_name":"small_red_triangle_down","short_names":["small_red_triangle_down"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1581,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"UP-POINTING SMALL RED TRIANGLE","unified":"1F53C","non_qualified":null,"docomo":null,"au":"E543","softbank":null,"google":"FEB01","image":"1f53c.png","sheet_x":30,"sheet_y":24,"short_name":"arrow_up_small","short_names":["arrow_up_small"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1447,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DOWN-POINTING SMALL RED TRIANGLE","unified":"1F53D","non_qualified":null,"docomo":null,"au":"E542","softbank":null,"google":"FEB00","image":"1f53d.png","sheet_x":30,"sheet_y":25,"short_name":"arrow_down_small","short_names":["arrow_down_small"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1449,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"OM","unified":"1F549-FE0F","non_qualified":"1F549","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f549-fe0f.png","sheet_x":30,"sheet_y":26,"short_name":"om_symbol","short_names":["om_symbol"],"text":null,"texts":null,"category":"Symbols","subcategory":"religion","sort_order":1414,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DOVE","unified":"1F54A-FE0F","non_qualified":"1F54A","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f54a-fe0f.png","sheet_x":30,"sheet_y":27,"short_name":"dove_of_peace","short_names":["dove_of_peace"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bird","sort_order":602,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"KAABA","unified":"1F54B","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f54b.png","sheet_x":30,"sheet_y":28,"short_name":"kaaba","short_names":["kaaba"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-religious","sort_order":854,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MOSQUE","unified":"1F54C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f54c.png","sheet_x":30,"sheet_y":29,"short_name":"mosque","short_names":["mosque"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-religious","sort_order":850,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SYNAGOGUE","unified":"1F54D","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f54d.png","sheet_x":30,"sheet_y":30,"short_name":"synagogue","short_names":["synagogue"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-religious","sort_order":852,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MENORAH WITH NINE BRANCHES","unified":"1F54E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f54e.png","sheet_x":30,"sheet_y":31,"short_name":"menorah_with_nine_branches","short_names":["menorah_with_nine_branches"],"text":null,"texts":null,"category":"Symbols","subcategory":"religion","sort_order":1422,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE ONE OCLOCK","unified":"1F550","non_qualified":null,"docomo":"E6BA","au":"E594","softbank":"E024","google":"FE01E","image":"1f550.png","sheet_x":30,"sheet_y":32,"short_name":"clock1","short_names":["clock1"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":955,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE TWO OCLOCK","unified":"1F551","non_qualified":null,"docomo":"E6BA","au":"E594","softbank":"E025","google":"FE01F","image":"1f551.png","sheet_x":30,"sheet_y":33,"short_name":"clock2","short_names":["clock2"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":957,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE THREE OCLOCK","unified":"1F552","non_qualified":null,"docomo":"E6BA","au":"E594","softbank":"E026","google":"FE020","image":"1f552.png","sheet_x":30,"sheet_y":34,"short_name":"clock3","short_names":["clock3"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":959,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE FOUR OCLOCK","unified":"1F553","non_qualified":null,"docomo":"E6BA","au":"E594","softbank":"E027","google":"FE021","image":"1f553.png","sheet_x":30,"sheet_y":35,"short_name":"clock4","short_names":["clock4"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":961,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE FIVE OCLOCK","unified":"1F554","non_qualified":null,"docomo":"E6BA","au":"E594","softbank":"E028","google":"FE022","image":"1f554.png","sheet_x":30,"sheet_y":36,"short_name":"clock5","short_names":["clock5"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":963,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE SIX OCLOCK","unified":"1F555","non_qualified":null,"docomo":"E6BA","au":"E594","softbank":"E029","google":"FE023","image":"1f555.png","sheet_x":30,"sheet_y":37,"short_name":"clock6","short_names":["clock6"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":965,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE SEVEN OCLOCK","unified":"1F556","non_qualified":null,"docomo":"E6BA","au":"E594","softbank":"E02A","google":"FE024","image":"1f556.png","sheet_x":30,"sheet_y":38,"short_name":"clock7","short_names":["clock7"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":967,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE EIGHT OCLOCK","unified":"1F557","non_qualified":null,"docomo":"E6BA","au":"E594","softbank":"E02B","google":"FE025","image":"1f557.png","sheet_x":30,"sheet_y":39,"short_name":"clock8","short_names":["clock8"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":969,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE NINE OCLOCK","unified":"1F558","non_qualified":null,"docomo":"E6BA","au":"E594","softbank":"E02C","google":"FE026","image":"1f558.png","sheet_x":30,"sheet_y":40,"short_name":"clock9","short_names":["clock9"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":971,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE TEN OCLOCK","unified":"1F559","non_qualified":null,"docomo":"E6BA","au":"E594","softbank":"E02D","google":"FE027","image":"1f559.png","sheet_x":30,"sheet_y":41,"short_name":"clock10","short_names":["clock10"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":973,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE ELEVEN OCLOCK","unified":"1F55A","non_qualified":null,"docomo":"E6BA","au":"E594","softbank":"E02E","google":"FE028","image":"1f55a.png","sheet_x":30,"sheet_y":42,"short_name":"clock11","short_names":["clock11"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":975,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE TWELVE OCLOCK","unified":"1F55B","non_qualified":null,"docomo":"E6BA","au":"E594","softbank":"E02F","google":"FE029","image":"1f55b.png","sheet_x":30,"sheet_y":43,"short_name":"clock12","short_names":["clock12"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":953,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE ONE-THIRTY","unified":"1F55C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f55c.png","sheet_x":30,"sheet_y":44,"short_name":"clock130","short_names":["clock130"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":956,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE TWO-THIRTY","unified":"1F55D","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f55d.png","sheet_x":30,"sheet_y":45,"short_name":"clock230","short_names":["clock230"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":958,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE THREE-THIRTY","unified":"1F55E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f55e.png","sheet_x":30,"sheet_y":46,"short_name":"clock330","short_names":["clock330"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":960,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE FOUR-THIRTY","unified":"1F55F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f55f.png","sheet_x":30,"sheet_y":47,"short_name":"clock430","short_names":["clock430"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":962,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE FIVE-THIRTY","unified":"1F560","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f560.png","sheet_x":30,"sheet_y":48,"short_name":"clock530","short_names":["clock530"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":964,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE SIX-THIRTY","unified":"1F561","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f561.png","sheet_x":30,"sheet_y":49,"short_name":"clock630","short_names":["clock630"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":966,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE SEVEN-THIRTY","unified":"1F562","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f562.png","sheet_x":30,"sheet_y":50,"short_name":"clock730","short_names":["clock730"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":968,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE EIGHT-THIRTY","unified":"1F563","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f563.png","sheet_x":30,"sheet_y":51,"short_name":"clock830","short_names":["clock830"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":970,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE NINE-THIRTY","unified":"1F564","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f564.png","sheet_x":30,"sheet_y":52,"short_name":"clock930","short_names":["clock930"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":972,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE TEN-THIRTY","unified":"1F565","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f565.png","sheet_x":30,"sheet_y":53,"short_name":"clock1030","short_names":["clock1030"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":974,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE ELEVEN-THIRTY","unified":"1F566","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f566.png","sheet_x":30,"sheet_y":54,"short_name":"clock1130","short_names":["clock1130"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":976,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE TWELVE-THIRTY","unified":"1F567","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f567.png","sheet_x":30,"sheet_y":55,"short_name":"clock1230","short_names":["clock1230"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":954,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CANDLE","unified":"1F56F-FE0F","non_qualified":"1F56F","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f56f-fe0f.png","sheet_x":30,"sheet_y":56,"short_name":"candle","short_names":["candle"],"text":null,"texts":null,"category":"Objects","subcategory":"light & video","sort_order":1213,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MANTELPIECE CLOCK","unified":"1F570-FE0F","non_qualified":"1F570","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f570-fe0f.png","sheet_x":30,"sheet_y":57,"short_name":"mantelpiece_clock","short_names":["mantelpiece_clock"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":952,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HOLE","unified":"1F573-FE0F","non_qualified":"1F573","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f573-fe0f.png","sheet_x":30,"sheet_y":58,"short_name":"hole","short_names":["hole"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":156,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PERSON IN SUIT LEVITATING","unified":"1F574-FE0F","non_qualified":"1F574","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f574-fe0f.png","sheet_x":30,"sheet_y":59,"short_name":"man_in_business_suit_levitating","short_names":["man_in_business_suit_levitating"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":424,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F574-1F3FB","non_qualified":null,"image":"1f574-1f3fb.png","sheet_x":30,"sheet_y":60,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F574-1F3FC","non_qualified":null,"image":"1f574-1f3fc.png","sheet_x":31,"sheet_y":0,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F574-1F3FD","non_qualified":null,"image":"1f574-1f3fd.png","sheet_x":31,"sheet_y":1,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F574-1F3FE","non_qualified":null,"image":"1f574-1f3fe.png","sheet_x":31,"sheet_y":2,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F574-1F3FF","non_qualified":null,"image":"1f574-1f3ff.png","sheet_x":31,"sheet_y":3,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN DETECTIVE","unified":"1F575-FE0F-200D-2640-FE0F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f575-fe0f-200d-2640-fe0f.png","sheet_x":31,"sheet_y":4,"short_name":"female-detective","short_names":["female-detective"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":334,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"1F575-1F3FB-200D-2640-FE0F","non_qualified":"1F575-1F3FB-200D-2640","image":"1f575-1f3fb-200d-2640-fe0f.png","sheet_x":31,"sheet_y":5,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F575-1F3FC-200D-2640-FE0F","non_qualified":"1F575-1F3FC-200D-2640","image":"1f575-1f3fc-200d-2640-fe0f.png","sheet_x":31,"sheet_y":6,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F575-1F3FD-200D-2640-FE0F","non_qualified":"1F575-1F3FD-200D-2640","image":"1f575-1f3fd-200d-2640-fe0f.png","sheet_x":31,"sheet_y":7,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F575-1F3FE-200D-2640-FE0F","non_qualified":"1F575-1F3FE-200D-2640","image":"1f575-1f3fe-200d-2640-fe0f.png","sheet_x":31,"sheet_y":8,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F575-1F3FF-200D-2640-FE0F","non_qualified":"1F575-1F3FF-200D-2640","image":"1f575-1f3ff-200d-2640-fe0f.png","sheet_x":31,"sheet_y":9,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN DETECTIVE","unified":"1F575-FE0F-200D-2642-FE0F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f575-fe0f-200d-2642-fe0f.png","sheet_x":31,"sheet_y":10,"short_name":"male-detective","short_names":["male-detective"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":333,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"1F575-1F3FB-200D-2642-FE0F","non_qualified":"1F575-1F3FB-200D-2642","image":"1f575-1f3fb-200d-2642-fe0f.png","sheet_x":31,"sheet_y":11,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F575-1F3FC-200D-2642-FE0F","non_qualified":"1F575-1F3FC-200D-2642","image":"1f575-1f3fc-200d-2642-fe0f.png","sheet_x":31,"sheet_y":12,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F575-1F3FD-200D-2642-FE0F","non_qualified":"1F575-1F3FD-200D-2642","image":"1f575-1f3fd-200d-2642-fe0f.png","sheet_x":31,"sheet_y":13,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F575-1F3FE-200D-2642-FE0F","non_qualified":"1F575-1F3FE-200D-2642","image":"1f575-1f3fe-200d-2642-fe0f.png","sheet_x":31,"sheet_y":14,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F575-1F3FF-200D-2642-FE0F","non_qualified":"1F575-1F3FF-200D-2642","image":"1f575-1f3ff-200d-2642-fe0f.png","sheet_x":31,"sheet_y":15,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F575-FE0F"},{"name":"DETECTIVE","unified":"1F575-FE0F","non_qualified":"1F575","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f575-fe0f.png","sheet_x":31,"sheet_y":16,"short_name":"sleuth_or_spy","short_names":["sleuth_or_spy"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":332,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F575-1F3FB","non_qualified":null,"image":"1f575-1f3fb.png","sheet_x":31,"sheet_y":17,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F575-1F3FC","non_qualified":null,"image":"1f575-1f3fc.png","sheet_x":31,"sheet_y":18,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F575-1F3FD","non_qualified":null,"image":"1f575-1f3fd.png","sheet_x":31,"sheet_y":19,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F575-1F3FE","non_qualified":null,"image":"1f575-1f3fe.png","sheet_x":31,"sheet_y":20,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F575-1F3FF","non_qualified":null,"image":"1f575-1f3ff.png","sheet_x":31,"sheet_y":21,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F575-FE0F-200D-2642-FE0F"},{"name":"SUNGLASSES","unified":"1F576-FE0F","non_qualified":"1F576","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f576-fe0f.png","sheet_x":31,"sheet_y":22,"short_name":"dark_sunglasses","short_names":["dark_sunglasses"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1111,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SPIDER","unified":"1F577-FE0F","non_qualified":"1F577","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f577-fe0f.png","sheet_x":31,"sheet_y":23,"short_name":"spider","short_names":["spider"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bug","sort_order":641,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SPIDER WEB","unified":"1F578-FE0F","non_qualified":"1F578","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f578-fe0f.png","sheet_x":31,"sheet_y":24,"short_name":"spider_web","short_names":["spider_web"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bug","sort_order":642,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"JOYSTICK","unified":"1F579-FE0F","non_qualified":"1F579","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f579-fe0f.png","sheet_x":31,"sheet_y":25,"short_name":"joystick","short_names":["joystick"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1087,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MAN DANCING","unified":"1F57A","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f57a.png","sheet_x":31,"sheet_y":26,"short_name":"man_dancing","short_names":["man_dancing"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":423,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F57A-1F3FB","non_qualified":null,"image":"1f57a-1f3fb.png","sheet_x":31,"sheet_y":27,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F57A-1F3FC","non_qualified":null,"image":"1f57a-1f3fc.png","sheet_x":31,"sheet_y":28,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F57A-1F3FD","non_qualified":null,"image":"1f57a-1f3fd.png","sheet_x":31,"sheet_y":29,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F57A-1F3FE","non_qualified":null,"image":"1f57a-1f3fe.png","sheet_x":31,"sheet_y":30,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F57A-1F3FF","non_qualified":null,"image":"1f57a-1f3ff.png","sheet_x":31,"sheet_y":31,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"LINKED PAPERCLIPS","unified":"1F587-FE0F","non_qualified":"1F587","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f587-fe0f.png","sheet_x":31,"sheet_y":32,"short_name":"linked_paperclips","short_names":["linked_paperclips"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1281,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PEN","unified":"1F58A-FE0F","non_qualified":"1F58A","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f58a-fe0f.png","sheet_x":31,"sheet_y":33,"short_name":"lower_left_ballpoint_pen","short_names":["lower_left_ballpoint_pen"],"text":null,"texts":null,"category":"Objects","subcategory":"writing","sort_order":1261,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FOUNTAIN PEN","unified":"1F58B-FE0F","non_qualified":"1F58B","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f58b-fe0f.png","sheet_x":31,"sheet_y":34,"short_name":"lower_left_fountain_pen","short_names":["lower_left_fountain_pen"],"text":null,"texts":null,"category":"Objects","subcategory":"writing","sort_order":1260,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PAINTBRUSH","unified":"1F58C-FE0F","non_qualified":"1F58C","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f58c-fe0f.png","sheet_x":31,"sheet_y":35,"short_name":"lower_left_paintbrush","short_names":["lower_left_paintbrush"],"text":null,"texts":null,"category":"Objects","subcategory":"writing","sort_order":1262,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CRAYON","unified":"1F58D-FE0F","non_qualified":"1F58D","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f58d-fe0f.png","sheet_x":31,"sheet_y":36,"short_name":"lower_left_crayon","short_names":["lower_left_crayon"],"text":null,"texts":null,"category":"Objects","subcategory":"writing","sort_order":1263,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HAND WITH FINGERS SPLAYED","unified":"1F590-FE0F","non_qualified":"1F590","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f590-fe0f.png","sheet_x":31,"sheet_y":37,"short_name":"raised_hand_with_fingers_splayed","short_names":["raised_hand_with_fingers_splayed"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-open","sort_order":166,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F590-1F3FB","non_qualified":null,"image":"1f590-1f3fb.png","sheet_x":31,"sheet_y":38,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F590-1F3FC","non_qualified":null,"image":"1f590-1f3fc.png","sheet_x":31,"sheet_y":39,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F590-1F3FD","non_qualified":null,"image":"1f590-1f3fd.png","sheet_x":31,"sheet_y":40,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F590-1F3FE","non_qualified":null,"image":"1f590-1f3fe.png","sheet_x":31,"sheet_y":41,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F590-1F3FF","non_qualified":null,"image":"1f590-1f3ff.png","sheet_x":31,"sheet_y":42,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"REVERSED HAND WITH MIDDLE FINGER EXTENDED","unified":"1F595","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f595.png","sheet_x":31,"sheet_y":43,"short_name":"middle_finger","short_names":["middle_finger","reversed_hand_with_middle_finger_extended"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-single-finger","sort_order":185,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F595-1F3FB","non_qualified":null,"image":"1f595-1f3fb.png","sheet_x":31,"sheet_y":44,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F595-1F3FC","non_qualified":null,"image":"1f595-1f3fc.png","sheet_x":31,"sheet_y":45,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F595-1F3FD","non_qualified":null,"image":"1f595-1f3fd.png","sheet_x":31,"sheet_y":46,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F595-1F3FE","non_qualified":null,"image":"1f595-1f3fe.png","sheet_x":31,"sheet_y":47,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F595-1F3FF","non_qualified":null,"image":"1f595-1f3ff.png","sheet_x":31,"sheet_y":48,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"RAISED HAND WITH PART BETWEEN MIDDLE AND RING FINGERS","unified":"1F596","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f596.png","sheet_x":31,"sheet_y":49,"short_name":"spock-hand","short_names":["spock-hand"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-open","sort_order":168,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F596-1F3FB","non_qualified":null,"image":"1f596-1f3fb.png","sheet_x":31,"sheet_y":50,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F596-1F3FC","non_qualified":null,"image":"1f596-1f3fc.png","sheet_x":31,"sheet_y":51,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F596-1F3FD","non_qualified":null,"image":"1f596-1f3fd.png","sheet_x":31,"sheet_y":52,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F596-1F3FE","non_qualified":null,"image":"1f596-1f3fe.png","sheet_x":31,"sheet_y":53,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F596-1F3FF","non_qualified":null,"image":"1f596-1f3ff.png","sheet_x":31,"sheet_y":54,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"BLACK HEART","unified":"1F5A4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f5a4.png","sheet_x":31,"sheet_y":55,"short_name":"black_heart","short_names":["black_heart"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":148,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DESKTOP COMPUTER","unified":"1F5A5-FE0F","non_qualified":"1F5A5","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f5a5-fe0f.png","sheet_x":31,"sheet_y":56,"short_name":"desktop_computer","short_names":["desktop_computer"],"text":null,"texts":null,"category":"Objects","subcategory":"computer","sort_order":1192,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PRINTER","unified":"1F5A8-FE0F","non_qualified":"1F5A8","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f5a8-fe0f.png","sheet_x":31,"sheet_y":57,"short_name":"printer","short_names":["printer"],"text":null,"texts":null,"category":"Objects","subcategory":"computer","sort_order":1193,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"COMPUTER MOUSE","unified":"1F5B1-FE0F","non_qualified":"1F5B1","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f5b1-fe0f.png","sheet_x":31,"sheet_y":58,"short_name":"three_button_mouse","short_names":["three_button_mouse"],"text":null,"texts":null,"category":"Objects","subcategory":"computer","sort_order":1195,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TRACKBALL","unified":"1F5B2-FE0F","non_qualified":"1F5B2","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f5b2-fe0f.png","sheet_x":31,"sheet_y":59,"short_name":"trackball","short_names":["trackball"],"text":null,"texts":null,"category":"Objects","subcategory":"computer","sort_order":1196,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FRAMED PICTURE","unified":"1F5BC-FE0F","non_qualified":"1F5BC","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f5bc-fe0f.png","sheet_x":31,"sheet_y":60,"short_name":"frame_with_picture","short_names":["frame_with_picture"],"text":null,"texts":null,"category":"Activities","subcategory":"arts & crafts","sort_order":1104,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CARD INDEX DIVIDERS","unified":"1F5C2-FE0F","non_qualified":"1F5C2","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f5c2-fe0f.png","sheet_x":32,"sheet_y":0,"short_name":"card_index_dividers","short_names":["card_index_dividers"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1268,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CARD FILE BOX","unified":"1F5C3-FE0F","non_qualified":"1F5C3","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f5c3-fe0f.png","sheet_x":32,"sheet_y":1,"short_name":"card_file_box","short_names":["card_file_box"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1285,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FILE CABINET","unified":"1F5C4-FE0F","non_qualified":"1F5C4","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f5c4-fe0f.png","sheet_x":32,"sheet_y":2,"short_name":"file_cabinet","short_names":["file_cabinet"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1286,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WASTEBASKET","unified":"1F5D1-FE0F","non_qualified":"1F5D1","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f5d1-fe0f.png","sheet_x":32,"sheet_y":3,"short_name":"wastebasket","short_names":["wastebasket"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1287,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SPIRAL NOTEPAD","unified":"1F5D2-FE0F","non_qualified":"1F5D2","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f5d2-fe0f.png","sheet_x":32,"sheet_y":4,"short_name":"spiral_note_pad","short_names":["spiral_note_pad"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1271,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SPIRAL CALENDAR","unified":"1F5D3-FE0F","non_qualified":"1F5D3","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f5d3-fe0f.png","sheet_x":32,"sheet_y":5,"short_name":"spiral_calendar_pad","short_names":["spiral_calendar_pad"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1272,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLAMP","unified":"1F5DC-FE0F","non_qualified":"1F5DC","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f5dc-fe0f.png","sheet_x":32,"sheet_y":6,"short_name":"compression","short_names":["compression"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1310,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"OLD KEY","unified":"1F5DD-FE0F","non_qualified":"1F5DD","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f5dd-fe0f.png","sheet_x":32,"sheet_y":7,"short_name":"old_key","short_names":["old_key"],"text":null,"texts":null,"category":"Objects","subcategory":"lock","sort_order":1293,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ROLLED-UP NEWSPAPER","unified":"1F5DE-FE0F","non_qualified":"1F5DE","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f5de-fe0f.png","sheet_x":32,"sheet_y":8,"short_name":"rolled_up_newspaper","short_names":["rolled_up_newspaper"],"text":null,"texts":null,"category":"Objects","subcategory":"book-paper","sort_order":1231,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DAGGER","unified":"1F5E1-FE0F","non_qualified":"1F5E1","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f5e1-fe0f.png","sheet_x":32,"sheet_y":9,"short_name":"dagger_knife","short_names":["dagger_knife"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1299,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SPEAKING HEAD","unified":"1F5E3-FE0F","non_qualified":"1F5E3","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f5e3-fe0f.png","sheet_x":32,"sheet_y":10,"short_name":"speaking_head_in_silhouette","short_names":["speaking_head_in_silhouette"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-symbol","sort_order":520,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LEFT SPEECH BUBBLE","unified":"1F5E8-FE0F","non_qualified":"1F5E8","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f5e8-fe0f.png","sheet_x":32,"sheet_y":11,"short_name":"left_speech_bubble","short_names":["left_speech_bubble"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":160,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RIGHT ANGER BUBBLE","unified":"1F5EF-FE0F","non_qualified":"1F5EF","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f5ef-fe0f.png","sheet_x":32,"sheet_y":12,"short_name":"right_anger_bubble","short_names":["right_anger_bubble"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":161,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BALLOT BOX WITH BALLOT","unified":"1F5F3-FE0F","non_qualified":"1F5F3","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f5f3-fe0f.png","sheet_x":32,"sheet_y":13,"short_name":"ballot_box_with_ballot","short_names":["ballot_box_with_ballot"],"text":null,"texts":null,"category":"Objects","subcategory":"mail","sort_order":1257,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WORLD MAP","unified":"1F5FA-FE0F","non_qualified":"1F5FA","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f5fa-fe0f.png","sheet_x":32,"sheet_y":14,"short_name":"world_map","short_names":["world_map"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-map","sort_order":810,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MOUNT FUJI","unified":"1F5FB","non_qualified":null,"docomo":"E740","au":"E5BD","softbank":"E03B","google":"FE4C3","image":"1f5fb.png","sheet_x":32,"sheet_y":15,"short_name":"mount_fuji","short_names":["mount_fuji"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-geographic","sort_order":816,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TOKYO TOWER","unified":"1F5FC","non_qualified":null,"docomo":null,"au":"E4C0","softbank":"E509","google":"FE4C4","image":"1f5fc.png","sheet_x":32,"sheet_y":16,"short_name":"tokyo_tower","short_names":["tokyo_tower"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":847,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"STATUE OF LIBERTY","unified":"1F5FD","non_qualified":null,"docomo":null,"au":null,"softbank":"E51D","google":"FE4C6","image":"1f5fd.png","sheet_x":32,"sheet_y":17,"short_name":"statue_of_liberty","short_names":["statue_of_liberty"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":848,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SILHOUETTE OF JAPAN","unified":"1F5FE","non_qualified":null,"docomo":null,"au":"E572","softbank":null,"google":"FE4C7","image":"1f5fe.png","sheet_x":32,"sheet_y":18,"short_name":"japan","short_names":["japan"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-map","sort_order":811,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MOYAI","unified":"1F5FF","non_qualified":null,"docomo":null,"au":"EB6C","softbank":null,"google":"FE4C8","image":"1f5ff.png","sheet_x":32,"sheet_y":19,"short_name":"moyai","short_names":["moyai"],"text":null,"texts":null,"category":"Objects","subcategory":"other-object","sort_order":1362,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GRINNING FACE","unified":"1F600","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f600.png","sheet_x":32,"sheet_y":20,"short_name":"grinning","short_names":["grinning"],"text":":D","texts":null,"category":"Smileys & Emotion","subcategory":"face-smiling","sort_order":1,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GRINNING FACE WITH SMILING EYES","unified":"1F601","non_qualified":null,"docomo":"E753","au":"EB80","softbank":"E404","google":"FE333","image":"1f601.png","sheet_x":32,"sheet_y":21,"short_name":"grin","short_names":["grin"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-smiling","sort_order":4,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE WITH TEARS OF JOY","unified":"1F602","non_qualified":null,"docomo":"E72A","au":"EB64","softbank":"E412","google":"FE334","image":"1f602.png","sheet_x":32,"sheet_y":22,"short_name":"joy","short_names":["joy"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-smiling","sort_order":8,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SMILING FACE WITH OPEN MOUTH","unified":"1F603","non_qualified":null,"docomo":"E6F0","au":"E471","softbank":"E057","google":"FE330","image":"1f603.png","sheet_x":32,"sheet_y":23,"short_name":"smiley","short_names":["smiley"],"text":":)","texts":["=)","=-)"],"category":"Smileys & Emotion","subcategory":"face-smiling","sort_order":2,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SMILING FACE WITH OPEN MOUTH AND SMILING EYES","unified":"1F604","non_qualified":null,"docomo":"E6F0","au":"E471","softbank":"E415","google":"FE338","image":"1f604.png","sheet_x":32,"sheet_y":24,"short_name":"smile","short_names":["smile"],"text":":)","texts":["C:","c:",":D",":-D"],"category":"Smileys & Emotion","subcategory":"face-smiling","sort_order":3,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SMILING FACE WITH OPEN MOUTH AND COLD SWEAT","unified":"1F605","non_qualified":null,"docomo":"E722","au":"E471-E5B1","softbank":null,"google":"FE331","image":"1f605.png","sheet_x":32,"sheet_y":25,"short_name":"sweat_smile","short_names":["sweat_smile"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-smiling","sort_order":6,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SMILING FACE WITH OPEN MOUTH AND TIGHTLY-CLOSED EYES","unified":"1F606","non_qualified":null,"docomo":"E72A","au":"EAC5","softbank":null,"google":"FE332","image":"1f606.png","sheet_x":32,"sheet_y":26,"short_name":"laughing","short_names":["laughing","satisfied"],"text":null,"texts":[":>",":->"],"category":"Smileys & Emotion","subcategory":"face-smiling","sort_order":5,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SMILING FACE WITH HALO","unified":"1F607","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f607.png","sheet_x":32,"sheet_y":27,"short_name":"innocent","short_names":["innocent"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-smiling","sort_order":14,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SMILING FACE WITH HORNS","unified":"1F608","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f608.png","sheet_x":32,"sheet_y":28,"short_name":"smiling_imp","short_names":["smiling_imp"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-negative","sort_order":103,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WINKING FACE","unified":"1F609","non_qualified":null,"docomo":"E729","au":"E5C3","softbank":"E405","google":"FE347","image":"1f609.png","sheet_x":32,"sheet_y":29,"short_name":"wink","short_names":["wink"],"text":";)","texts":[";)",";-)"],"category":"Smileys & Emotion","subcategory":"face-smiling","sort_order":12,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SMILING FACE WITH SMILING EYES","unified":"1F60A","non_qualified":null,"docomo":"E6F0","au":"EACD","softbank":"E056","google":"FE335","image":"1f60a.png","sheet_x":32,"sheet_y":30,"short_name":"blush","short_names":["blush"],"text":":)","texts":null,"category":"Smileys & Emotion","subcategory":"face-smiling","sort_order":13,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE SAVOURING DELICIOUS FOOD","unified":"1F60B","non_qualified":null,"docomo":"E752","au":"EACD","softbank":null,"google":"FE32B","image":"1f60b.png","sheet_x":32,"sheet_y":31,"short_name":"yum","short_names":["yum"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-tongue","sort_order":24,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RELIEVED FACE","unified":"1F60C","non_qualified":null,"docomo":"E721","au":"EAC5","softbank":"E40A","google":"FE33E","image":"1f60c.png","sheet_x":32,"sheet_y":32,"short_name":"relieved","short_names":["relieved"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-sleepy","sort_order":50,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SMILING FACE WITH HEART-SHAPED EYES","unified":"1F60D","non_qualified":null,"docomo":"E726","au":"E5C4","softbank":"E106","google":"FE327","image":"1f60d.png","sheet_x":32,"sheet_y":33,"short_name":"heart_eyes","short_names":["heart_eyes"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-affection","sort_order":16,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SMILING FACE WITH SUNGLASSES","unified":"1F60E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f60e.png","sheet_x":32,"sheet_y":34,"short_name":"sunglasses","short_names":["sunglasses"],"text":null,"texts":["8)"],"category":"Smileys & Emotion","subcategory":"face-glasses","sort_order":70,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SMIRKING FACE","unified":"1F60F","non_qualified":null,"docomo":"E72C","au":"EABF","softbank":"E402","google":"FE343","image":"1f60f.png","sheet_x":32,"sheet_y":35,"short_name":"smirk","short_names":["smirk"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-neutral-skeptical","sort_order":44,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NEUTRAL FACE","unified":"1F610","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f610.png","sheet_x":32,"sheet_y":36,"short_name":"neutral_face","short_names":["neutral_face"],"text":null,"texts":[":|",":-|"],"category":"Smileys & Emotion","subcategory":"face-neutral-skeptical","sort_order":39,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EXPRESSIONLESS FACE","unified":"1F611","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f611.png","sheet_x":32,"sheet_y":37,"short_name":"expressionless","short_names":["expressionless"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-neutral-skeptical","sort_order":40,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"UNAMUSED FACE","unified":"1F612","non_qualified":null,"docomo":"E725","au":"EAC9","softbank":"E40E","google":"FE326","image":"1f612.png","sheet_x":32,"sheet_y":38,"short_name":"unamused","short_names":["unamused"],"text":":(","texts":null,"category":"Smileys & Emotion","subcategory":"face-neutral-skeptical","sort_order":45,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE WITH COLD SWEAT","unified":"1F613","non_qualified":null,"docomo":"E723","au":"E5C6","softbank":"E108","google":"FE344","image":"1f613.png","sheet_x":32,"sheet_y":39,"short_name":"sweat","short_names":["sweat"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":95,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PENSIVE FACE","unified":"1F614","non_qualified":null,"docomo":"E720","au":"EAC0","softbank":"E403","google":"FE340","image":"1f614.png","sheet_x":32,"sheet_y":40,"short_name":"pensive","short_names":["pensive"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-sleepy","sort_order":51,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CONFUSED FACE","unified":"1F615","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f615.png","sheet_x":32,"sheet_y":41,"short_name":"confused","short_names":["confused"],"text":null,"texts":[":\\",":-\\",":\/",":-\/"],"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":73,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CONFOUNDED FACE","unified":"1F616","non_qualified":null,"docomo":"E6F3","au":"EAC3","softbank":"E407","google":"FE33F","image":"1f616.png","sheet_x":32,"sheet_y":42,"short_name":"confounded","short_names":["confounded"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":92,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"KISSING FACE","unified":"1F617","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f617.png","sheet_x":32,"sheet_y":43,"short_name":"kissing","short_names":["kissing"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-affection","sort_order":19,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE THROWING A KISS","unified":"1F618","non_qualified":null,"docomo":"E726","au":"EACF","softbank":"E418","google":"FE32C","image":"1f618.png","sheet_x":32,"sheet_y":44,"short_name":"kissing_heart","short_names":["kissing_heart"],"text":null,"texts":[":*",":-*"],"category":"Smileys & Emotion","subcategory":"face-affection","sort_order":18,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"KISSING FACE WITH SMILING EYES","unified":"1F619","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f619.png","sheet_x":32,"sheet_y":45,"short_name":"kissing_smiling_eyes","short_names":["kissing_smiling_eyes"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-affection","sort_order":22,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"KISSING FACE WITH CLOSED EYES","unified":"1F61A","non_qualified":null,"docomo":"E726","au":"EACE","softbank":"E417","google":"FE32D","image":"1f61a.png","sheet_x":32,"sheet_y":46,"short_name":"kissing_closed_eyes","short_names":["kissing_closed_eyes"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-affection","sort_order":21,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE WITH STUCK-OUT TONGUE","unified":"1F61B","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f61b.png","sheet_x":32,"sheet_y":47,"short_name":"stuck_out_tongue","short_names":["stuck_out_tongue"],"text":":p","texts":[":p",":-p",":P",":-P",":b",":-b"],"category":"Smileys & Emotion","subcategory":"face-tongue","sort_order":25,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE WITH STUCK-OUT TONGUE AND WINKING EYE","unified":"1F61C","non_qualified":null,"docomo":"E728","au":"E4E7","softbank":"E105","google":"FE329","image":"1f61c.png","sheet_x":32,"sheet_y":48,"short_name":"stuck_out_tongue_winking_eye","short_names":["stuck_out_tongue_winking_eye"],"text":";p","texts":[";p",";-p",";b",";-b",";P",";-P"],"category":"Smileys & Emotion","subcategory":"face-tongue","sort_order":26,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE WITH STUCK-OUT TONGUE AND TIGHTLY-CLOSED EYES","unified":"1F61D","non_qualified":null,"docomo":"E728","au":"E4E7","softbank":"E409","google":"FE32A","image":"1f61d.png","sheet_x":32,"sheet_y":49,"short_name":"stuck_out_tongue_closed_eyes","short_names":["stuck_out_tongue_closed_eyes"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-tongue","sort_order":28,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DISAPPOINTED FACE","unified":"1F61E","non_qualified":null,"docomo":"E6F2","au":"EAC0","softbank":"E058","google":"FE323","image":"1f61e.png","sheet_x":32,"sheet_y":50,"short_name":"disappointed","short_names":["disappointed"],"text":":(","texts":["):",":(",":-("],"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":94,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WORRIED FACE","unified":"1F61F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f61f.png","sheet_x":32,"sheet_y":51,"short_name":"worried","short_names":["worried"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":75,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ANGRY FACE","unified":"1F620","non_qualified":null,"docomo":"E6F1","au":"E472","softbank":"E059","google":"FE320","image":"1f620.png","sheet_x":32,"sheet_y":52,"short_name":"angry","short_names":["angry"],"text":null,"texts":[">:(",">:-("],"category":"Smileys & Emotion","subcategory":"face-negative","sort_order":101,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"POUTING FACE","unified":"1F621","non_qualified":null,"docomo":"E724","au":"EB5D","softbank":"E416","google":"FE33D","image":"1f621.png","sheet_x":32,"sheet_y":53,"short_name":"rage","short_names":["rage"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-negative","sort_order":100,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CRYING FACE","unified":"1F622","non_qualified":null,"docomo":"E72E","au":"EB69","softbank":"E413","google":"FE339","image":"1f622.png","sheet_x":32,"sheet_y":54,"short_name":"cry","short_names":["cry"],"text":":'(","texts":[":'("],"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":89,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PERSEVERING FACE","unified":"1F623","non_qualified":null,"docomo":"E72B","au":"EAC2","softbank":"E406","google":"FE33C","image":"1f623.png","sheet_x":32,"sheet_y":55,"short_name":"persevere","short_names":["persevere"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":93,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE WITH LOOK OF TRIUMPH","unified":"1F624","non_qualified":null,"docomo":"E753","au":"EAC1","softbank":null,"google":"FE328","image":"1f624.png","sheet_x":32,"sheet_y":56,"short_name":"triumph","short_names":["triumph"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-negative","sort_order":99,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DISAPPOINTED BUT RELIEVED FACE","unified":"1F625","non_qualified":null,"docomo":"E723","au":"E5C6","softbank":"E401","google":"FE345","image":"1f625.png","sheet_x":32,"sheet_y":57,"short_name":"disappointed_relieved","short_names":["disappointed_relieved"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":88,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FROWNING FACE WITH OPEN MOUTH","unified":"1F626","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f626.png","sheet_x":32,"sheet_y":58,"short_name":"frowning","short_names":["frowning"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":84,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ANGUISHED FACE","unified":"1F627","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f627.png","sheet_x":32,"sheet_y":59,"short_name":"anguished","short_names":["anguished"],"text":null,"texts":["D:"],"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":85,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FEARFUL FACE","unified":"1F628","non_qualified":null,"docomo":"E757","au":"EAC6","softbank":"E40B","google":"FE33B","image":"1f628.png","sheet_x":32,"sheet_y":60,"short_name":"fearful","short_names":["fearful"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":86,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WEARY FACE","unified":"1F629","non_qualified":null,"docomo":"E6F3","au":"EB67","softbank":null,"google":"FE321","image":"1f629.png","sheet_x":33,"sheet_y":0,"short_name":"weary","short_names":["weary"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":96,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SLEEPY FACE","unified":"1F62A","non_qualified":null,"docomo":"E701","au":"EAC4","softbank":"E408","google":"FE342","image":"1f62a.png","sheet_x":33,"sheet_y":1,"short_name":"sleepy","short_names":["sleepy"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-sleepy","sort_order":52,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TIRED FACE","unified":"1F62B","non_qualified":null,"docomo":"E72B","au":"E474","softbank":null,"google":"FE346","image":"1f62b.png","sheet_x":33,"sheet_y":2,"short_name":"tired_face","short_names":["tired_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":97,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GRIMACING FACE","unified":"1F62C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f62c.png","sheet_x":33,"sheet_y":3,"short_name":"grimacing","short_names":["grimacing"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-neutral-skeptical","sort_order":47,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LOUDLY CRYING FACE","unified":"1F62D","non_qualified":null,"docomo":"E72D","au":"E473","softbank":"E411","google":"FE33A","image":"1f62d.png","sheet_x":33,"sheet_y":4,"short_name":"sob","short_names":["sob"],"text":":'(","texts":null,"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":90,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE EXHALING","unified":"1F62E-200D-1F4A8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f62e-200d-1f4a8.png","sheet_x":33,"sheet_y":5,"short_name":"face_exhaling","short_names":["face_exhaling"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-neutral-skeptical","sort_order":48,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"FACE WITH OPEN MOUTH","unified":"1F62E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f62e.png","sheet_x":33,"sheet_y":6,"short_name":"open_mouth","short_names":["open_mouth"],"text":null,"texts":[":o",":-o",":O",":-O"],"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":78,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HUSHED FACE","unified":"1F62F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f62f.png","sheet_x":33,"sheet_y":7,"short_name":"hushed","short_names":["hushed"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":79,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE WITH OPEN MOUTH AND COLD SWEAT","unified":"1F630","non_qualified":null,"docomo":"E723","au":"EACB","softbank":"E40F","google":"FE325","image":"1f630.png","sheet_x":33,"sheet_y":8,"short_name":"cold_sweat","short_names":["cold_sweat"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":87,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE SCREAMING IN FEAR","unified":"1F631","non_qualified":null,"docomo":"E757","au":"E5C5","softbank":"E107","google":"FE341","image":"1f631.png","sheet_x":33,"sheet_y":9,"short_name":"scream","short_names":["scream"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":91,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ASTONISHED FACE","unified":"1F632","non_qualified":null,"docomo":"E6F4","au":"EACA","softbank":"E410","google":"FE322","image":"1f632.png","sheet_x":33,"sheet_y":10,"short_name":"astonished","short_names":["astonished"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":80,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FLUSHED FACE","unified":"1F633","non_qualified":null,"docomo":"E72A","au":"EAC8","softbank":"E40D","google":"FE32F","image":"1f633.png","sheet_x":33,"sheet_y":11,"short_name":"flushed","short_names":["flushed"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":81,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SLEEPING FACE","unified":"1F634","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f634.png","sheet_x":33,"sheet_y":12,"short_name":"sleeping","short_names":["sleeping"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-sleepy","sort_order":54,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE WITH SPIRAL EYES","unified":"1F635-200D-1F4AB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f635-200d-1f4ab.png","sheet_x":33,"sheet_y":13,"short_name":"face_with_spiral_eyes","short_names":["face_with_spiral_eyes"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-unwell","sort_order":65,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"DIZZY FACE","unified":"1F635","non_qualified":null,"docomo":"E6F4","au":"E5AE","softbank":null,"google":"FE324","image":"1f635.png","sheet_x":33,"sheet_y":14,"short_name":"dizzy_face","short_names":["dizzy_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-unwell","sort_order":64,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE IN CLOUDS","unified":"1F636-200D-1F32B-FE0F","non_qualified":"1F636-200D-1F32B","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f636-200d-1f32b-fe0f.png","sheet_x":33,"sheet_y":15,"short_name":"face_in_clouds","short_names":["face_in_clouds"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-neutral-skeptical","sort_order":43,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"FACE WITHOUT MOUTH","unified":"1F636","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f636.png","sheet_x":33,"sheet_y":16,"short_name":"no_mouth","short_names":["no_mouth"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-neutral-skeptical","sort_order":41,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE WITH MEDICAL MASK","unified":"1F637","non_qualified":null,"docomo":null,"au":"EAC7","softbank":"E40C","google":"FE32E","image":"1f637.png","sheet_x":33,"sheet_y":17,"short_name":"mask","short_names":["mask"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-unwell","sort_order":55,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GRINNING CAT FACE WITH SMILING EYES","unified":"1F638","non_qualified":null,"docomo":"E753","au":"EB7F","softbank":null,"google":"FE349","image":"1f638.png","sheet_x":33,"sheet_y":18,"short_name":"smile_cat","short_names":["smile_cat"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"cat-face","sort_order":116,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CAT FACE WITH TEARS OF JOY","unified":"1F639","non_qualified":null,"docomo":"E72A","au":"EB63","softbank":null,"google":"FE34A","image":"1f639.png","sheet_x":33,"sheet_y":19,"short_name":"joy_cat","short_names":["joy_cat"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"cat-face","sort_order":117,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SMILING CAT FACE WITH OPEN MOUTH","unified":"1F63A","non_qualified":null,"docomo":"E6F0","au":"EB61","softbank":null,"google":"FE348","image":"1f63a.png","sheet_x":33,"sheet_y":20,"short_name":"smiley_cat","short_names":["smiley_cat"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"cat-face","sort_order":115,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SMILING CAT FACE WITH HEART-SHAPED EYES","unified":"1F63B","non_qualified":null,"docomo":"E726","au":"EB65","softbank":null,"google":"FE34C","image":"1f63b.png","sheet_x":33,"sheet_y":21,"short_name":"heart_eyes_cat","short_names":["heart_eyes_cat"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"cat-face","sort_order":118,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CAT FACE WITH WRY SMILE","unified":"1F63C","non_qualified":null,"docomo":"E753","au":"EB6A","softbank":null,"google":"FE34F","image":"1f63c.png","sheet_x":33,"sheet_y":22,"short_name":"smirk_cat","short_names":["smirk_cat"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"cat-face","sort_order":119,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"KISSING CAT FACE WITH CLOSED EYES","unified":"1F63D","non_qualified":null,"docomo":"E726","au":"EB60","softbank":null,"google":"FE34B","image":"1f63d.png","sheet_x":33,"sheet_y":23,"short_name":"kissing_cat","short_names":["kissing_cat"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"cat-face","sort_order":120,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"POUTING CAT FACE","unified":"1F63E","non_qualified":null,"docomo":"E724","au":"EB5E","softbank":null,"google":"FE34E","image":"1f63e.png","sheet_x":33,"sheet_y":24,"short_name":"pouting_cat","short_names":["pouting_cat"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"cat-face","sort_order":123,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CRYING CAT FACE","unified":"1F63F","non_qualified":null,"docomo":"E72E","au":"EB68","softbank":null,"google":"FE34D","image":"1f63f.png","sheet_x":33,"sheet_y":25,"short_name":"crying_cat_face","short_names":["crying_cat_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"cat-face","sort_order":122,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WEARY CAT FACE","unified":"1F640","non_qualified":null,"docomo":"E6F3","au":"EB66","softbank":null,"google":"FE350","image":"1f640.png","sheet_x":33,"sheet_y":26,"short_name":"scream_cat","short_names":["scream_cat"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"cat-face","sort_order":121,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SLIGHTLY FROWNING FACE","unified":"1F641","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f641.png","sheet_x":33,"sheet_y":27,"short_name":"slightly_frowning_face","short_names":["slightly_frowning_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":76,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SLIGHTLY SMILING FACE","unified":"1F642","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f642.png","sheet_x":33,"sheet_y":28,"short_name":"slightly_smiling_face","short_names":["slightly_smiling_face"],"text":null,"texts":[":)","(:",":-)"],"category":"Smileys & Emotion","subcategory":"face-smiling","sort_order":9,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"UPSIDE-DOWN FACE","unified":"1F643","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f643.png","sheet_x":33,"sheet_y":29,"short_name":"upside_down_face","short_names":["upside_down_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-smiling","sort_order":10,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE WITH ROLLING EYES","unified":"1F644","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f644.png","sheet_x":33,"sheet_y":30,"short_name":"face_with_rolling_eyes","short_names":["face_with_rolling_eyes"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-neutral-skeptical","sort_order":46,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WOMAN GESTURING NO","unified":"1F645-200D-2640-FE0F","non_qualified":"1F645-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f645-200d-2640-fe0f.png","sheet_x":33,"sheet_y":31,"short_name":"woman-gesturing-no","short_names":["woman-gesturing-no"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":259,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F645-1F3FB-200D-2640-FE0F","non_qualified":"1F645-1F3FB-200D-2640","image":"1f645-1f3fb-200d-2640-fe0f.png","sheet_x":33,"sheet_y":32,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F645-1F3FC-200D-2640-FE0F","non_qualified":"1F645-1F3FC-200D-2640","image":"1f645-1f3fc-200d-2640-fe0f.png","sheet_x":33,"sheet_y":33,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F645-1F3FD-200D-2640-FE0F","non_qualified":"1F645-1F3FD-200D-2640","image":"1f645-1f3fd-200d-2640-fe0f.png","sheet_x":33,"sheet_y":34,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F645-1F3FE-200D-2640-FE0F","non_qualified":"1F645-1F3FE-200D-2640","image":"1f645-1f3fe-200d-2640-fe0f.png","sheet_x":33,"sheet_y":35,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F645-1F3FF-200D-2640-FE0F","non_qualified":"1F645-1F3FF-200D-2640","image":"1f645-1f3ff-200d-2640-fe0f.png","sheet_x":33,"sheet_y":36,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F645"},{"name":"MAN GESTURING NO","unified":"1F645-200D-2642-FE0F","non_qualified":"1F645-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f645-200d-2642-fe0f.png","sheet_x":33,"sheet_y":37,"short_name":"man-gesturing-no","short_names":["man-gesturing-no"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":258,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F645-1F3FB-200D-2642-FE0F","non_qualified":"1F645-1F3FB-200D-2642","image":"1f645-1f3fb-200d-2642-fe0f.png","sheet_x":33,"sheet_y":38,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F645-1F3FC-200D-2642-FE0F","non_qualified":"1F645-1F3FC-200D-2642","image":"1f645-1f3fc-200d-2642-fe0f.png","sheet_x":33,"sheet_y":39,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F645-1F3FD-200D-2642-FE0F","non_qualified":"1F645-1F3FD-200D-2642","image":"1f645-1f3fd-200d-2642-fe0f.png","sheet_x":33,"sheet_y":40,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F645-1F3FE-200D-2642-FE0F","non_qualified":"1F645-1F3FE-200D-2642","image":"1f645-1f3fe-200d-2642-fe0f.png","sheet_x":33,"sheet_y":41,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F645-1F3FF-200D-2642-FE0F","non_qualified":"1F645-1F3FF-200D-2642","image":"1f645-1f3ff-200d-2642-fe0f.png","sheet_x":33,"sheet_y":42,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"FACE WITH NO GOOD GESTURE","unified":"1F645","non_qualified":null,"docomo":"E72F","au":"EAD7","softbank":"E423","google":"FE351","image":"1f645.png","sheet_x":33,"sheet_y":43,"short_name":"no_good","short_names":["no_good"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":257,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F645-1F3FB","non_qualified":null,"image":"1f645-1f3fb.png","sheet_x":33,"sheet_y":44,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F645-1F3FC","non_qualified":null,"image":"1f645-1f3fc.png","sheet_x":33,"sheet_y":45,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F645-1F3FD","non_qualified":null,"image":"1f645-1f3fd.png","sheet_x":33,"sheet_y":46,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F645-1F3FE","non_qualified":null,"image":"1f645-1f3fe.png","sheet_x":33,"sheet_y":47,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F645-1F3FF","non_qualified":null,"image":"1f645-1f3ff.png","sheet_x":33,"sheet_y":48,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F645-200D-2640-FE0F"},{"name":"WOMAN GESTURING OK","unified":"1F646-200D-2640-FE0F","non_qualified":"1F646-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f646-200d-2640-fe0f.png","sheet_x":33,"sheet_y":49,"short_name":"woman-gesturing-ok","short_names":["woman-gesturing-ok"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":262,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F646-1F3FB-200D-2640-FE0F","non_qualified":"1F646-1F3FB-200D-2640","image":"1f646-1f3fb-200d-2640-fe0f.png","sheet_x":33,"sheet_y":50,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F646-1F3FC-200D-2640-FE0F","non_qualified":"1F646-1F3FC-200D-2640","image":"1f646-1f3fc-200d-2640-fe0f.png","sheet_x":33,"sheet_y":51,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F646-1F3FD-200D-2640-FE0F","non_qualified":"1F646-1F3FD-200D-2640","image":"1f646-1f3fd-200d-2640-fe0f.png","sheet_x":33,"sheet_y":52,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F646-1F3FE-200D-2640-FE0F","non_qualified":"1F646-1F3FE-200D-2640","image":"1f646-1f3fe-200d-2640-fe0f.png","sheet_x":33,"sheet_y":53,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F646-1F3FF-200D-2640-FE0F","non_qualified":"1F646-1F3FF-200D-2640","image":"1f646-1f3ff-200d-2640-fe0f.png","sheet_x":33,"sheet_y":54,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F646"},{"name":"MAN GESTURING OK","unified":"1F646-200D-2642-FE0F","non_qualified":"1F646-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f646-200d-2642-fe0f.png","sheet_x":33,"sheet_y":55,"short_name":"man-gesturing-ok","short_names":["man-gesturing-ok"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":261,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F646-1F3FB-200D-2642-FE0F","non_qualified":"1F646-1F3FB-200D-2642","image":"1f646-1f3fb-200d-2642-fe0f.png","sheet_x":33,"sheet_y":56,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F646-1F3FC-200D-2642-FE0F","non_qualified":"1F646-1F3FC-200D-2642","image":"1f646-1f3fc-200d-2642-fe0f.png","sheet_x":33,"sheet_y":57,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F646-1F3FD-200D-2642-FE0F","non_qualified":"1F646-1F3FD-200D-2642","image":"1f646-1f3fd-200d-2642-fe0f.png","sheet_x":33,"sheet_y":58,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F646-1F3FE-200D-2642-FE0F","non_qualified":"1F646-1F3FE-200D-2642","image":"1f646-1f3fe-200d-2642-fe0f.png","sheet_x":33,"sheet_y":59,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F646-1F3FF-200D-2642-FE0F","non_qualified":"1F646-1F3FF-200D-2642","image":"1f646-1f3ff-200d-2642-fe0f.png","sheet_x":33,"sheet_y":60,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"FACE WITH OK GESTURE","unified":"1F646","non_qualified":null,"docomo":"E70B","au":"EAD8","softbank":"E424","google":"FE352","image":"1f646.png","sheet_x":34,"sheet_y":0,"short_name":"ok_woman","short_names":["ok_woman"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":260,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F646-1F3FB","non_qualified":null,"image":"1f646-1f3fb.png","sheet_x":34,"sheet_y":1,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F646-1F3FC","non_qualified":null,"image":"1f646-1f3fc.png","sheet_x":34,"sheet_y":2,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F646-1F3FD","non_qualified":null,"image":"1f646-1f3fd.png","sheet_x":34,"sheet_y":3,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F646-1F3FE","non_qualified":null,"image":"1f646-1f3fe.png","sheet_x":34,"sheet_y":4,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F646-1F3FF","non_qualified":null,"image":"1f646-1f3ff.png","sheet_x":34,"sheet_y":5,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F646-200D-2640-FE0F"},{"name":"WOMAN BOWING","unified":"1F647-200D-2640-FE0F","non_qualified":"1F647-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f647-200d-2640-fe0f.png","sheet_x":34,"sheet_y":6,"short_name":"woman-bowing","short_names":["woman-bowing"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":274,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F647-1F3FB-200D-2640-FE0F","non_qualified":"1F647-1F3FB-200D-2640","image":"1f647-1f3fb-200d-2640-fe0f.png","sheet_x":34,"sheet_y":7,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F647-1F3FC-200D-2640-FE0F","non_qualified":"1F647-1F3FC-200D-2640","image":"1f647-1f3fc-200d-2640-fe0f.png","sheet_x":34,"sheet_y":8,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F647-1F3FD-200D-2640-FE0F","non_qualified":"1F647-1F3FD-200D-2640","image":"1f647-1f3fd-200d-2640-fe0f.png","sheet_x":34,"sheet_y":9,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F647-1F3FE-200D-2640-FE0F","non_qualified":"1F647-1F3FE-200D-2640","image":"1f647-1f3fe-200d-2640-fe0f.png","sheet_x":34,"sheet_y":10,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F647-1F3FF-200D-2640-FE0F","non_qualified":"1F647-1F3FF-200D-2640","image":"1f647-1f3ff-200d-2640-fe0f.png","sheet_x":34,"sheet_y":11,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN BOWING","unified":"1F647-200D-2642-FE0F","non_qualified":"1F647-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f647-200d-2642-fe0f.png","sheet_x":34,"sheet_y":12,"short_name":"man-bowing","short_names":["man-bowing"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":273,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F647-1F3FB-200D-2642-FE0F","non_qualified":"1F647-1F3FB-200D-2642","image":"1f647-1f3fb-200d-2642-fe0f.png","sheet_x":34,"sheet_y":13,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F647-1F3FC-200D-2642-FE0F","non_qualified":"1F647-1F3FC-200D-2642","image":"1f647-1f3fc-200d-2642-fe0f.png","sheet_x":34,"sheet_y":14,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F647-1F3FD-200D-2642-FE0F","non_qualified":"1F647-1F3FD-200D-2642","image":"1f647-1f3fd-200d-2642-fe0f.png","sheet_x":34,"sheet_y":15,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F647-1F3FE-200D-2642-FE0F","non_qualified":"1F647-1F3FE-200D-2642","image":"1f647-1f3fe-200d-2642-fe0f.png","sheet_x":34,"sheet_y":16,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F647-1F3FF-200D-2642-FE0F","non_qualified":"1F647-1F3FF-200D-2642","image":"1f647-1f3ff-200d-2642-fe0f.png","sheet_x":34,"sheet_y":17,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F647"},{"name":"PERSON BOWING DEEPLY","unified":"1F647","non_qualified":null,"docomo":null,"au":"EAD9","softbank":"E426","google":"FE353","image":"1f647.png","sheet_x":34,"sheet_y":18,"short_name":"bow","short_names":["bow"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":272,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F647-1F3FB","non_qualified":null,"image":"1f647-1f3fb.png","sheet_x":34,"sheet_y":19,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F647-1F3FC","non_qualified":null,"image":"1f647-1f3fc.png","sheet_x":34,"sheet_y":20,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F647-1F3FD","non_qualified":null,"image":"1f647-1f3fd.png","sheet_x":34,"sheet_y":21,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F647-1F3FE","non_qualified":null,"image":"1f647-1f3fe.png","sheet_x":34,"sheet_y":22,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F647-1F3FF","non_qualified":null,"image":"1f647-1f3ff.png","sheet_x":34,"sheet_y":23,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F647-200D-2642-FE0F"},{"name":"SEE-NO-EVIL MONKEY","unified":"1F648","non_qualified":null,"docomo":null,"au":"EB50","softbank":null,"google":"FE354","image":"1f648.png","sheet_x":34,"sheet_y":24,"short_name":"see_no_evil","short_names":["see_no_evil"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"monkey-face","sort_order":124,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HEAR-NO-EVIL MONKEY","unified":"1F649","non_qualified":null,"docomo":null,"au":"EB52","softbank":null,"google":"FE356","image":"1f649.png","sheet_x":34,"sheet_y":25,"short_name":"hear_no_evil","short_names":["hear_no_evil"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"monkey-face","sort_order":125,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SPEAK-NO-EVIL MONKEY","unified":"1F64A","non_qualified":null,"docomo":null,"au":"EB51","softbank":null,"google":"FE355","image":"1f64a.png","sheet_x":34,"sheet_y":26,"short_name":"speak_no_evil","short_names":["speak_no_evil"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"monkey-face","sort_order":126,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WOMAN RAISING HAND","unified":"1F64B-200D-2640-FE0F","non_qualified":"1F64B-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f64b-200d-2640-fe0f.png","sheet_x":34,"sheet_y":27,"short_name":"woman-raising-hand","short_names":["woman-raising-hand"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":268,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F64B-1F3FB-200D-2640-FE0F","non_qualified":"1F64B-1F3FB-200D-2640","image":"1f64b-1f3fb-200d-2640-fe0f.png","sheet_x":34,"sheet_y":28,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F64B-1F3FC-200D-2640-FE0F","non_qualified":"1F64B-1F3FC-200D-2640","image":"1f64b-1f3fc-200d-2640-fe0f.png","sheet_x":34,"sheet_y":29,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F64B-1F3FD-200D-2640-FE0F","non_qualified":"1F64B-1F3FD-200D-2640","image":"1f64b-1f3fd-200d-2640-fe0f.png","sheet_x":34,"sheet_y":30,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F64B-1F3FE-200D-2640-FE0F","non_qualified":"1F64B-1F3FE-200D-2640","image":"1f64b-1f3fe-200d-2640-fe0f.png","sheet_x":34,"sheet_y":31,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F64B-1F3FF-200D-2640-FE0F","non_qualified":"1F64B-1F3FF-200D-2640","image":"1f64b-1f3ff-200d-2640-fe0f.png","sheet_x":34,"sheet_y":32,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F64B"},{"name":"MAN RAISING HAND","unified":"1F64B-200D-2642-FE0F","non_qualified":"1F64B-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f64b-200d-2642-fe0f.png","sheet_x":34,"sheet_y":33,"short_name":"man-raising-hand","short_names":["man-raising-hand"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":267,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F64B-1F3FB-200D-2642-FE0F","non_qualified":"1F64B-1F3FB-200D-2642","image":"1f64b-1f3fb-200d-2642-fe0f.png","sheet_x":34,"sheet_y":34,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F64B-1F3FC-200D-2642-FE0F","non_qualified":"1F64B-1F3FC-200D-2642","image":"1f64b-1f3fc-200d-2642-fe0f.png","sheet_x":34,"sheet_y":35,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F64B-1F3FD-200D-2642-FE0F","non_qualified":"1F64B-1F3FD-200D-2642","image":"1f64b-1f3fd-200d-2642-fe0f.png","sheet_x":34,"sheet_y":36,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F64B-1F3FE-200D-2642-FE0F","non_qualified":"1F64B-1F3FE-200D-2642","image":"1f64b-1f3fe-200d-2642-fe0f.png","sheet_x":34,"sheet_y":37,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F64B-1F3FF-200D-2642-FE0F","non_qualified":"1F64B-1F3FF-200D-2642","image":"1f64b-1f3ff-200d-2642-fe0f.png","sheet_x":34,"sheet_y":38,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"HAPPY PERSON RAISING ONE HAND","unified":"1F64B","non_qualified":null,"docomo":null,"au":"EB85","softbank":null,"google":"FE357","image":"1f64b.png","sheet_x":34,"sheet_y":39,"short_name":"raising_hand","short_names":["raising_hand"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":266,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F64B-1F3FB","non_qualified":null,"image":"1f64b-1f3fb.png","sheet_x":34,"sheet_y":40,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F64B-1F3FC","non_qualified":null,"image":"1f64b-1f3fc.png","sheet_x":34,"sheet_y":41,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F64B-1F3FD","non_qualified":null,"image":"1f64b-1f3fd.png","sheet_x":34,"sheet_y":42,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F64B-1F3FE","non_qualified":null,"image":"1f64b-1f3fe.png","sheet_x":34,"sheet_y":43,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F64B-1F3FF","non_qualified":null,"image":"1f64b-1f3ff.png","sheet_x":34,"sheet_y":44,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F64B-200D-2640-FE0F"},{"name":"PERSON RAISING BOTH HANDS IN CELEBRATION","unified":"1F64C","non_qualified":null,"docomo":null,"au":"EB86","softbank":"E427","google":"FE358","image":"1f64c.png","sheet_x":34,"sheet_y":45,"short_name":"raised_hands","short_names":["raised_hands"],"text":null,"texts":null,"category":"People & Body","subcategory":"hands","sort_order":196,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F64C-1F3FB","non_qualified":null,"image":"1f64c-1f3fb.png","sheet_x":34,"sheet_y":46,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F64C-1F3FC","non_qualified":null,"image":"1f64c-1f3fc.png","sheet_x":34,"sheet_y":47,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F64C-1F3FD","non_qualified":null,"image":"1f64c-1f3fd.png","sheet_x":34,"sheet_y":48,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F64C-1F3FE","non_qualified":null,"image":"1f64c-1f3fe.png","sheet_x":34,"sheet_y":49,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F64C-1F3FF","non_qualified":null,"image":"1f64c-1f3ff.png","sheet_x":34,"sheet_y":50,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN FROWNING","unified":"1F64D-200D-2640-FE0F","non_qualified":"1F64D-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f64d-200d-2640-fe0f.png","sheet_x":34,"sheet_y":51,"short_name":"woman-frowning","short_names":["woman-frowning"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":253,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F64D-1F3FB-200D-2640-FE0F","non_qualified":"1F64D-1F3FB-200D-2640","image":"1f64d-1f3fb-200d-2640-fe0f.png","sheet_x":34,"sheet_y":52,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F64D-1F3FC-200D-2640-FE0F","non_qualified":"1F64D-1F3FC-200D-2640","image":"1f64d-1f3fc-200d-2640-fe0f.png","sheet_x":34,"sheet_y":53,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F64D-1F3FD-200D-2640-FE0F","non_qualified":"1F64D-1F3FD-200D-2640","image":"1f64d-1f3fd-200d-2640-fe0f.png","sheet_x":34,"sheet_y":54,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F64D-1F3FE-200D-2640-FE0F","non_qualified":"1F64D-1F3FE-200D-2640","image":"1f64d-1f3fe-200d-2640-fe0f.png","sheet_x":34,"sheet_y":55,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F64D-1F3FF-200D-2640-FE0F","non_qualified":"1F64D-1F3FF-200D-2640","image":"1f64d-1f3ff-200d-2640-fe0f.png","sheet_x":34,"sheet_y":56,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F64D"},{"name":"MAN FROWNING","unified":"1F64D-200D-2642-FE0F","non_qualified":"1F64D-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f64d-200d-2642-fe0f.png","sheet_x":34,"sheet_y":57,"short_name":"man-frowning","short_names":["man-frowning"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":252,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F64D-1F3FB-200D-2642-FE0F","non_qualified":"1F64D-1F3FB-200D-2642","image":"1f64d-1f3fb-200d-2642-fe0f.png","sheet_x":34,"sheet_y":58,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F64D-1F3FC-200D-2642-FE0F","non_qualified":"1F64D-1F3FC-200D-2642","image":"1f64d-1f3fc-200d-2642-fe0f.png","sheet_x":34,"sheet_y":59,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F64D-1F3FD-200D-2642-FE0F","non_qualified":"1F64D-1F3FD-200D-2642","image":"1f64d-1f3fd-200d-2642-fe0f.png","sheet_x":34,"sheet_y":60,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F64D-1F3FE-200D-2642-FE0F","non_qualified":"1F64D-1F3FE-200D-2642","image":"1f64d-1f3fe-200d-2642-fe0f.png","sheet_x":35,"sheet_y":0,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F64D-1F3FF-200D-2642-FE0F","non_qualified":"1F64D-1F3FF-200D-2642","image":"1f64d-1f3ff-200d-2642-fe0f.png","sheet_x":35,"sheet_y":1,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"PERSON FROWNING","unified":"1F64D","non_qualified":null,"docomo":"E6F3","au":"EB87","softbank":null,"google":"FE359","image":"1f64d.png","sheet_x":35,"sheet_y":2,"short_name":"person_frowning","short_names":["person_frowning"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":251,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F64D-1F3FB","non_qualified":null,"image":"1f64d-1f3fb.png","sheet_x":35,"sheet_y":3,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F64D-1F3FC","non_qualified":null,"image":"1f64d-1f3fc.png","sheet_x":35,"sheet_y":4,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F64D-1F3FD","non_qualified":null,"image":"1f64d-1f3fd.png","sheet_x":35,"sheet_y":5,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F64D-1F3FE","non_qualified":null,"image":"1f64d-1f3fe.png","sheet_x":35,"sheet_y":6,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F64D-1F3FF","non_qualified":null,"image":"1f64d-1f3ff.png","sheet_x":35,"sheet_y":7,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F64D-200D-2640-FE0F"},{"name":"WOMAN POUTING","unified":"1F64E-200D-2640-FE0F","non_qualified":"1F64E-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f64e-200d-2640-fe0f.png","sheet_x":35,"sheet_y":8,"short_name":"woman-pouting","short_names":["woman-pouting"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":256,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F64E-1F3FB-200D-2640-FE0F","non_qualified":"1F64E-1F3FB-200D-2640","image":"1f64e-1f3fb-200d-2640-fe0f.png","sheet_x":35,"sheet_y":9,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F64E-1F3FC-200D-2640-FE0F","non_qualified":"1F64E-1F3FC-200D-2640","image":"1f64e-1f3fc-200d-2640-fe0f.png","sheet_x":35,"sheet_y":10,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F64E-1F3FD-200D-2640-FE0F","non_qualified":"1F64E-1F3FD-200D-2640","image":"1f64e-1f3fd-200d-2640-fe0f.png","sheet_x":35,"sheet_y":11,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F64E-1F3FE-200D-2640-FE0F","non_qualified":"1F64E-1F3FE-200D-2640","image":"1f64e-1f3fe-200d-2640-fe0f.png","sheet_x":35,"sheet_y":12,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F64E-1F3FF-200D-2640-FE0F","non_qualified":"1F64E-1F3FF-200D-2640","image":"1f64e-1f3ff-200d-2640-fe0f.png","sheet_x":35,"sheet_y":13,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F64E"},{"name":"MAN POUTING","unified":"1F64E-200D-2642-FE0F","non_qualified":"1F64E-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f64e-200d-2642-fe0f.png","sheet_x":35,"sheet_y":14,"short_name":"man-pouting","short_names":["man-pouting"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":255,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F64E-1F3FB-200D-2642-FE0F","non_qualified":"1F64E-1F3FB-200D-2642","image":"1f64e-1f3fb-200d-2642-fe0f.png","sheet_x":35,"sheet_y":15,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F64E-1F3FC-200D-2642-FE0F","non_qualified":"1F64E-1F3FC-200D-2642","image":"1f64e-1f3fc-200d-2642-fe0f.png","sheet_x":35,"sheet_y":16,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F64E-1F3FD-200D-2642-FE0F","non_qualified":"1F64E-1F3FD-200D-2642","image":"1f64e-1f3fd-200d-2642-fe0f.png","sheet_x":35,"sheet_y":17,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F64E-1F3FE-200D-2642-FE0F","non_qualified":"1F64E-1F3FE-200D-2642","image":"1f64e-1f3fe-200d-2642-fe0f.png","sheet_x":35,"sheet_y":18,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F64E-1F3FF-200D-2642-FE0F","non_qualified":"1F64E-1F3FF-200D-2642","image":"1f64e-1f3ff-200d-2642-fe0f.png","sheet_x":35,"sheet_y":19,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"PERSON WITH POUTING FACE","unified":"1F64E","non_qualified":null,"docomo":"E6F1","au":"EB88","softbank":null,"google":"FE35A","image":"1f64e.png","sheet_x":35,"sheet_y":20,"short_name":"person_with_pouting_face","short_names":["person_with_pouting_face"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":254,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F64E-1F3FB","non_qualified":null,"image":"1f64e-1f3fb.png","sheet_x":35,"sheet_y":21,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F64E-1F3FC","non_qualified":null,"image":"1f64e-1f3fc.png","sheet_x":35,"sheet_y":22,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F64E-1F3FD","non_qualified":null,"image":"1f64e-1f3fd.png","sheet_x":35,"sheet_y":23,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F64E-1F3FE","non_qualified":null,"image":"1f64e-1f3fe.png","sheet_x":35,"sheet_y":24,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F64E-1F3FF","non_qualified":null,"image":"1f64e-1f3ff.png","sheet_x":35,"sheet_y":25,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F64E-200D-2640-FE0F"},{"name":"PERSON WITH FOLDED HANDS","unified":"1F64F","non_qualified":null,"docomo":null,"au":"EAD2","softbank":"E41D","google":"FE35B","image":"1f64f.png","sheet_x":35,"sheet_y":26,"short_name":"pray","short_names":["pray"],"text":null,"texts":null,"category":"People & Body","subcategory":"hands","sort_order":201,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F64F-1F3FB","non_qualified":null,"image":"1f64f-1f3fb.png","sheet_x":35,"sheet_y":27,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F64F-1F3FC","non_qualified":null,"image":"1f64f-1f3fc.png","sheet_x":35,"sheet_y":28,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F64F-1F3FD","non_qualified":null,"image":"1f64f-1f3fd.png","sheet_x":35,"sheet_y":29,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F64F-1F3FE","non_qualified":null,"image":"1f64f-1f3fe.png","sheet_x":35,"sheet_y":30,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F64F-1F3FF","non_qualified":null,"image":"1f64f-1f3ff.png","sheet_x":35,"sheet_y":31,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"ROCKET","unified":"1F680","non_qualified":null,"docomo":null,"au":"E5C8","softbank":"E10D","google":"FE7ED","image":"1f680.png","sheet_x":35,"sheet_y":32,"short_name":"rocket","short_names":["rocket"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-air","sort_order":942,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HELICOPTER","unified":"1F681","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f681.png","sheet_x":35,"sheet_y":33,"short_name":"helicopter","short_names":["helicopter"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-air","sort_order":937,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"STEAM LOCOMOTIVE","unified":"1F682","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f682.png","sheet_x":35,"sheet_y":34,"short_name":"steam_locomotive","short_names":["steam_locomotive"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":872,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RAILWAY CAR","unified":"1F683","non_qualified":null,"docomo":"E65B","au":"E4B5","softbank":"E01E","google":"FE7DF","image":"1f683.png","sheet_x":35,"sheet_y":35,"short_name":"railway_car","short_names":["railway_car"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":873,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HIGH-SPEED TRAIN","unified":"1F684","non_qualified":null,"docomo":"E65D","au":"E4B0","softbank":"E435","google":"FE7E2","image":"1f684.png","sheet_x":35,"sheet_y":36,"short_name":"bullettrain_side","short_names":["bullettrain_side"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":874,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HIGH-SPEED TRAIN WITH BULLET NOSE","unified":"1F685","non_qualified":null,"docomo":"E65D","au":"E4B0","softbank":"E01F","google":"FE7E3","image":"1f685.png","sheet_x":35,"sheet_y":37,"short_name":"bullettrain_front","short_names":["bullettrain_front"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":875,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TRAIN","unified":"1F686","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f686.png","sheet_x":35,"sheet_y":38,"short_name":"train2","short_names":["train2"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":876,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"METRO","unified":"1F687","non_qualified":null,"docomo":"E65C","au":"E5BC","softbank":"E434","google":"FE7E0","image":"1f687.png","sheet_x":35,"sheet_y":39,"short_name":"metro","short_names":["metro"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":877,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LIGHT RAIL","unified":"1F688","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f688.png","sheet_x":35,"sheet_y":40,"short_name":"light_rail","short_names":["light_rail"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":878,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"STATION","unified":"1F689","non_qualified":null,"docomo":null,"au":"EB6D","softbank":"E039","google":"FE7EC","image":"1f689.png","sheet_x":35,"sheet_y":41,"short_name":"station","short_names":["station"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":879,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TRAM","unified":"1F68A","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f68a.png","sheet_x":35,"sheet_y":42,"short_name":"tram","short_names":["tram"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":880,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TRAM CAR","unified":"1F68B","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f68b.png","sheet_x":35,"sheet_y":43,"short_name":"train","short_names":["train"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":883,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BUS","unified":"1F68C","non_qualified":null,"docomo":"E660","au":"E4AF","softbank":"E159","google":"FE7E6","image":"1f68c.png","sheet_x":35,"sheet_y":44,"short_name":"bus","short_names":["bus"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":884,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ONCOMING BUS","unified":"1F68D","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f68d.png","sheet_x":35,"sheet_y":45,"short_name":"oncoming_bus","short_names":["oncoming_bus"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":885,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TROLLEYBUS","unified":"1F68E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f68e.png","sheet_x":35,"sheet_y":46,"short_name":"trolleybus","short_names":["trolleybus"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":886,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BUS STOP","unified":"1F68F","non_qualified":null,"docomo":null,"au":"E4A7","softbank":"E150","google":"FE7E7","image":"1f68f.png","sheet_x":35,"sheet_y":47,"short_name":"busstop","short_names":["busstop"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":911,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MINIBUS","unified":"1F690","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f690.png","sheet_x":35,"sheet_y":48,"short_name":"minibus","short_names":["minibus"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":887,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"AMBULANCE","unified":"1F691","non_qualified":null,"docomo":null,"au":"EAE0","softbank":"E431","google":"FE7F3","image":"1f691.png","sheet_x":35,"sheet_y":49,"short_name":"ambulance","short_names":["ambulance"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":888,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FIRE ENGINE","unified":"1F692","non_qualified":null,"docomo":null,"au":"EADF","softbank":"E430","google":"FE7F2","image":"1f692.png","sheet_x":35,"sheet_y":50,"short_name":"fire_engine","short_names":["fire_engine"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":889,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"POLICE CAR","unified":"1F693","non_qualified":null,"docomo":null,"au":"EAE1","softbank":"E432","google":"FE7F4","image":"1f693.png","sheet_x":35,"sheet_y":51,"short_name":"police_car","short_names":["police_car"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":890,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ONCOMING POLICE CAR","unified":"1F694","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f694.png","sheet_x":35,"sheet_y":52,"short_name":"oncoming_police_car","short_names":["oncoming_police_car"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":891,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TAXI","unified":"1F695","non_qualified":null,"docomo":"E65E","au":"E4B1","softbank":"E15A","google":"FE7EF","image":"1f695.png","sheet_x":35,"sheet_y":53,"short_name":"taxi","short_names":["taxi"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":892,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ONCOMING TAXI","unified":"1F696","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f696.png","sheet_x":35,"sheet_y":54,"short_name":"oncoming_taxi","short_names":["oncoming_taxi"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":893,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"AUTOMOBILE","unified":"1F697","non_qualified":null,"docomo":"E65E","au":"E4B1","softbank":"E01B","google":"FE7E4","image":"1f697.png","sheet_x":35,"sheet_y":55,"short_name":"car","short_names":["car","red_car"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":894,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ONCOMING AUTOMOBILE","unified":"1F698","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f698.png","sheet_x":35,"sheet_y":56,"short_name":"oncoming_automobile","short_names":["oncoming_automobile"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":895,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RECREATIONAL VEHICLE","unified":"1F699","non_qualified":null,"docomo":"E65F","au":"E4B1","softbank":"E42E","google":"FE7E5","image":"1f699.png","sheet_x":35,"sheet_y":57,"short_name":"blue_car","short_names":["blue_car"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":896,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DELIVERY TRUCK","unified":"1F69A","non_qualified":null,"docomo":null,"au":"E4B2","softbank":"E42F","google":"FE7F1","image":"1f69a.png","sheet_x":35,"sheet_y":58,"short_name":"truck","short_names":["truck"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":898,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ARTICULATED LORRY","unified":"1F69B","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f69b.png","sheet_x":35,"sheet_y":59,"short_name":"articulated_lorry","short_names":["articulated_lorry"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":899,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TRACTOR","unified":"1F69C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f69c.png","sheet_x":35,"sheet_y":60,"short_name":"tractor","short_names":["tractor"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":900,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MONORAIL","unified":"1F69D","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f69d.png","sheet_x":36,"sheet_y":0,"short_name":"monorail","short_names":["monorail"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":881,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MOUNTAIN RAILWAY","unified":"1F69E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f69e.png","sheet_x":36,"sheet_y":1,"short_name":"mountain_railway","short_names":["mountain_railway"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":882,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SUSPENSION RAILWAY","unified":"1F69F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f69f.png","sheet_x":36,"sheet_y":2,"short_name":"suspension_railway","short_names":["suspension_railway"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-air","sort_order":938,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MOUNTAIN CABLEWAY","unified":"1F6A0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6a0.png","sheet_x":36,"sheet_y":3,"short_name":"mountain_cableway","short_names":["mountain_cableway"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-air","sort_order":939,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"AERIAL TRAMWAY","unified":"1F6A1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6a1.png","sheet_x":36,"sheet_y":4,"short_name":"aerial_tramway","short_names":["aerial_tramway"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-air","sort_order":940,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SHIP","unified":"1F6A2","non_qualified":null,"docomo":"E661","au":"EA82","softbank":"E202","google":"FE7E8","image":"1f6a2.png","sheet_x":36,"sheet_y":5,"short_name":"ship","short_names":["ship"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-water","sort_order":930,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WOMAN ROWING BOAT","unified":"1F6A3-200D-2640-FE0F","non_qualified":"1F6A3-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6a3-200d-2640-fe0f.png","sheet_x":36,"sheet_y":6,"short_name":"woman-rowing-boat","short_names":["woman-rowing-boat"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":446,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F6A3-1F3FB-200D-2640-FE0F","non_qualified":"1F6A3-1F3FB-200D-2640","image":"1f6a3-1f3fb-200d-2640-fe0f.png","sheet_x":36,"sheet_y":7,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F6A3-1F3FC-200D-2640-FE0F","non_qualified":"1F6A3-1F3FC-200D-2640","image":"1f6a3-1f3fc-200d-2640-fe0f.png","sheet_x":36,"sheet_y":8,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F6A3-1F3FD-200D-2640-FE0F","non_qualified":"1F6A3-1F3FD-200D-2640","image":"1f6a3-1f3fd-200d-2640-fe0f.png","sheet_x":36,"sheet_y":9,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F6A3-1F3FE-200D-2640-FE0F","non_qualified":"1F6A3-1F3FE-200D-2640","image":"1f6a3-1f3fe-200d-2640-fe0f.png","sheet_x":36,"sheet_y":10,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F6A3-1F3FF-200D-2640-FE0F","non_qualified":"1F6A3-1F3FF-200D-2640","image":"1f6a3-1f3ff-200d-2640-fe0f.png","sheet_x":36,"sheet_y":11,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN ROWING BOAT","unified":"1F6A3-200D-2642-FE0F","non_qualified":"1F6A3-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6a3-200d-2642-fe0f.png","sheet_x":36,"sheet_y":12,"short_name":"man-rowing-boat","short_names":["man-rowing-boat"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":445,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F6A3-1F3FB-200D-2642-FE0F","non_qualified":"1F6A3-1F3FB-200D-2642","image":"1f6a3-1f3fb-200d-2642-fe0f.png","sheet_x":36,"sheet_y":13,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F6A3-1F3FC-200D-2642-FE0F","non_qualified":"1F6A3-1F3FC-200D-2642","image":"1f6a3-1f3fc-200d-2642-fe0f.png","sheet_x":36,"sheet_y":14,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F6A3-1F3FD-200D-2642-FE0F","non_qualified":"1F6A3-1F3FD-200D-2642","image":"1f6a3-1f3fd-200d-2642-fe0f.png","sheet_x":36,"sheet_y":15,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F6A3-1F3FE-200D-2642-FE0F","non_qualified":"1F6A3-1F3FE-200D-2642","image":"1f6a3-1f3fe-200d-2642-fe0f.png","sheet_x":36,"sheet_y":16,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F6A3-1F3FF-200D-2642-FE0F","non_qualified":"1F6A3-1F3FF-200D-2642","image":"1f6a3-1f3ff-200d-2642-fe0f.png","sheet_x":36,"sheet_y":17,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F6A3"},{"name":"ROWBOAT","unified":"1F6A3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6a3.png","sheet_x":36,"sheet_y":18,"short_name":"rowboat","short_names":["rowboat"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":444,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F6A3-1F3FB","non_qualified":null,"image":"1f6a3-1f3fb.png","sheet_x":36,"sheet_y":19,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F6A3-1F3FC","non_qualified":null,"image":"1f6a3-1f3fc.png","sheet_x":36,"sheet_y":20,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F6A3-1F3FD","non_qualified":null,"image":"1f6a3-1f3fd.png","sheet_x":36,"sheet_y":21,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F6A3-1F3FE","non_qualified":null,"image":"1f6a3-1f3fe.png","sheet_x":36,"sheet_y":22,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F6A3-1F3FF","non_qualified":null,"image":"1f6a3-1f3ff.png","sheet_x":36,"sheet_y":23,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F6A3-200D-2642-FE0F"},{"name":"SPEEDBOAT","unified":"1F6A4","non_qualified":null,"docomo":"E6A3","au":"E4B4","softbank":"E135","google":"FE7EE","image":"1f6a4.png","sheet_x":36,"sheet_y":24,"short_name":"speedboat","short_names":["speedboat"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-water","sort_order":926,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HORIZONTAL TRAFFIC LIGHT","unified":"1F6A5","non_qualified":null,"docomo":"E66D","au":"E46A","softbank":"E14E","google":"FE7F7","image":"1f6a5.png","sheet_x":36,"sheet_y":25,"short_name":"traffic_light","short_names":["traffic_light"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":918,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"VERTICAL TRAFFIC LIGHT","unified":"1F6A6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6a6.png","sheet_x":36,"sheet_y":26,"short_name":"vertical_traffic_light","short_names":["vertical_traffic_light"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":919,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CONSTRUCTION SIGN","unified":"1F6A7","non_qualified":null,"docomo":null,"au":"E5D7","softbank":"E137","google":"FE7F8","image":"1f6a7.png","sheet_x":36,"sheet_y":27,"short_name":"construction","short_names":["construction"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":921,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"POLICE CARS REVOLVING LIGHT","unified":"1F6A8","non_qualified":null,"docomo":null,"au":"EB73","softbank":null,"google":"FE7F9","image":"1f6a8.png","sheet_x":36,"sheet_y":28,"short_name":"rotating_light","short_names":["rotating_light"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":917,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TRIANGULAR FLAG ON POST","unified":"1F6A9","non_qualified":null,"docomo":"E6DE","au":"EB2C","softbank":null,"google":"FEB22","image":"1f6a9.png","sheet_x":36,"sheet_y":29,"short_name":"triangular_flag_on_post","short_names":["triangular_flag_on_post"],"text":null,"texts":null,"category":"Flags","subcategory":"flag","sort_order":1587,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DOOR","unified":"1F6AA","non_qualified":null,"docomo":"E714","au":null,"softbank":null,"google":"FE4F3","image":"1f6aa.png","sheet_x":36,"sheet_y":30,"short_name":"door","short_names":["door"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1333,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NO ENTRY SIGN","unified":"1F6AB","non_qualified":null,"docomo":"E738","au":"E541","softbank":null,"google":"FEB48","image":"1f6ab.png","sheet_x":36,"sheet_y":31,"short_name":"no_entry_sign","short_names":["no_entry_sign"],"text":null,"texts":null,"category":"Symbols","subcategory":"warning","sort_order":1381,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SMOKING SYMBOL","unified":"1F6AC","non_qualified":null,"docomo":"E67F","au":"E47D","softbank":"E30E","google":"FEB1E","image":"1f6ac.png","sheet_x":36,"sheet_y":32,"short_name":"smoking","short_names":["smoking"],"text":null,"texts":null,"category":"Objects","subcategory":"other-object","sort_order":1358,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NO SMOKING SYMBOL","unified":"1F6AD","non_qualified":null,"docomo":"E680","au":"E47E","softbank":"E208","google":"FEB1F","image":"1f6ad.png","sheet_x":36,"sheet_y":33,"short_name":"no_smoking","short_names":["no_smoking"],"text":null,"texts":null,"category":"Symbols","subcategory":"warning","sort_order":1383,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PUT LITTER IN ITS PLACE SYMBOL","unified":"1F6AE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6ae.png","sheet_x":36,"sheet_y":34,"short_name":"put_litter_in_its_place","short_names":["put_litter_in_its_place"],"text":null,"texts":null,"category":"Symbols","subcategory":"transport-sign","sort_order":1366,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DO NOT LITTER SYMBOL","unified":"1F6AF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6af.png","sheet_x":36,"sheet_y":35,"short_name":"do_not_litter","short_names":["do_not_litter"],"text":null,"texts":null,"category":"Symbols","subcategory":"warning","sort_order":1384,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"POTABLE WATER SYMBOL","unified":"1F6B0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6b0.png","sheet_x":36,"sheet_y":36,"short_name":"potable_water","short_names":["potable_water"],"text":null,"texts":null,"category":"Symbols","subcategory":"transport-sign","sort_order":1367,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NON-POTABLE WATER SYMBOL","unified":"1F6B1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6b1.png","sheet_x":36,"sheet_y":37,"short_name":"non-potable_water","short_names":["non-potable_water"],"text":null,"texts":null,"category":"Symbols","subcategory":"warning","sort_order":1385,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BICYCLE","unified":"1F6B2","non_qualified":null,"docomo":"E71D","au":"E4AE","softbank":"E136","google":"FE7EB","image":"1f6b2.png","sheet_x":36,"sheet_y":38,"short_name":"bike","short_names":["bike"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":907,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NO BICYCLES","unified":"1F6B3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6b3.png","sheet_x":36,"sheet_y":39,"short_name":"no_bicycles","short_names":["no_bicycles"],"text":null,"texts":null,"category":"Symbols","subcategory":"warning","sort_order":1382,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WOMAN BIKING","unified":"1F6B4-200D-2640-FE0F","non_qualified":"1F6B4-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6b4-200d-2640-fe0f.png","sheet_x":36,"sheet_y":40,"short_name":"woman-biking","short_names":["woman-biking"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":458,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F6B4-1F3FB-200D-2640-FE0F","non_qualified":"1F6B4-1F3FB-200D-2640","image":"1f6b4-1f3fb-200d-2640-fe0f.png","sheet_x":36,"sheet_y":41,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F6B4-1F3FC-200D-2640-FE0F","non_qualified":"1F6B4-1F3FC-200D-2640","image":"1f6b4-1f3fc-200d-2640-fe0f.png","sheet_x":36,"sheet_y":42,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F6B4-1F3FD-200D-2640-FE0F","non_qualified":"1F6B4-1F3FD-200D-2640","image":"1f6b4-1f3fd-200d-2640-fe0f.png","sheet_x":36,"sheet_y":43,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F6B4-1F3FE-200D-2640-FE0F","non_qualified":"1F6B4-1F3FE-200D-2640","image":"1f6b4-1f3fe-200d-2640-fe0f.png","sheet_x":36,"sheet_y":44,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F6B4-1F3FF-200D-2640-FE0F","non_qualified":"1F6B4-1F3FF-200D-2640","image":"1f6b4-1f3ff-200d-2640-fe0f.png","sheet_x":36,"sheet_y":45,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN BIKING","unified":"1F6B4-200D-2642-FE0F","non_qualified":"1F6B4-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6b4-200d-2642-fe0f.png","sheet_x":36,"sheet_y":46,"short_name":"man-biking","short_names":["man-biking"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":457,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F6B4-1F3FB-200D-2642-FE0F","non_qualified":"1F6B4-1F3FB-200D-2642","image":"1f6b4-1f3fb-200d-2642-fe0f.png","sheet_x":36,"sheet_y":47,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F6B4-1F3FC-200D-2642-FE0F","non_qualified":"1F6B4-1F3FC-200D-2642","image":"1f6b4-1f3fc-200d-2642-fe0f.png","sheet_x":36,"sheet_y":48,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F6B4-1F3FD-200D-2642-FE0F","non_qualified":"1F6B4-1F3FD-200D-2642","image":"1f6b4-1f3fd-200d-2642-fe0f.png","sheet_x":36,"sheet_y":49,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F6B4-1F3FE-200D-2642-FE0F","non_qualified":"1F6B4-1F3FE-200D-2642","image":"1f6b4-1f3fe-200d-2642-fe0f.png","sheet_x":36,"sheet_y":50,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F6B4-1F3FF-200D-2642-FE0F","non_qualified":"1F6B4-1F3FF-200D-2642","image":"1f6b4-1f3ff-200d-2642-fe0f.png","sheet_x":36,"sheet_y":51,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F6B4"},{"name":"BICYCLIST","unified":"1F6B4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6b4.png","sheet_x":36,"sheet_y":52,"short_name":"bicyclist","short_names":["bicyclist"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":456,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F6B4-1F3FB","non_qualified":null,"image":"1f6b4-1f3fb.png","sheet_x":36,"sheet_y":53,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F6B4-1F3FC","non_qualified":null,"image":"1f6b4-1f3fc.png","sheet_x":36,"sheet_y":54,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F6B4-1F3FD","non_qualified":null,"image":"1f6b4-1f3fd.png","sheet_x":36,"sheet_y":55,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F6B4-1F3FE","non_qualified":null,"image":"1f6b4-1f3fe.png","sheet_x":36,"sheet_y":56,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F6B4-1F3FF","non_qualified":null,"image":"1f6b4-1f3ff.png","sheet_x":36,"sheet_y":57,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F6B4-200D-2642-FE0F"},{"name":"WOMAN MOUNTAIN BIKING","unified":"1F6B5-200D-2640-FE0F","non_qualified":"1F6B5-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6b5-200d-2640-fe0f.png","sheet_x":36,"sheet_y":58,"short_name":"woman-mountain-biking","short_names":["woman-mountain-biking"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":461,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F6B5-1F3FB-200D-2640-FE0F","non_qualified":"1F6B5-1F3FB-200D-2640","image":"1f6b5-1f3fb-200d-2640-fe0f.png","sheet_x":36,"sheet_y":59,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F6B5-1F3FC-200D-2640-FE0F","non_qualified":"1F6B5-1F3FC-200D-2640","image":"1f6b5-1f3fc-200d-2640-fe0f.png","sheet_x":36,"sheet_y":60,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F6B5-1F3FD-200D-2640-FE0F","non_qualified":"1F6B5-1F3FD-200D-2640","image":"1f6b5-1f3fd-200d-2640-fe0f.png","sheet_x":37,"sheet_y":0,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F6B5-1F3FE-200D-2640-FE0F","non_qualified":"1F6B5-1F3FE-200D-2640","image":"1f6b5-1f3fe-200d-2640-fe0f.png","sheet_x":37,"sheet_y":1,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F6B5-1F3FF-200D-2640-FE0F","non_qualified":"1F6B5-1F3FF-200D-2640","image":"1f6b5-1f3ff-200d-2640-fe0f.png","sheet_x":37,"sheet_y":2,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN MOUNTAIN BIKING","unified":"1F6B5-200D-2642-FE0F","non_qualified":"1F6B5-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6b5-200d-2642-fe0f.png","sheet_x":37,"sheet_y":3,"short_name":"man-mountain-biking","short_names":["man-mountain-biking"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":460,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F6B5-1F3FB-200D-2642-FE0F","non_qualified":"1F6B5-1F3FB-200D-2642","image":"1f6b5-1f3fb-200d-2642-fe0f.png","sheet_x":37,"sheet_y":4,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F6B5-1F3FC-200D-2642-FE0F","non_qualified":"1F6B5-1F3FC-200D-2642","image":"1f6b5-1f3fc-200d-2642-fe0f.png","sheet_x":37,"sheet_y":5,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F6B5-1F3FD-200D-2642-FE0F","non_qualified":"1F6B5-1F3FD-200D-2642","image":"1f6b5-1f3fd-200d-2642-fe0f.png","sheet_x":37,"sheet_y":6,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F6B5-1F3FE-200D-2642-FE0F","non_qualified":"1F6B5-1F3FE-200D-2642","image":"1f6b5-1f3fe-200d-2642-fe0f.png","sheet_x":37,"sheet_y":7,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F6B5-1F3FF-200D-2642-FE0F","non_qualified":"1F6B5-1F3FF-200D-2642","image":"1f6b5-1f3ff-200d-2642-fe0f.png","sheet_x":37,"sheet_y":8,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F6B5"},{"name":"MOUNTAIN BICYCLIST","unified":"1F6B5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6b5.png","sheet_x":37,"sheet_y":9,"short_name":"mountain_bicyclist","short_names":["mountain_bicyclist"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":459,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F6B5-1F3FB","non_qualified":null,"image":"1f6b5-1f3fb.png","sheet_x":37,"sheet_y":10,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F6B5-1F3FC","non_qualified":null,"image":"1f6b5-1f3fc.png","sheet_x":37,"sheet_y":11,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F6B5-1F3FD","non_qualified":null,"image":"1f6b5-1f3fd.png","sheet_x":37,"sheet_y":12,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F6B5-1F3FE","non_qualified":null,"image":"1f6b5-1f3fe.png","sheet_x":37,"sheet_y":13,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F6B5-1F3FF","non_qualified":null,"image":"1f6b5-1f3ff.png","sheet_x":37,"sheet_y":14,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F6B5-200D-2642-FE0F"},{"name":"WOMAN WALKING","unified":"1F6B6-200D-2640-FE0F","non_qualified":"1F6B6-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6b6-200d-2640-fe0f.png","sheet_x":37,"sheet_y":15,"short_name":"woman-walking","short_names":["woman-walking"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":403,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F6B6-1F3FB-200D-2640-FE0F","non_qualified":"1F6B6-1F3FB-200D-2640","image":"1f6b6-1f3fb-200d-2640-fe0f.png","sheet_x":37,"sheet_y":16,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F6B6-1F3FC-200D-2640-FE0F","non_qualified":"1F6B6-1F3FC-200D-2640","image":"1f6b6-1f3fc-200d-2640-fe0f.png","sheet_x":37,"sheet_y":17,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F6B6-1F3FD-200D-2640-FE0F","non_qualified":"1F6B6-1F3FD-200D-2640","image":"1f6b6-1f3fd-200d-2640-fe0f.png","sheet_x":37,"sheet_y":18,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F6B6-1F3FE-200D-2640-FE0F","non_qualified":"1F6B6-1F3FE-200D-2640","image":"1f6b6-1f3fe-200d-2640-fe0f.png","sheet_x":37,"sheet_y":19,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F6B6-1F3FF-200D-2640-FE0F","non_qualified":"1F6B6-1F3FF-200D-2640","image":"1f6b6-1f3ff-200d-2640-fe0f.png","sheet_x":37,"sheet_y":20,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN WALKING","unified":"1F6B6-200D-2642-FE0F","non_qualified":"1F6B6-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6b6-200d-2642-fe0f.png","sheet_x":37,"sheet_y":21,"short_name":"man-walking","short_names":["man-walking"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":402,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F6B6-1F3FB-200D-2642-FE0F","non_qualified":"1F6B6-1F3FB-200D-2642","image":"1f6b6-1f3fb-200d-2642-fe0f.png","sheet_x":37,"sheet_y":22,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F6B6-1F3FC-200D-2642-FE0F","non_qualified":"1F6B6-1F3FC-200D-2642","image":"1f6b6-1f3fc-200d-2642-fe0f.png","sheet_x":37,"sheet_y":23,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F6B6-1F3FD-200D-2642-FE0F","non_qualified":"1F6B6-1F3FD-200D-2642","image":"1f6b6-1f3fd-200d-2642-fe0f.png","sheet_x":37,"sheet_y":24,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F6B6-1F3FE-200D-2642-FE0F","non_qualified":"1F6B6-1F3FE-200D-2642","image":"1f6b6-1f3fe-200d-2642-fe0f.png","sheet_x":37,"sheet_y":25,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F6B6-1F3FF-200D-2642-FE0F","non_qualified":"1F6B6-1F3FF-200D-2642","image":"1f6b6-1f3ff-200d-2642-fe0f.png","sheet_x":37,"sheet_y":26,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F6B6"},{"name":"PEDESTRIAN","unified":"1F6B6","non_qualified":null,"docomo":"E733","au":"EB72","softbank":"E201","google":"FE7F0","image":"1f6b6.png","sheet_x":37,"sheet_y":27,"short_name":"walking","short_names":["walking"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":401,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F6B6-1F3FB","non_qualified":null,"image":"1f6b6-1f3fb.png","sheet_x":37,"sheet_y":28,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F6B6-1F3FC","non_qualified":null,"image":"1f6b6-1f3fc.png","sheet_x":37,"sheet_y":29,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F6B6-1F3FD","non_qualified":null,"image":"1f6b6-1f3fd.png","sheet_x":37,"sheet_y":30,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F6B6-1F3FE","non_qualified":null,"image":"1f6b6-1f3fe.png","sheet_x":37,"sheet_y":31,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F6B6-1F3FF","non_qualified":null,"image":"1f6b6-1f3ff.png","sheet_x":37,"sheet_y":32,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F6B6-200D-2642-FE0F"},{"name":"NO PEDESTRIANS","unified":"1F6B7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6b7.png","sheet_x":37,"sheet_y":33,"short_name":"no_pedestrians","short_names":["no_pedestrians"],"text":null,"texts":null,"category":"Symbols","subcategory":"warning","sort_order":1386,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CHILDREN CROSSING","unified":"1F6B8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6b8.png","sheet_x":37,"sheet_y":34,"short_name":"children_crossing","short_names":["children_crossing"],"text":null,"texts":null,"category":"Symbols","subcategory":"warning","sort_order":1379,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MENS SYMBOL","unified":"1F6B9","non_qualified":null,"docomo":null,"au":null,"softbank":"E138","google":"FEB33","image":"1f6b9.png","sheet_x":37,"sheet_y":35,"short_name":"mens","short_names":["mens"],"text":null,"texts":null,"category":"Symbols","subcategory":"transport-sign","sort_order":1369,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WOMENS SYMBOL","unified":"1F6BA","non_qualified":null,"docomo":null,"au":null,"softbank":"E139","google":"FEB34","image":"1f6ba.png","sheet_x":37,"sheet_y":36,"short_name":"womens","short_names":["womens"],"text":null,"texts":null,"category":"Symbols","subcategory":"transport-sign","sort_order":1370,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RESTROOM","unified":"1F6BB","non_qualified":null,"docomo":"E66E","au":"E4A5","softbank":"E151","google":"FE506","image":"1f6bb.png","sheet_x":37,"sheet_y":37,"short_name":"restroom","short_names":["restroom"],"text":null,"texts":null,"category":"Symbols","subcategory":"transport-sign","sort_order":1371,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BABY SYMBOL","unified":"1F6BC","non_qualified":null,"docomo":null,"au":"EB18","softbank":"E13A","google":"FEB35","image":"1f6bc.png","sheet_x":37,"sheet_y":38,"short_name":"baby_symbol","short_names":["baby_symbol"],"text":null,"texts":null,"category":"Symbols","subcategory":"transport-sign","sort_order":1372,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TOILET","unified":"1F6BD","non_qualified":null,"docomo":"E66E","au":"E4A5","softbank":"E140","google":"FE507","image":"1f6bd.png","sheet_x":37,"sheet_y":39,"short_name":"toilet","short_names":["toilet"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1340,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WATER CLOSET","unified":"1F6BE","non_qualified":null,"docomo":"E66E","au":"E4A5","softbank":"E309","google":"FE508","image":"1f6be.png","sheet_x":37,"sheet_y":40,"short_name":"wc","short_names":["wc"],"text":null,"texts":null,"category":"Symbols","subcategory":"transport-sign","sort_order":1373,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SHOWER","unified":"1F6BF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6bf.png","sheet_x":37,"sheet_y":41,"short_name":"shower","short_names":["shower"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1342,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BATH","unified":"1F6C0","non_qualified":null,"docomo":"E6F7","au":"E5D8","softbank":"E13F","google":"FE505","image":"1f6c0.png","sheet_x":37,"sheet_y":42,"short_name":"bath","short_names":["bath"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-resting","sort_order":480,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F6C0-1F3FB","non_qualified":null,"image":"1f6c0-1f3fb.png","sheet_x":37,"sheet_y":43,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F6C0-1F3FC","non_qualified":null,"image":"1f6c0-1f3fc.png","sheet_x":37,"sheet_y":44,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F6C0-1F3FD","non_qualified":null,"image":"1f6c0-1f3fd.png","sheet_x":37,"sheet_y":45,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F6C0-1F3FE","non_qualified":null,"image":"1f6c0-1f3fe.png","sheet_x":37,"sheet_y":46,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F6C0-1F3FF","non_qualified":null,"image":"1f6c0-1f3ff.png","sheet_x":37,"sheet_y":47,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"BATHTUB","unified":"1F6C1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6c1.png","sheet_x":37,"sheet_y":48,"short_name":"bathtub","short_names":["bathtub"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1343,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PASSPORT CONTROL","unified":"1F6C2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6c2.png","sheet_x":37,"sheet_y":49,"short_name":"passport_control","short_names":["passport_control"],"text":null,"texts":null,"category":"Symbols","subcategory":"transport-sign","sort_order":1374,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CUSTOMS","unified":"1F6C3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6c3.png","sheet_x":37,"sheet_y":50,"short_name":"customs","short_names":["customs"],"text":null,"texts":null,"category":"Symbols","subcategory":"transport-sign","sort_order":1375,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BAGGAGE CLAIM","unified":"1F6C4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6c4.png","sheet_x":37,"sheet_y":51,"short_name":"baggage_claim","short_names":["baggage_claim"],"text":null,"texts":null,"category":"Symbols","subcategory":"transport-sign","sort_order":1376,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LEFT LUGGAGE","unified":"1F6C5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6c5.png","sheet_x":37,"sheet_y":52,"short_name":"left_luggage","short_names":["left_luggage"],"text":null,"texts":null,"category":"Symbols","subcategory":"transport-sign","sort_order":1377,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"COUCH AND LAMP","unified":"1F6CB-FE0F","non_qualified":"1F6CB","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6cb-fe0f.png","sheet_x":37,"sheet_y":53,"short_name":"couch_and_lamp","short_names":["couch_and_lamp"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1338,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SLEEPING ACCOMMODATION","unified":"1F6CC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6cc.png","sheet_x":37,"sheet_y":54,"short_name":"sleeping_accommodation","short_names":["sleeping_accommodation"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-resting","sort_order":481,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F6CC-1F3FB","non_qualified":null,"image":"1f6cc-1f3fb.png","sheet_x":37,"sheet_y":55,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F6CC-1F3FC","non_qualified":null,"image":"1f6cc-1f3fc.png","sheet_x":37,"sheet_y":56,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F6CC-1F3FD","non_qualified":null,"image":"1f6cc-1f3fd.png","sheet_x":37,"sheet_y":57,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F6CC-1F3FE","non_qualified":null,"image":"1f6cc-1f3fe.png","sheet_x":37,"sheet_y":58,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F6CC-1F3FF","non_qualified":null,"image":"1f6cc-1f3ff.png","sheet_x":37,"sheet_y":59,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"SHOPPING BAGS","unified":"1F6CD-FE0F","non_qualified":"1F6CD","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6cd-fe0f.png","sheet_x":37,"sheet_y":60,"short_name":"shopping_bags","short_names":["shopping_bags"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1133,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BELLHOP BELL","unified":"1F6CE-FE0F","non_qualified":"1F6CE","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6ce-fe0f.png","sheet_x":38,"sheet_y":0,"short_name":"bellhop_bell","short_names":["bellhop_bell"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"hotel","sort_order":944,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BED","unified":"1F6CF-FE0F","non_qualified":"1F6CF","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6cf-fe0f.png","sheet_x":38,"sheet_y":1,"short_name":"bed","short_names":["bed"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1337,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PLACE OF WORSHIP","unified":"1F6D0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6d0.png","sheet_x":38,"sheet_y":2,"short_name":"place_of_worship","short_names":["place_of_worship"],"text":null,"texts":null,"category":"Symbols","subcategory":"religion","sort_order":1412,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"OCTAGONAL SIGN","unified":"1F6D1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6d1.png","sheet_x":38,"sheet_y":3,"short_name":"octagonal_sign","short_names":["octagonal_sign"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":920,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SHOPPING TROLLEY","unified":"1F6D2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6d2.png","sheet_x":38,"sheet_y":4,"short_name":"shopping_trolley","short_names":["shopping_trolley"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1357,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HINDU TEMPLE","unified":"1F6D5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6d5.png","sheet_x":38,"sheet_y":5,"short_name":"hindu_temple","short_names":["hindu_temple"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-religious","sort_order":851,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HUT","unified":"1F6D6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6d6.png","sheet_x":38,"sheet_y":6,"short_name":"hut","short_names":["hut"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":828,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ELEVATOR","unified":"1F6D7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6d7.png","sheet_x":38,"sheet_y":7,"short_name":"elevator","short_names":["elevator"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1334,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PLAYGROUND SLIDE","unified":"1F6DD","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6dd.png","sheet_x":38,"sheet_y":8,"short_name":"playground_slide","short_names":["playground_slide"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-other","sort_order":867,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"WHEEL","unified":"1F6DE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6de.png","sheet_x":38,"sheet_y":9,"short_name":"wheel","short_names":["wheel"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":916,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"RING BUOY","unified":"1F6DF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6df.png","sheet_x":38,"sheet_y":10,"short_name":"ring_buoy","short_names":["ring_buoy"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-water","sort_order":923,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"HAMMER AND WRENCH","unified":"1F6E0-FE0F","non_qualified":"1F6E0","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6e0-fe0f.png","sheet_x":38,"sheet_y":11,"short_name":"hammer_and_wrench","short_names":["hammer_and_wrench"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1298,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SHIELD","unified":"1F6E1-FE0F","non_qualified":"1F6E1","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6e1-fe0f.png","sheet_x":38,"sheet_y":12,"short_name":"shield","short_names":["shield"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1304,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"OIL DRUM","unified":"1F6E2-FE0F","non_qualified":"1F6E2","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6e2-fe0f.png","sheet_x":38,"sheet_y":13,"short_name":"oil_drum","short_names":["oil_drum"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":914,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MOTORWAY","unified":"1F6E3-FE0F","non_qualified":"1F6E3","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6e3-fe0f.png","sheet_x":38,"sheet_y":14,"short_name":"motorway","short_names":["motorway"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":912,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RAILWAY TRACK","unified":"1F6E4-FE0F","non_qualified":"1F6E4","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6e4-fe0f.png","sheet_x":38,"sheet_y":15,"short_name":"railway_track","short_names":["railway_track"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":913,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MOTOR BOAT","unified":"1F6E5-FE0F","non_qualified":"1F6E5","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6e5-fe0f.png","sheet_x":38,"sheet_y":16,"short_name":"motor_boat","short_names":["motor_boat"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-water","sort_order":929,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SMALL AIRPLANE","unified":"1F6E9-FE0F","non_qualified":"1F6E9","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6e9-fe0f.png","sheet_x":38,"sheet_y":17,"short_name":"small_airplane","short_names":["small_airplane"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-air","sort_order":932,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"AIRPLANE DEPARTURE","unified":"1F6EB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6eb.png","sheet_x":38,"sheet_y":18,"short_name":"airplane_departure","short_names":["airplane_departure"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-air","sort_order":933,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"AIRPLANE ARRIVING","unified":"1F6EC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6ec.png","sheet_x":38,"sheet_y":19,"short_name":"airplane_arriving","short_names":["airplane_arriving"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-air","sort_order":934,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SATELLITE","unified":"1F6F0-FE0F","non_qualified":"1F6F0","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6f0-fe0f.png","sheet_x":38,"sheet_y":20,"short_name":"satellite","short_names":["satellite"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-air","sort_order":941,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PASSENGER SHIP","unified":"1F6F3-FE0F","non_qualified":"1F6F3","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6f3-fe0f.png","sheet_x":38,"sheet_y":21,"short_name":"passenger_ship","short_names":["passenger_ship"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-water","sort_order":927,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SCOOTER","unified":"1F6F4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6f4.png","sheet_x":38,"sheet_y":22,"short_name":"scooter","short_names":["scooter"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":908,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MOTOR SCOOTER","unified":"1F6F5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6f5.png","sheet_x":38,"sheet_y":23,"short_name":"motor_scooter","short_names":["motor_scooter"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":903,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CANOE","unified":"1F6F6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6f6.png","sheet_x":38,"sheet_y":24,"short_name":"canoe","short_names":["canoe"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-water","sort_order":925,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SLED","unified":"1F6F7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6f7.png","sheet_x":38,"sheet_y":25,"short_name":"sled","short_names":["sled"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1076,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FLYING SAUCER","unified":"1F6F8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6f8.png","sheet_x":38,"sheet_y":26,"short_name":"flying_saucer","short_names":["flying_saucer"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-air","sort_order":943,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SKATEBOARD","unified":"1F6F9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6f9.png","sheet_x":38,"sheet_y":27,"short_name":"skateboard","short_names":["skateboard"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":909,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"AUTO RICKSHAW","unified":"1F6FA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6fa.png","sheet_x":38,"sheet_y":28,"short_name":"auto_rickshaw","short_names":["auto_rickshaw"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":906,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PICKUP TRUCK","unified":"1F6FB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6fb.png","sheet_x":38,"sheet_y":29,"short_name":"pickup_truck","short_names":["pickup_truck"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":897,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ROLLER SKATE","unified":"1F6FC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6fc.png","sheet_x":38,"sheet_y":30,"short_name":"roller_skate","short_names":["roller_skate"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":910,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LARGE ORANGE CIRCLE","unified":"1F7E0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f7e0.png","sheet_x":38,"sheet_y":31,"short_name":"large_orange_circle","short_names":["large_orange_circle"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1553,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LARGE YELLOW CIRCLE","unified":"1F7E1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f7e1.png","sheet_x":38,"sheet_y":32,"short_name":"large_yellow_circle","short_names":["large_yellow_circle"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1554,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LARGE GREEN CIRCLE","unified":"1F7E2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f7e2.png","sheet_x":38,"sheet_y":33,"short_name":"large_green_circle","short_names":["large_green_circle"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1555,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LARGE PURPLE CIRCLE","unified":"1F7E3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f7e3.png","sheet_x":38,"sheet_y":34,"short_name":"large_purple_circle","short_names":["large_purple_circle"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1557,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LARGE BROWN CIRCLE","unified":"1F7E4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f7e4.png","sheet_x":38,"sheet_y":35,"short_name":"large_brown_circle","short_names":["large_brown_circle"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1558,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LARGE RED SQUARE","unified":"1F7E5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f7e5.png","sheet_x":38,"sheet_y":36,"short_name":"large_red_square","short_names":["large_red_square"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1561,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LARGE BLUE SQUARE","unified":"1F7E6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f7e6.png","sheet_x":38,"sheet_y":37,"short_name":"large_blue_square","short_names":["large_blue_square"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1565,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LARGE ORANGE SQUARE","unified":"1F7E7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f7e7.png","sheet_x":38,"sheet_y":38,"short_name":"large_orange_square","short_names":["large_orange_square"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1562,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LARGE YELLOW SQUARE","unified":"1F7E8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f7e8.png","sheet_x":38,"sheet_y":39,"short_name":"large_yellow_square","short_names":["large_yellow_square"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1563,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LARGE GREEN SQUARE","unified":"1F7E9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f7e9.png","sheet_x":38,"sheet_y":40,"short_name":"large_green_square","short_names":["large_green_square"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1564,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LARGE PURPLE SQUARE","unified":"1F7EA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f7ea.png","sheet_x":38,"sheet_y":41,"short_name":"large_purple_square","short_names":["large_purple_square"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1566,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LARGE BROWN SQUARE","unified":"1F7EB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f7eb.png","sheet_x":38,"sheet_y":42,"short_name":"large_brown_square","short_names":["large_brown_square"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1567,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HEAVY EQUALS SIGN","unified":"1F7F0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f7f0.png","sheet_x":38,"sheet_y":43,"short_name":"heavy_equals_sign","short_names":["heavy_equals_sign"],"text":null,"texts":null,"category":"Symbols","subcategory":"math","sort_order":1468,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"PINCHED FINGERS","unified":"1F90C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f90c.png","sheet_x":38,"sheet_y":44,"short_name":"pinched_fingers","short_names":["pinched_fingers"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-partial","sort_order":174,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F90C-1F3FB","non_qualified":null,"image":"1f90c-1f3fb.png","sheet_x":38,"sheet_y":45,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F90C-1F3FC","non_qualified":null,"image":"1f90c-1f3fc.png","sheet_x":38,"sheet_y":46,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F90C-1F3FD","non_qualified":null,"image":"1f90c-1f3fd.png","sheet_x":38,"sheet_y":47,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F90C-1F3FE","non_qualified":null,"image":"1f90c-1f3fe.png","sheet_x":38,"sheet_y":48,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F90C-1F3FF","non_qualified":null,"image":"1f90c-1f3ff.png","sheet_x":38,"sheet_y":49,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WHITE HEART","unified":"1F90D","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f90d.png","sheet_x":38,"sheet_y":50,"short_name":"white_heart","short_names":["white_heart"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":149,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BROWN HEART","unified":"1F90E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f90e.png","sheet_x":38,"sheet_y":51,"short_name":"brown_heart","short_names":["brown_heart"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":147,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PINCHING HAND","unified":"1F90F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f90f.png","sheet_x":38,"sheet_y":52,"short_name":"pinching_hand","short_names":["pinching_hand"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-partial","sort_order":175,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F90F-1F3FB","non_qualified":null,"image":"1f90f-1f3fb.png","sheet_x":38,"sheet_y":53,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F90F-1F3FC","non_qualified":null,"image":"1f90f-1f3fc.png","sheet_x":38,"sheet_y":54,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F90F-1F3FD","non_qualified":null,"image":"1f90f-1f3fd.png","sheet_x":38,"sheet_y":55,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F90F-1F3FE","non_qualified":null,"image":"1f90f-1f3fe.png","sheet_x":38,"sheet_y":56,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F90F-1F3FF","non_qualified":null,"image":"1f90f-1f3ff.png","sheet_x":38,"sheet_y":57,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"ZIPPER-MOUTH FACE","unified":"1F910","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f910.png","sheet_x":38,"sheet_y":58,"short_name":"zipper_mouth_face","short_names":["zipper_mouth_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-neutral-skeptical","sort_order":37,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MONEY-MOUTH FACE","unified":"1F911","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f911.png","sheet_x":38,"sheet_y":59,"short_name":"money_mouth_face","short_names":["money_mouth_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-tongue","sort_order":29,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE WITH THERMOMETER","unified":"1F912","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f912.png","sheet_x":38,"sheet_y":60,"short_name":"face_with_thermometer","short_names":["face_with_thermometer"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-unwell","sort_order":56,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NERD FACE","unified":"1F913","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f913.png","sheet_x":39,"sheet_y":0,"short_name":"nerd_face","short_names":["nerd_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-glasses","sort_order":71,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"THINKING FACE","unified":"1F914","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f914.png","sheet_x":39,"sheet_y":1,"short_name":"thinking_face","short_names":["thinking_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-hand","sort_order":35,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE WITH HEAD-BANDAGE","unified":"1F915","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f915.png","sheet_x":39,"sheet_y":2,"short_name":"face_with_head_bandage","short_names":["face_with_head_bandage"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-unwell","sort_order":57,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ROBOT FACE","unified":"1F916","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f916.png","sheet_x":39,"sheet_y":3,"short_name":"robot_face","short_names":["robot_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-costume","sort_order":114,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HUGGING FACE","unified":"1F917","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f917.png","sheet_x":39,"sheet_y":4,"short_name":"hugging_face","short_names":["hugging_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-hand","sort_order":30,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SIGN OF THE HORNS","unified":"1F918","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f918.png","sheet_x":39,"sheet_y":5,"short_name":"the_horns","short_names":["the_horns","sign_of_the_horns"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-partial","sort_order":180,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F918-1F3FB","non_qualified":null,"image":"1f918-1f3fb.png","sheet_x":39,"sheet_y":6,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F918-1F3FC","non_qualified":null,"image":"1f918-1f3fc.png","sheet_x":39,"sheet_y":7,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F918-1F3FD","non_qualified":null,"image":"1f918-1f3fd.png","sheet_x":39,"sheet_y":8,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F918-1F3FE","non_qualified":null,"image":"1f918-1f3fe.png","sheet_x":39,"sheet_y":9,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F918-1F3FF","non_qualified":null,"image":"1f918-1f3ff.png","sheet_x":39,"sheet_y":10,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"CALL ME HAND","unified":"1F919","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f919.png","sheet_x":39,"sheet_y":11,"short_name":"call_me_hand","short_names":["call_me_hand"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-partial","sort_order":181,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F919-1F3FB","non_qualified":null,"image":"1f919-1f3fb.png","sheet_x":39,"sheet_y":12,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F919-1F3FC","non_qualified":null,"image":"1f919-1f3fc.png","sheet_x":39,"sheet_y":13,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F919-1F3FD","non_qualified":null,"image":"1f919-1f3fd.png","sheet_x":39,"sheet_y":14,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F919-1F3FE","non_qualified":null,"image":"1f919-1f3fe.png","sheet_x":39,"sheet_y":15,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F919-1F3FF","non_qualified":null,"image":"1f919-1f3ff.png","sheet_x":39,"sheet_y":16,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"RAISED BACK OF HAND","unified":"1F91A","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f91a.png","sheet_x":39,"sheet_y":17,"short_name":"raised_back_of_hand","short_names":["raised_back_of_hand"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-open","sort_order":165,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F91A-1F3FB","non_qualified":null,"image":"1f91a-1f3fb.png","sheet_x":39,"sheet_y":18,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F91A-1F3FC","non_qualified":null,"image":"1f91a-1f3fc.png","sheet_x":39,"sheet_y":19,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F91A-1F3FD","non_qualified":null,"image":"1f91a-1f3fd.png","sheet_x":39,"sheet_y":20,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F91A-1F3FE","non_qualified":null,"image":"1f91a-1f3fe.png","sheet_x":39,"sheet_y":21,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F91A-1F3FF","non_qualified":null,"image":"1f91a-1f3ff.png","sheet_x":39,"sheet_y":22,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"LEFT-FACING FIST","unified":"1F91B","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f91b.png","sheet_x":39,"sheet_y":23,"short_name":"left-facing_fist","short_names":["left-facing_fist"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-closed","sort_order":193,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F91B-1F3FB","non_qualified":null,"image":"1f91b-1f3fb.png","sheet_x":39,"sheet_y":24,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F91B-1F3FC","non_qualified":null,"image":"1f91b-1f3fc.png","sheet_x":39,"sheet_y":25,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F91B-1F3FD","non_qualified":null,"image":"1f91b-1f3fd.png","sheet_x":39,"sheet_y":26,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F91B-1F3FE","non_qualified":null,"image":"1f91b-1f3fe.png","sheet_x":39,"sheet_y":27,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F91B-1F3FF","non_qualified":null,"image":"1f91b-1f3ff.png","sheet_x":39,"sheet_y":28,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"RIGHT-FACING FIST","unified":"1F91C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f91c.png","sheet_x":39,"sheet_y":29,"short_name":"right-facing_fist","short_names":["right-facing_fist"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-closed","sort_order":194,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F91C-1F3FB","non_qualified":null,"image":"1f91c-1f3fb.png","sheet_x":39,"sheet_y":30,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F91C-1F3FC","non_qualified":null,"image":"1f91c-1f3fc.png","sheet_x":39,"sheet_y":31,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F91C-1F3FD","non_qualified":null,"image":"1f91c-1f3fd.png","sheet_x":39,"sheet_y":32,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F91C-1F3FE","non_qualified":null,"image":"1f91c-1f3fe.png","sheet_x":39,"sheet_y":33,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F91C-1F3FF","non_qualified":null,"image":"1f91c-1f3ff.png","sheet_x":39,"sheet_y":34,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"HANDSHAKE","unified":"1F91D","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f91d.png","sheet_x":39,"sheet_y":35,"short_name":"handshake","short_names":["handshake"],"text":null,"texts":null,"category":"People & Body","subcategory":"hands","sort_order":200,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F91D-1F3FB","non_qualified":null,"image":"1f91d-1f3fb.png","sheet_x":39,"sheet_y":36,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F91D-1F3FC","non_qualified":null,"image":"1f91d-1f3fc.png","sheet_x":39,"sheet_y":37,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F91D-1F3FD","non_qualified":null,"image":"1f91d-1f3fd.png","sheet_x":39,"sheet_y":38,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F91D-1F3FE","non_qualified":null,"image":"1f91d-1f3fe.png","sheet_x":39,"sheet_y":39,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F91D-1F3FF","non_qualified":null,"image":"1f91d-1f3ff.png","sheet_x":39,"sheet_y":40,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FC":{"unified":"1FAF1-1F3FB-200D-1FAF2-1F3FC","non_qualified":null,"image":"1faf1-1f3fb-200d-1faf2-1f3fc.png","sheet_x":39,"sheet_y":41,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FB-1F3FD":{"unified":"1FAF1-1F3FB-200D-1FAF2-1F3FD","non_qualified":null,"image":"1faf1-1f3fb-200d-1faf2-1f3fd.png","sheet_x":39,"sheet_y":42,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FB-1F3FE":{"unified":"1FAF1-1F3FB-200D-1FAF2-1F3FE","non_qualified":null,"image":"1faf1-1f3fb-200d-1faf2-1f3fe.png","sheet_x":39,"sheet_y":43,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FB-1F3FF":{"unified":"1FAF1-1F3FB-200D-1FAF2-1F3FF","non_qualified":null,"image":"1faf1-1f3fb-200d-1faf2-1f3ff.png","sheet_x":39,"sheet_y":44,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FB":{"unified":"1FAF1-1F3FC-200D-1FAF2-1F3FB","non_qualified":null,"image":"1faf1-1f3fc-200d-1faf2-1f3fb.png","sheet_x":39,"sheet_y":45,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FD":{"unified":"1FAF1-1F3FC-200D-1FAF2-1F3FD","non_qualified":null,"image":"1faf1-1f3fc-200d-1faf2-1f3fd.png","sheet_x":39,"sheet_y":46,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FE":{"unified":"1FAF1-1F3FC-200D-1FAF2-1F3FE","non_qualified":null,"image":"1faf1-1f3fc-200d-1faf2-1f3fe.png","sheet_x":39,"sheet_y":47,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FF":{"unified":"1FAF1-1F3FC-200D-1FAF2-1F3FF","non_qualified":null,"image":"1faf1-1f3fc-200d-1faf2-1f3ff.png","sheet_x":39,"sheet_y":48,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FB":{"unified":"1FAF1-1F3FD-200D-1FAF2-1F3FB","non_qualified":null,"image":"1faf1-1f3fd-200d-1faf2-1f3fb.png","sheet_x":39,"sheet_y":49,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FC":{"unified":"1FAF1-1F3FD-200D-1FAF2-1F3FC","non_qualified":null,"image":"1faf1-1f3fd-200d-1faf2-1f3fc.png","sheet_x":39,"sheet_y":50,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FE":{"unified":"1FAF1-1F3FD-200D-1FAF2-1F3FE","non_qualified":null,"image":"1faf1-1f3fd-200d-1faf2-1f3fe.png","sheet_x":39,"sheet_y":51,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FF":{"unified":"1FAF1-1F3FD-200D-1FAF2-1F3FF","non_qualified":null,"image":"1faf1-1f3fd-200d-1faf2-1f3ff.png","sheet_x":39,"sheet_y":52,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE-1F3FB":{"unified":"1FAF1-1F3FE-200D-1FAF2-1F3FB","non_qualified":null,"image":"1faf1-1f3fe-200d-1faf2-1f3fb.png","sheet_x":39,"sheet_y":53,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE-1F3FC":{"unified":"1FAF1-1F3FE-200D-1FAF2-1F3FC","non_qualified":null,"image":"1faf1-1f3fe-200d-1faf2-1f3fc.png","sheet_x":39,"sheet_y":54,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE-1F3FD":{"unified":"1FAF1-1F3FE-200D-1FAF2-1F3FD","non_qualified":null,"image":"1faf1-1f3fe-200d-1faf2-1f3fd.png","sheet_x":39,"sheet_y":55,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE-1F3FF":{"unified":"1FAF1-1F3FE-200D-1FAF2-1F3FF","non_qualified":null,"image":"1faf1-1f3fe-200d-1faf2-1f3ff.png","sheet_x":39,"sheet_y":56,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF-1F3FB":{"unified":"1FAF1-1F3FF-200D-1FAF2-1F3FB","non_qualified":null,"image":"1faf1-1f3ff-200d-1faf2-1f3fb.png","sheet_x":39,"sheet_y":57,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF-1F3FC":{"unified":"1FAF1-1F3FF-200D-1FAF2-1F3FC","non_qualified":null,"image":"1faf1-1f3ff-200d-1faf2-1f3fc.png","sheet_x":39,"sheet_y":58,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF-1F3FD":{"unified":"1FAF1-1F3FF-200D-1FAF2-1F3FD","non_qualified":null,"image":"1faf1-1f3ff-200d-1faf2-1f3fd.png","sheet_x":39,"sheet_y":59,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF-1F3FE":{"unified":"1FAF1-1F3FF-200D-1FAF2-1F3FE","non_qualified":null,"image":"1faf1-1f3ff-200d-1faf2-1f3fe.png","sheet_x":39,"sheet_y":60,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false}}},{"name":"HAND WITH INDEX AND MIDDLE FINGERS CROSSED","unified":"1F91E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f91e.png","sheet_x":40,"sheet_y":0,"short_name":"crossed_fingers","short_names":["crossed_fingers","hand_with_index_and_middle_fingers_crossed"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-partial","sort_order":177,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F91E-1F3FB","non_qualified":null,"image":"1f91e-1f3fb.png","sheet_x":40,"sheet_y":1,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F91E-1F3FC","non_qualified":null,"image":"1f91e-1f3fc.png","sheet_x":40,"sheet_y":2,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F91E-1F3FD","non_qualified":null,"image":"1f91e-1f3fd.png","sheet_x":40,"sheet_y":3,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F91E-1F3FE","non_qualified":null,"image":"1f91e-1f3fe.png","sheet_x":40,"sheet_y":4,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F91E-1F3FF","non_qualified":null,"image":"1f91e-1f3ff.png","sheet_x":40,"sheet_y":5,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"I LOVE YOU HAND SIGN","unified":"1F91F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f91f.png","sheet_x":40,"sheet_y":6,"short_name":"i_love_you_hand_sign","short_names":["i_love_you_hand_sign"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-partial","sort_order":179,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F91F-1F3FB","non_qualified":null,"image":"1f91f-1f3fb.png","sheet_x":40,"sheet_y":7,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F91F-1F3FC","non_qualified":null,"image":"1f91f-1f3fc.png","sheet_x":40,"sheet_y":8,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F91F-1F3FD","non_qualified":null,"image":"1f91f-1f3fd.png","sheet_x":40,"sheet_y":9,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F91F-1F3FE","non_qualified":null,"image":"1f91f-1f3fe.png","sheet_x":40,"sheet_y":10,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F91F-1F3FF","non_qualified":null,"image":"1f91f-1f3ff.png","sheet_x":40,"sheet_y":11,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"FACE WITH COWBOY HAT","unified":"1F920","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f920.png","sheet_x":40,"sheet_y":12,"short_name":"face_with_cowboy_hat","short_names":["face_with_cowboy_hat"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-hat","sort_order":67,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOWN FACE","unified":"1F921","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f921.png","sheet_x":40,"sheet_y":13,"short_name":"clown_face","short_names":["clown_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-costume","sort_order":108,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NAUSEATED FACE","unified":"1F922","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f922.png","sheet_x":40,"sheet_y":14,"short_name":"nauseated_face","short_names":["nauseated_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-unwell","sort_order":58,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ROLLING ON THE FLOOR LAUGHING","unified":"1F923","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f923.png","sheet_x":40,"sheet_y":15,"short_name":"rolling_on_the_floor_laughing","short_names":["rolling_on_the_floor_laughing"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-smiling","sort_order":7,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DROOLING FACE","unified":"1F924","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f924.png","sheet_x":40,"sheet_y":16,"short_name":"drooling_face","short_names":["drooling_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-sleepy","sort_order":53,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LYING FACE","unified":"1F925","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f925.png","sheet_x":40,"sheet_y":17,"short_name":"lying_face","short_names":["lying_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-neutral-skeptical","sort_order":49,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WOMAN FACEPALMING","unified":"1F926-200D-2640-FE0F","non_qualified":"1F926-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f926-200d-2640-fe0f.png","sheet_x":40,"sheet_y":18,"short_name":"woman-facepalming","short_names":["woman-facepalming"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":277,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F926-1F3FB-200D-2640-FE0F","non_qualified":"1F926-1F3FB-200D-2640","image":"1f926-1f3fb-200d-2640-fe0f.png","sheet_x":40,"sheet_y":19,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F926-1F3FC-200D-2640-FE0F","non_qualified":"1F926-1F3FC-200D-2640","image":"1f926-1f3fc-200d-2640-fe0f.png","sheet_x":40,"sheet_y":20,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F926-1F3FD-200D-2640-FE0F","non_qualified":"1F926-1F3FD-200D-2640","image":"1f926-1f3fd-200d-2640-fe0f.png","sheet_x":40,"sheet_y":21,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F926-1F3FE-200D-2640-FE0F","non_qualified":"1F926-1F3FE-200D-2640","image":"1f926-1f3fe-200d-2640-fe0f.png","sheet_x":40,"sheet_y":22,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F926-1F3FF-200D-2640-FE0F","non_qualified":"1F926-1F3FF-200D-2640","image":"1f926-1f3ff-200d-2640-fe0f.png","sheet_x":40,"sheet_y":23,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN FACEPALMING","unified":"1F926-200D-2642-FE0F","non_qualified":"1F926-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f926-200d-2642-fe0f.png","sheet_x":40,"sheet_y":24,"short_name":"man-facepalming","short_names":["man-facepalming"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":276,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F926-1F3FB-200D-2642-FE0F","non_qualified":"1F926-1F3FB-200D-2642","image":"1f926-1f3fb-200d-2642-fe0f.png","sheet_x":40,"sheet_y":25,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F926-1F3FC-200D-2642-FE0F","non_qualified":"1F926-1F3FC-200D-2642","image":"1f926-1f3fc-200d-2642-fe0f.png","sheet_x":40,"sheet_y":26,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F926-1F3FD-200D-2642-FE0F","non_qualified":"1F926-1F3FD-200D-2642","image":"1f926-1f3fd-200d-2642-fe0f.png","sheet_x":40,"sheet_y":27,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F926-1F3FE-200D-2642-FE0F","non_qualified":"1F926-1F3FE-200D-2642","image":"1f926-1f3fe-200d-2642-fe0f.png","sheet_x":40,"sheet_y":28,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F926-1F3FF-200D-2642-FE0F","non_qualified":"1F926-1F3FF-200D-2642","image":"1f926-1f3ff-200d-2642-fe0f.png","sheet_x":40,"sheet_y":29,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"FACE PALM","unified":"1F926","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f926.png","sheet_x":40,"sheet_y":30,"short_name":"face_palm","short_names":["face_palm"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":275,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F926-1F3FB","non_qualified":null,"image":"1f926-1f3fb.png","sheet_x":40,"sheet_y":31,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F926-1F3FC","non_qualified":null,"image":"1f926-1f3fc.png","sheet_x":40,"sheet_y":32,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F926-1F3FD","non_qualified":null,"image":"1f926-1f3fd.png","sheet_x":40,"sheet_y":33,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F926-1F3FE","non_qualified":null,"image":"1f926-1f3fe.png","sheet_x":40,"sheet_y":34,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F926-1F3FF","non_qualified":null,"image":"1f926-1f3ff.png","sheet_x":40,"sheet_y":35,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"SNEEZING FACE","unified":"1F927","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f927.png","sheet_x":40,"sheet_y":36,"short_name":"sneezing_face","short_names":["sneezing_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-unwell","sort_order":60,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE WITH ONE EYEBROW RAISED","unified":"1F928","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f928.png","sheet_x":40,"sheet_y":37,"short_name":"face_with_raised_eyebrow","short_names":["face_with_raised_eyebrow","face_with_one_eyebrow_raised"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-neutral-skeptical","sort_order":38,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GRINNING FACE WITH STAR EYES","unified":"1F929","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f929.png","sheet_x":40,"sheet_y":38,"short_name":"star-struck","short_names":["star-struck","grinning_face_with_star_eyes"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-affection","sort_order":17,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GRINNING FACE WITH ONE LARGE AND ONE SMALL EYE","unified":"1F92A","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f92a.png","sheet_x":40,"sheet_y":39,"short_name":"zany_face","short_names":["zany_face","grinning_face_with_one_large_and_one_small_eye"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-tongue","sort_order":27,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE WITH FINGER COVERING CLOSED LIPS","unified":"1F92B","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f92b.png","sheet_x":40,"sheet_y":40,"short_name":"shushing_face","short_names":["shushing_face","face_with_finger_covering_closed_lips"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-hand","sort_order":34,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SERIOUS FACE WITH SYMBOLS COVERING MOUTH","unified":"1F92C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f92c.png","sheet_x":40,"sheet_y":41,"short_name":"face_with_symbols_on_mouth","short_names":["face_with_symbols_on_mouth","serious_face_with_symbols_covering_mouth"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-negative","sort_order":102,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SMILING FACE WITH SMILING EYES AND HAND COVERING MOUTH","unified":"1F92D","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f92d.png","sheet_x":40,"sheet_y":42,"short_name":"face_with_hand_over_mouth","short_names":["face_with_hand_over_mouth","smiling_face_with_smiling_eyes_and_hand_covering_mouth"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-hand","sort_order":31,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE WITH OPEN MOUTH VOMITING","unified":"1F92E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f92e.png","sheet_x":40,"sheet_y":43,"short_name":"face_vomiting","short_names":["face_vomiting","face_with_open_mouth_vomiting"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-unwell","sort_order":59,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SHOCKED FACE WITH EXPLODING HEAD","unified":"1F92F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f92f.png","sheet_x":40,"sheet_y":44,"short_name":"exploding_head","short_names":["exploding_head","shocked_face_with_exploding_head"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-unwell","sort_order":66,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PREGNANT WOMAN","unified":"1F930","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f930.png","sheet_x":40,"sheet_y":45,"short_name":"pregnant_woman","short_names":["pregnant_woman"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":356,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F930-1F3FB","non_qualified":null,"image":"1f930-1f3fb.png","sheet_x":40,"sheet_y":46,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F930-1F3FC","non_qualified":null,"image":"1f930-1f3fc.png","sheet_x":40,"sheet_y":47,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F930-1F3FD","non_qualified":null,"image":"1f930-1f3fd.png","sheet_x":40,"sheet_y":48,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F930-1F3FE","non_qualified":null,"image":"1f930-1f3fe.png","sheet_x":40,"sheet_y":49,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F930-1F3FF","non_qualified":null,"image":"1f930-1f3ff.png","sheet_x":40,"sheet_y":50,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"BREAST-FEEDING","unified":"1F931","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f931.png","sheet_x":40,"sheet_y":51,"short_name":"breast-feeding","short_names":["breast-feeding"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":359,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F931-1F3FB","non_qualified":null,"image":"1f931-1f3fb.png","sheet_x":40,"sheet_y":52,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F931-1F3FC","non_qualified":null,"image":"1f931-1f3fc.png","sheet_x":40,"sheet_y":53,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F931-1F3FD","non_qualified":null,"image":"1f931-1f3fd.png","sheet_x":40,"sheet_y":54,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F931-1F3FE","non_qualified":null,"image":"1f931-1f3fe.png","sheet_x":40,"sheet_y":55,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F931-1F3FF","non_qualified":null,"image":"1f931-1f3ff.png","sheet_x":40,"sheet_y":56,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"PALMS UP TOGETHER","unified":"1F932","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f932.png","sheet_x":40,"sheet_y":57,"short_name":"palms_up_together","short_names":["palms_up_together"],"text":null,"texts":null,"category":"People & Body","subcategory":"hands","sort_order":199,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F932-1F3FB","non_qualified":null,"image":"1f932-1f3fb.png","sheet_x":40,"sheet_y":58,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F932-1F3FC","non_qualified":null,"image":"1f932-1f3fc.png","sheet_x":40,"sheet_y":59,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F932-1F3FD","non_qualified":null,"image":"1f932-1f3fd.png","sheet_x":40,"sheet_y":60,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F932-1F3FE","non_qualified":null,"image":"1f932-1f3fe.png","sheet_x":41,"sheet_y":0,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F932-1F3FF","non_qualified":null,"image":"1f932-1f3ff.png","sheet_x":41,"sheet_y":1,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"SELFIE","unified":"1F933","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f933.png","sheet_x":41,"sheet_y":2,"short_name":"selfie","short_names":["selfie"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-prop","sort_order":204,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F933-1F3FB","non_qualified":null,"image":"1f933-1f3fb.png","sheet_x":41,"sheet_y":3,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F933-1F3FC","non_qualified":null,"image":"1f933-1f3fc.png","sheet_x":41,"sheet_y":4,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F933-1F3FD","non_qualified":null,"image":"1f933-1f3fd.png","sheet_x":41,"sheet_y":5,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F933-1F3FE","non_qualified":null,"image":"1f933-1f3fe.png","sheet_x":41,"sheet_y":6,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F933-1F3FF","non_qualified":null,"image":"1f933-1f3ff.png","sheet_x":41,"sheet_y":7,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"PRINCE","unified":"1F934","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f934.png","sheet_x":41,"sheet_y":8,"short_name":"prince","short_names":["prince"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":343,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F934-1F3FB","non_qualified":null,"image":"1f934-1f3fb.png","sheet_x":41,"sheet_y":9,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F934-1F3FC","non_qualified":null,"image":"1f934-1f3fc.png","sheet_x":41,"sheet_y":10,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F934-1F3FD","non_qualified":null,"image":"1f934-1f3fd.png","sheet_x":41,"sheet_y":11,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F934-1F3FE","non_qualified":null,"image":"1f934-1f3fe.png","sheet_x":41,"sheet_y":12,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F934-1F3FF","non_qualified":null,"image":"1f934-1f3ff.png","sheet_x":41,"sheet_y":13,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN IN TUXEDO","unified":"1F935-200D-2640-FE0F","non_qualified":"1F935-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f935-200d-2640-fe0f.png","sheet_x":41,"sheet_y":14,"short_name":"woman_in_tuxedo","short_names":["woman_in_tuxedo"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":352,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F935-1F3FB-200D-2640-FE0F","non_qualified":"1F935-1F3FB-200D-2640","image":"1f935-1f3fb-200d-2640-fe0f.png","sheet_x":41,"sheet_y":15,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F935-1F3FC-200D-2640-FE0F","non_qualified":"1F935-1F3FC-200D-2640","image":"1f935-1f3fc-200d-2640-fe0f.png","sheet_x":41,"sheet_y":16,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F935-1F3FD-200D-2640-FE0F","non_qualified":"1F935-1F3FD-200D-2640","image":"1f935-1f3fd-200d-2640-fe0f.png","sheet_x":41,"sheet_y":17,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F935-1F3FE-200D-2640-FE0F","non_qualified":"1F935-1F3FE-200D-2640","image":"1f935-1f3fe-200d-2640-fe0f.png","sheet_x":41,"sheet_y":18,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F935-1F3FF-200D-2640-FE0F","non_qualified":"1F935-1F3FF-200D-2640","image":"1f935-1f3ff-200d-2640-fe0f.png","sheet_x":41,"sheet_y":19,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN IN TUXEDO","unified":"1F935-200D-2642-FE0F","non_qualified":"1F935-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f935-200d-2642-fe0f.png","sheet_x":41,"sheet_y":20,"short_name":"man_in_tuxedo","short_names":["man_in_tuxedo"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":351,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F935-1F3FB-200D-2642-FE0F","non_qualified":"1F935-1F3FB-200D-2642","image":"1f935-1f3fb-200d-2642-fe0f.png","sheet_x":41,"sheet_y":21,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F935-1F3FC-200D-2642-FE0F","non_qualified":"1F935-1F3FC-200D-2642","image":"1f935-1f3fc-200d-2642-fe0f.png","sheet_x":41,"sheet_y":22,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F935-1F3FD-200D-2642-FE0F","non_qualified":"1F935-1F3FD-200D-2642","image":"1f935-1f3fd-200d-2642-fe0f.png","sheet_x":41,"sheet_y":23,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F935-1F3FE-200D-2642-FE0F","non_qualified":"1F935-1F3FE-200D-2642","image":"1f935-1f3fe-200d-2642-fe0f.png","sheet_x":41,"sheet_y":24,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F935-1F3FF-200D-2642-FE0F","non_qualified":"1F935-1F3FF-200D-2642","image":"1f935-1f3ff-200d-2642-fe0f.png","sheet_x":41,"sheet_y":25,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN IN TUXEDO","unified":"1F935","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f935.png","sheet_x":41,"sheet_y":26,"short_name":"person_in_tuxedo","short_names":["person_in_tuxedo"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":350,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F935-1F3FB","non_qualified":null,"image":"1f935-1f3fb.png","sheet_x":41,"sheet_y":27,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F935-1F3FC","non_qualified":null,"image":"1f935-1f3fc.png","sheet_x":41,"sheet_y":28,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F935-1F3FD","non_qualified":null,"image":"1f935-1f3fd.png","sheet_x":41,"sheet_y":29,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F935-1F3FE","non_qualified":null,"image":"1f935-1f3fe.png","sheet_x":41,"sheet_y":30,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F935-1F3FF","non_qualified":null,"image":"1f935-1f3ff.png","sheet_x":41,"sheet_y":31,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MOTHER CHRISTMAS","unified":"1F936","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f936.png","sheet_x":41,"sheet_y":32,"short_name":"mrs_claus","short_names":["mrs_claus","mother_christmas"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":365,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F936-1F3FB","non_qualified":null,"image":"1f936-1f3fb.png","sheet_x":41,"sheet_y":33,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F936-1F3FC","non_qualified":null,"image":"1f936-1f3fc.png","sheet_x":41,"sheet_y":34,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F936-1F3FD","non_qualified":null,"image":"1f936-1f3fd.png","sheet_x":41,"sheet_y":35,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F936-1F3FE","non_qualified":null,"image":"1f936-1f3fe.png","sheet_x":41,"sheet_y":36,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F936-1F3FF","non_qualified":null,"image":"1f936-1f3ff.png","sheet_x":41,"sheet_y":37,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN SHRUGGING","unified":"1F937-200D-2640-FE0F","non_qualified":"1F937-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f937-200d-2640-fe0f.png","sheet_x":41,"sheet_y":38,"short_name":"woman-shrugging","short_names":["woman-shrugging"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":280,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F937-1F3FB-200D-2640-FE0F","non_qualified":"1F937-1F3FB-200D-2640","image":"1f937-1f3fb-200d-2640-fe0f.png","sheet_x":41,"sheet_y":39,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F937-1F3FC-200D-2640-FE0F","non_qualified":"1F937-1F3FC-200D-2640","image":"1f937-1f3fc-200d-2640-fe0f.png","sheet_x":41,"sheet_y":40,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F937-1F3FD-200D-2640-FE0F","non_qualified":"1F937-1F3FD-200D-2640","image":"1f937-1f3fd-200d-2640-fe0f.png","sheet_x":41,"sheet_y":41,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F937-1F3FE-200D-2640-FE0F","non_qualified":"1F937-1F3FE-200D-2640","image":"1f937-1f3fe-200d-2640-fe0f.png","sheet_x":41,"sheet_y":42,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F937-1F3FF-200D-2640-FE0F","non_qualified":"1F937-1F3FF-200D-2640","image":"1f937-1f3ff-200d-2640-fe0f.png","sheet_x":41,"sheet_y":43,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN SHRUGGING","unified":"1F937-200D-2642-FE0F","non_qualified":"1F937-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f937-200d-2642-fe0f.png","sheet_x":41,"sheet_y":44,"short_name":"man-shrugging","short_names":["man-shrugging"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":279,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F937-1F3FB-200D-2642-FE0F","non_qualified":"1F937-1F3FB-200D-2642","image":"1f937-1f3fb-200d-2642-fe0f.png","sheet_x":41,"sheet_y":45,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F937-1F3FC-200D-2642-FE0F","non_qualified":"1F937-1F3FC-200D-2642","image":"1f937-1f3fc-200d-2642-fe0f.png","sheet_x":41,"sheet_y":46,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F937-1F3FD-200D-2642-FE0F","non_qualified":"1F937-1F3FD-200D-2642","image":"1f937-1f3fd-200d-2642-fe0f.png","sheet_x":41,"sheet_y":47,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F937-1F3FE-200D-2642-FE0F","non_qualified":"1F937-1F3FE-200D-2642","image":"1f937-1f3fe-200d-2642-fe0f.png","sheet_x":41,"sheet_y":48,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F937-1F3FF-200D-2642-FE0F","non_qualified":"1F937-1F3FF-200D-2642","image":"1f937-1f3ff-200d-2642-fe0f.png","sheet_x":41,"sheet_y":49,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"SHRUG","unified":"1F937","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f937.png","sheet_x":41,"sheet_y":50,"short_name":"shrug","short_names":["shrug"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":278,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F937-1F3FB","non_qualified":null,"image":"1f937-1f3fb.png","sheet_x":41,"sheet_y":51,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F937-1F3FC","non_qualified":null,"image":"1f937-1f3fc.png","sheet_x":41,"sheet_y":52,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F937-1F3FD","non_qualified":null,"image":"1f937-1f3fd.png","sheet_x":41,"sheet_y":53,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F937-1F3FE","non_qualified":null,"image":"1f937-1f3fe.png","sheet_x":41,"sheet_y":54,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F937-1F3FF","non_qualified":null,"image":"1f937-1f3ff.png","sheet_x":41,"sheet_y":55,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN CARTWHEELING","unified":"1F938-200D-2640-FE0F","non_qualified":"1F938-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f938-200d-2640-fe0f.png","sheet_x":41,"sheet_y":56,"short_name":"woman-cartwheeling","short_names":["woman-cartwheeling"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":464,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F938-1F3FB-200D-2640-FE0F","non_qualified":"1F938-1F3FB-200D-2640","image":"1f938-1f3fb-200d-2640-fe0f.png","sheet_x":41,"sheet_y":57,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F938-1F3FC-200D-2640-FE0F","non_qualified":"1F938-1F3FC-200D-2640","image":"1f938-1f3fc-200d-2640-fe0f.png","sheet_x":41,"sheet_y":58,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F938-1F3FD-200D-2640-FE0F","non_qualified":"1F938-1F3FD-200D-2640","image":"1f938-1f3fd-200d-2640-fe0f.png","sheet_x":41,"sheet_y":59,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F938-1F3FE-200D-2640-FE0F","non_qualified":"1F938-1F3FE-200D-2640","image":"1f938-1f3fe-200d-2640-fe0f.png","sheet_x":41,"sheet_y":60,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F938-1F3FF-200D-2640-FE0F","non_qualified":"1F938-1F3FF-200D-2640","image":"1f938-1f3ff-200d-2640-fe0f.png","sheet_x":42,"sheet_y":0,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN CARTWHEELING","unified":"1F938-200D-2642-FE0F","non_qualified":"1F938-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f938-200d-2642-fe0f.png","sheet_x":42,"sheet_y":1,"short_name":"man-cartwheeling","short_names":["man-cartwheeling"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":463,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F938-1F3FB-200D-2642-FE0F","non_qualified":"1F938-1F3FB-200D-2642","image":"1f938-1f3fb-200d-2642-fe0f.png","sheet_x":42,"sheet_y":2,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F938-1F3FC-200D-2642-FE0F","non_qualified":"1F938-1F3FC-200D-2642","image":"1f938-1f3fc-200d-2642-fe0f.png","sheet_x":42,"sheet_y":3,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F938-1F3FD-200D-2642-FE0F","non_qualified":"1F938-1F3FD-200D-2642","image":"1f938-1f3fd-200d-2642-fe0f.png","sheet_x":42,"sheet_y":4,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F938-1F3FE-200D-2642-FE0F","non_qualified":"1F938-1F3FE-200D-2642","image":"1f938-1f3fe-200d-2642-fe0f.png","sheet_x":42,"sheet_y":5,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F938-1F3FF-200D-2642-FE0F","non_qualified":"1F938-1F3FF-200D-2642","image":"1f938-1f3ff-200d-2642-fe0f.png","sheet_x":42,"sheet_y":6,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"PERSON DOING CARTWHEEL","unified":"1F938","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f938.png","sheet_x":42,"sheet_y":7,"short_name":"person_doing_cartwheel","short_names":["person_doing_cartwheel"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":462,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F938-1F3FB","non_qualified":null,"image":"1f938-1f3fb.png","sheet_x":42,"sheet_y":8,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F938-1F3FC","non_qualified":null,"image":"1f938-1f3fc.png","sheet_x":42,"sheet_y":9,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F938-1F3FD","non_qualified":null,"image":"1f938-1f3fd.png","sheet_x":42,"sheet_y":10,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F938-1F3FE","non_qualified":null,"image":"1f938-1f3fe.png","sheet_x":42,"sheet_y":11,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F938-1F3FF","non_qualified":null,"image":"1f938-1f3ff.png","sheet_x":42,"sheet_y":12,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN JUGGLING","unified":"1F939-200D-2640-FE0F","non_qualified":"1F939-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f939-200d-2640-fe0f.png","sheet_x":42,"sheet_y":13,"short_name":"woman-juggling","short_names":["woman-juggling"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":476,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F939-1F3FB-200D-2640-FE0F","non_qualified":"1F939-1F3FB-200D-2640","image":"1f939-1f3fb-200d-2640-fe0f.png","sheet_x":42,"sheet_y":14,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F939-1F3FC-200D-2640-FE0F","non_qualified":"1F939-1F3FC-200D-2640","image":"1f939-1f3fc-200d-2640-fe0f.png","sheet_x":42,"sheet_y":15,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F939-1F3FD-200D-2640-FE0F","non_qualified":"1F939-1F3FD-200D-2640","image":"1f939-1f3fd-200d-2640-fe0f.png","sheet_x":42,"sheet_y":16,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F939-1F3FE-200D-2640-FE0F","non_qualified":"1F939-1F3FE-200D-2640","image":"1f939-1f3fe-200d-2640-fe0f.png","sheet_x":42,"sheet_y":17,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F939-1F3FF-200D-2640-FE0F","non_qualified":"1F939-1F3FF-200D-2640","image":"1f939-1f3ff-200d-2640-fe0f.png","sheet_x":42,"sheet_y":18,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN JUGGLING","unified":"1F939-200D-2642-FE0F","non_qualified":"1F939-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f939-200d-2642-fe0f.png","sheet_x":42,"sheet_y":19,"short_name":"man-juggling","short_names":["man-juggling"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":475,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F939-1F3FB-200D-2642-FE0F","non_qualified":"1F939-1F3FB-200D-2642","image":"1f939-1f3fb-200d-2642-fe0f.png","sheet_x":42,"sheet_y":20,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F939-1F3FC-200D-2642-FE0F","non_qualified":"1F939-1F3FC-200D-2642","image":"1f939-1f3fc-200d-2642-fe0f.png","sheet_x":42,"sheet_y":21,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F939-1F3FD-200D-2642-FE0F","non_qualified":"1F939-1F3FD-200D-2642","image":"1f939-1f3fd-200d-2642-fe0f.png","sheet_x":42,"sheet_y":22,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F939-1F3FE-200D-2642-FE0F","non_qualified":"1F939-1F3FE-200D-2642","image":"1f939-1f3fe-200d-2642-fe0f.png","sheet_x":42,"sheet_y":23,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F939-1F3FF-200D-2642-FE0F","non_qualified":"1F939-1F3FF-200D-2642","image":"1f939-1f3ff-200d-2642-fe0f.png","sheet_x":42,"sheet_y":24,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"JUGGLING","unified":"1F939","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f939.png","sheet_x":42,"sheet_y":25,"short_name":"juggling","short_names":["juggling"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":474,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F939-1F3FB","non_qualified":null,"image":"1f939-1f3fb.png","sheet_x":42,"sheet_y":26,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F939-1F3FC","non_qualified":null,"image":"1f939-1f3fc.png","sheet_x":42,"sheet_y":27,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F939-1F3FD","non_qualified":null,"image":"1f939-1f3fd.png","sheet_x":42,"sheet_y":28,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F939-1F3FE","non_qualified":null,"image":"1f939-1f3fe.png","sheet_x":42,"sheet_y":29,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F939-1F3FF","non_qualified":null,"image":"1f939-1f3ff.png","sheet_x":42,"sheet_y":30,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"FENCER","unified":"1F93A","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f93a.png","sheet_x":42,"sheet_y":31,"short_name":"fencer","short_names":["fencer"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":434,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WOMEN WRESTLING","unified":"1F93C-200D-2640-FE0F","non_qualified":"1F93C-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f93c-200d-2640-fe0f.png","sheet_x":42,"sheet_y":32,"short_name":"woman-wrestling","short_names":["woman-wrestling"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":467,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MEN WRESTLING","unified":"1F93C-200D-2642-FE0F","non_qualified":"1F93C-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f93c-200d-2642-fe0f.png","sheet_x":42,"sheet_y":33,"short_name":"man-wrestling","short_names":["man-wrestling"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":466,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WRESTLERS","unified":"1F93C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f93c.png","sheet_x":42,"sheet_y":34,"short_name":"wrestlers","short_names":["wrestlers"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":465,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WOMAN PLAYING WATER POLO","unified":"1F93D-200D-2640-FE0F","non_qualified":"1F93D-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f93d-200d-2640-fe0f.png","sheet_x":42,"sheet_y":35,"short_name":"woman-playing-water-polo","short_names":["woman-playing-water-polo"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":470,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F93D-1F3FB-200D-2640-FE0F","non_qualified":"1F93D-1F3FB-200D-2640","image":"1f93d-1f3fb-200d-2640-fe0f.png","sheet_x":42,"sheet_y":36,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F93D-1F3FC-200D-2640-FE0F","non_qualified":"1F93D-1F3FC-200D-2640","image":"1f93d-1f3fc-200d-2640-fe0f.png","sheet_x":42,"sheet_y":37,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F93D-1F3FD-200D-2640-FE0F","non_qualified":"1F93D-1F3FD-200D-2640","image":"1f93d-1f3fd-200d-2640-fe0f.png","sheet_x":42,"sheet_y":38,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F93D-1F3FE-200D-2640-FE0F","non_qualified":"1F93D-1F3FE-200D-2640","image":"1f93d-1f3fe-200d-2640-fe0f.png","sheet_x":42,"sheet_y":39,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F93D-1F3FF-200D-2640-FE0F","non_qualified":"1F93D-1F3FF-200D-2640","image":"1f93d-1f3ff-200d-2640-fe0f.png","sheet_x":42,"sheet_y":40,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN PLAYING WATER POLO","unified":"1F93D-200D-2642-FE0F","non_qualified":"1F93D-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f93d-200d-2642-fe0f.png","sheet_x":42,"sheet_y":41,"short_name":"man-playing-water-polo","short_names":["man-playing-water-polo"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":469,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F93D-1F3FB-200D-2642-FE0F","non_qualified":"1F93D-1F3FB-200D-2642","image":"1f93d-1f3fb-200d-2642-fe0f.png","sheet_x":42,"sheet_y":42,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F93D-1F3FC-200D-2642-FE0F","non_qualified":"1F93D-1F3FC-200D-2642","image":"1f93d-1f3fc-200d-2642-fe0f.png","sheet_x":42,"sheet_y":43,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F93D-1F3FD-200D-2642-FE0F","non_qualified":"1F93D-1F3FD-200D-2642","image":"1f93d-1f3fd-200d-2642-fe0f.png","sheet_x":42,"sheet_y":44,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F93D-1F3FE-200D-2642-FE0F","non_qualified":"1F93D-1F3FE-200D-2642","image":"1f93d-1f3fe-200d-2642-fe0f.png","sheet_x":42,"sheet_y":45,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F93D-1F3FF-200D-2642-FE0F","non_qualified":"1F93D-1F3FF-200D-2642","image":"1f93d-1f3ff-200d-2642-fe0f.png","sheet_x":42,"sheet_y":46,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WATER POLO","unified":"1F93D","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f93d.png","sheet_x":42,"sheet_y":47,"short_name":"water_polo","short_names":["water_polo"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":468,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F93D-1F3FB","non_qualified":null,"image":"1f93d-1f3fb.png","sheet_x":42,"sheet_y":48,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F93D-1F3FC","non_qualified":null,"image":"1f93d-1f3fc.png","sheet_x":42,"sheet_y":49,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F93D-1F3FD","non_qualified":null,"image":"1f93d-1f3fd.png","sheet_x":42,"sheet_y":50,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F93D-1F3FE","non_qualified":null,"image":"1f93d-1f3fe.png","sheet_x":42,"sheet_y":51,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F93D-1F3FF","non_qualified":null,"image":"1f93d-1f3ff.png","sheet_x":42,"sheet_y":52,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN PLAYING HANDBALL","unified":"1F93E-200D-2640-FE0F","non_qualified":"1F93E-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f93e-200d-2640-fe0f.png","sheet_x":42,"sheet_y":53,"short_name":"woman-playing-handball","short_names":["woman-playing-handball"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":473,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F93E-1F3FB-200D-2640-FE0F","non_qualified":"1F93E-1F3FB-200D-2640","image":"1f93e-1f3fb-200d-2640-fe0f.png","sheet_x":42,"sheet_y":54,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F93E-1F3FC-200D-2640-FE0F","non_qualified":"1F93E-1F3FC-200D-2640","image":"1f93e-1f3fc-200d-2640-fe0f.png","sheet_x":42,"sheet_y":55,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F93E-1F3FD-200D-2640-FE0F","non_qualified":"1F93E-1F3FD-200D-2640","image":"1f93e-1f3fd-200d-2640-fe0f.png","sheet_x":42,"sheet_y":56,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F93E-1F3FE-200D-2640-FE0F","non_qualified":"1F93E-1F3FE-200D-2640","image":"1f93e-1f3fe-200d-2640-fe0f.png","sheet_x":42,"sheet_y":57,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F93E-1F3FF-200D-2640-FE0F","non_qualified":"1F93E-1F3FF-200D-2640","image":"1f93e-1f3ff-200d-2640-fe0f.png","sheet_x":42,"sheet_y":58,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN PLAYING HANDBALL","unified":"1F93E-200D-2642-FE0F","non_qualified":"1F93E-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f93e-200d-2642-fe0f.png","sheet_x":42,"sheet_y":59,"short_name":"man-playing-handball","short_names":["man-playing-handball"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":472,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F93E-1F3FB-200D-2642-FE0F","non_qualified":"1F93E-1F3FB-200D-2642","image":"1f93e-1f3fb-200d-2642-fe0f.png","sheet_x":42,"sheet_y":60,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F93E-1F3FC-200D-2642-FE0F","non_qualified":"1F93E-1F3FC-200D-2642","image":"1f93e-1f3fc-200d-2642-fe0f.png","sheet_x":43,"sheet_y":0,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F93E-1F3FD-200D-2642-FE0F","non_qualified":"1F93E-1F3FD-200D-2642","image":"1f93e-1f3fd-200d-2642-fe0f.png","sheet_x":43,"sheet_y":1,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F93E-1F3FE-200D-2642-FE0F","non_qualified":"1F93E-1F3FE-200D-2642","image":"1f93e-1f3fe-200d-2642-fe0f.png","sheet_x":43,"sheet_y":2,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F93E-1F3FF-200D-2642-FE0F","non_qualified":"1F93E-1F3FF-200D-2642","image":"1f93e-1f3ff-200d-2642-fe0f.png","sheet_x":43,"sheet_y":3,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"HANDBALL","unified":"1F93E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f93e.png","sheet_x":43,"sheet_y":4,"short_name":"handball","short_names":["handball"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":471,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F93E-1F3FB","non_qualified":null,"image":"1f93e-1f3fb.png","sheet_x":43,"sheet_y":5,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F93E-1F3FC","non_qualified":null,"image":"1f93e-1f3fc.png","sheet_x":43,"sheet_y":6,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F93E-1F3FD","non_qualified":null,"image":"1f93e-1f3fd.png","sheet_x":43,"sheet_y":7,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F93E-1F3FE","non_qualified":null,"image":"1f93e-1f3fe.png","sheet_x":43,"sheet_y":8,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F93E-1F3FF","non_qualified":null,"image":"1f93e-1f3ff.png","sheet_x":43,"sheet_y":9,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"DIVING MASK","unified":"1F93F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f93f.png","sheet_x":43,"sheet_y":10,"short_name":"diving_mask","short_names":["diving_mask"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1073,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WILTED FLOWER","unified":"1F940","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f940.png","sheet_x":43,"sheet_y":11,"short_name":"wilted_flower","short_names":["wilted_flower"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-flower","sort_order":654,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DRUM WITH DRUMSTICKS","unified":"1F941","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f941.png","sheet_x":43,"sheet_y":12,"short_name":"drum_with_drumsticks","short_names":["drum_with_drumsticks"],"text":null,"texts":null,"category":"Objects","subcategory":"musical-instrument","sort_order":1180,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLINKING GLASSES","unified":"1F942","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f942.png","sheet_x":43,"sheet_y":13,"short_name":"clinking_glasses","short_names":["clinking_glasses"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"drink","sort_order":791,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TUMBLER GLASS","unified":"1F943","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f943.png","sheet_x":43,"sheet_y":14,"short_name":"tumbler_glass","short_names":["tumbler_glass"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"drink","sort_order":792,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SPOON","unified":"1F944","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f944.png","sheet_x":43,"sheet_y":15,"short_name":"spoon","short_names":["spoon"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"dishware","sort_order":802,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GOAL NET","unified":"1F945","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f945.png","sheet_x":43,"sheet_y":16,"short_name":"goal_net","short_names":["goal_net"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1069,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FIRST PLACE MEDAL","unified":"1F947","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f947.png","sheet_x":43,"sheet_y":17,"short_name":"first_place_medal","short_names":["first_place_medal"],"text":null,"texts":null,"category":"Activities","subcategory":"award-medal","sort_order":1048,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SECOND PLACE MEDAL","unified":"1F948","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f948.png","sheet_x":43,"sheet_y":18,"short_name":"second_place_medal","short_names":["second_place_medal"],"text":null,"texts":null,"category":"Activities","subcategory":"award-medal","sort_order":1049,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"THIRD PLACE MEDAL","unified":"1F949","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f949.png","sheet_x":43,"sheet_y":19,"short_name":"third_place_medal","short_names":["third_place_medal"],"text":null,"texts":null,"category":"Activities","subcategory":"award-medal","sort_order":1050,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BOXING GLOVE","unified":"1F94A","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f94a.png","sheet_x":43,"sheet_y":20,"short_name":"boxing_glove","short_names":["boxing_glove"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1067,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MARTIAL ARTS UNIFORM","unified":"1F94B","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f94b.png","sheet_x":43,"sheet_y":21,"short_name":"martial_arts_uniform","short_names":["martial_arts_uniform"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1068,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CURLING STONE","unified":"1F94C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f94c.png","sheet_x":43,"sheet_y":22,"short_name":"curling_stone","short_names":["curling_stone"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1077,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LACROSSE STICK AND BALL","unified":"1F94D","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f94d.png","sheet_x":43,"sheet_y":23,"short_name":"lacrosse","short_names":["lacrosse"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1064,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SOFTBALL","unified":"1F94E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f94e.png","sheet_x":43,"sheet_y":24,"short_name":"softball","short_names":["softball"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1053,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FLYING DISC","unified":"1F94F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f94f.png","sheet_x":43,"sheet_y":25,"short_name":"flying_disc","short_names":["flying_disc"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1059,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CROISSANT","unified":"1F950","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f950.png","sheet_x":43,"sheet_y":26,"short_name":"croissant","short_names":["croissant"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":710,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"AVOCADO","unified":"1F951","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f951.png","sheet_x":43,"sheet_y":27,"short_name":"avocado","short_names":["avocado"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-vegetable","sort_order":693,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CUCUMBER","unified":"1F952","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f952.png","sheet_x":43,"sheet_y":28,"short_name":"cucumber","short_names":["cucumber"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-vegetable","sort_order":700,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BACON","unified":"1F953","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f953.png","sheet_x":43,"sheet_y":29,"short_name":"bacon","short_names":["bacon"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":721,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"POTATO","unified":"1F954","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f954.png","sheet_x":43,"sheet_y":30,"short_name":"potato","short_names":["potato"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-vegetable","sort_order":695,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CARROT","unified":"1F955","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f955.png","sheet_x":43,"sheet_y":31,"short_name":"carrot","short_names":["carrot"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-vegetable","sort_order":696,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BAGUETTE BREAD","unified":"1F956","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f956.png","sheet_x":43,"sheet_y":32,"short_name":"baguette_bread","short_names":["baguette_bread"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":711,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GREEN SALAD","unified":"1F957","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f957.png","sheet_x":43,"sheet_y":33,"short_name":"green_salad","short_names":["green_salad"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":738,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SHALLOW PAN OF FOOD","unified":"1F958","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f958.png","sheet_x":43,"sheet_y":34,"short_name":"shallow_pan_of_food","short_names":["shallow_pan_of_food"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":734,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"STUFFED FLATBREAD","unified":"1F959","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f959.png","sheet_x":43,"sheet_y":35,"short_name":"stuffed_flatbread","short_names":["stuffed_flatbread"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":730,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EGG","unified":"1F95A","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f95a.png","sheet_x":43,"sheet_y":36,"short_name":"egg","short_names":["egg"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":732,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GLASS OF MILK","unified":"1F95B","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f95b.png","sheet_x":43,"sheet_y":37,"short_name":"glass_of_milk","short_names":["glass_of_milk"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"drink","sort_order":780,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PEANUTS","unified":"1F95C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f95c.png","sheet_x":43,"sheet_y":38,"short_name":"peanuts","short_names":["peanuts"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-vegetable","sort_order":706,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"KIWIFRUIT","unified":"1F95D","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f95d.png","sheet_x":43,"sheet_y":39,"short_name":"kiwifruit","short_names":["kiwifruit"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-fruit","sort_order":689,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PANCAKES","unified":"1F95E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f95e.png","sheet_x":43,"sheet_y":40,"short_name":"pancakes","short_names":["pancakes"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":715,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DUMPLING","unified":"1F95F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f95f.png","sheet_x":43,"sheet_y":41,"short_name":"dumpling","short_names":["dumpling"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-asian","sort_order":757,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FORTUNE COOKIE","unified":"1F960","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f960.png","sheet_x":43,"sheet_y":42,"short_name":"fortune_cookie","short_names":["fortune_cookie"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-asian","sort_order":758,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TAKEOUT BOX","unified":"1F961","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f961.png","sheet_x":43,"sheet_y":43,"short_name":"takeout_box","short_names":["takeout_box"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-asian","sort_order":759,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CHOPSTICKS","unified":"1F962","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f962.png","sheet_x":43,"sheet_y":44,"short_name":"chopsticks","short_names":["chopsticks"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"dishware","sort_order":799,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BOWL WITH SPOON","unified":"1F963","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f963.png","sheet_x":43,"sheet_y":45,"short_name":"bowl_with_spoon","short_names":["bowl_with_spoon"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":737,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CUP WITH STRAW","unified":"1F964","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f964.png","sheet_x":43,"sheet_y":46,"short_name":"cup_with_straw","short_names":["cup_with_straw"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"drink","sort_order":794,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"COCONUT","unified":"1F965","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f965.png","sheet_x":43,"sheet_y":47,"short_name":"coconut","short_names":["coconut"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-fruit","sort_order":692,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BROCCOLI","unified":"1F966","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f966.png","sheet_x":43,"sheet_y":48,"short_name":"broccoli","short_names":["broccoli"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-vegetable","sort_order":702,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PIE","unified":"1F967","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f967.png","sheet_x":43,"sheet_y":49,"short_name":"pie","short_names":["pie"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-sweet","sort_order":773,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PRETZEL","unified":"1F968","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f968.png","sheet_x":43,"sheet_y":50,"short_name":"pretzel","short_names":["pretzel"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":713,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CUT OF MEAT","unified":"1F969","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f969.png","sheet_x":43,"sheet_y":51,"short_name":"cut_of_meat","short_names":["cut_of_meat"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":720,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SANDWICH","unified":"1F96A","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f96a.png","sheet_x":43,"sheet_y":52,"short_name":"sandwich","short_names":["sandwich"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":726,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CANNED FOOD","unified":"1F96B","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f96b.png","sheet_x":43,"sheet_y":53,"short_name":"canned_food","short_names":["canned_food"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":742,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LEAFY GREEN","unified":"1F96C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f96c.png","sheet_x":43,"sheet_y":54,"short_name":"leafy_green","short_names":["leafy_green"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-vegetable","sort_order":701,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MANGO","unified":"1F96D","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f96d.png","sheet_x":43,"sheet_y":55,"short_name":"mango","short_names":["mango"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-fruit","sort_order":681,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MOON CAKE","unified":"1F96E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f96e.png","sheet_x":43,"sheet_y":56,"short_name":"moon_cake","short_names":["moon_cake"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-asian","sort_order":755,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BAGEL","unified":"1F96F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f96f.png","sheet_x":43,"sheet_y":57,"short_name":"bagel","short_names":["bagel"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":714,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SMILING FACE WITH SMILING EYES AND THREE HEARTS","unified":"1F970","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f970.png","sheet_x":43,"sheet_y":58,"short_name":"smiling_face_with_3_hearts","short_names":["smiling_face_with_3_hearts"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-affection","sort_order":15,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"YAWNING FACE","unified":"1F971","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f971.png","sheet_x":43,"sheet_y":59,"short_name":"yawning_face","short_names":["yawning_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":98,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SMILING FACE WITH TEAR","unified":"1F972","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f972.png","sheet_x":43,"sheet_y":60,"short_name":"smiling_face_with_tear","short_names":["smiling_face_with_tear"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-affection","sort_order":23,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE WITH PARTY HORN AND PARTY HAT","unified":"1F973","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f973.png","sheet_x":44,"sheet_y":0,"short_name":"partying_face","short_names":["partying_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-hat","sort_order":68,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE WITH UNEVEN EYES AND WAVY MOUTH","unified":"1F974","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f974.png","sheet_x":44,"sheet_y":1,"short_name":"woozy_face","short_names":["woozy_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-unwell","sort_order":63,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"OVERHEATED FACE","unified":"1F975","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f975.png","sheet_x":44,"sheet_y":2,"short_name":"hot_face","short_names":["hot_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-unwell","sort_order":61,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FREEZING FACE","unified":"1F976","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f976.png","sheet_x":44,"sheet_y":3,"short_name":"cold_face","short_names":["cold_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-unwell","sort_order":62,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NINJA","unified":"1F977","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f977.png","sheet_x":44,"sheet_y":4,"short_name":"ninja","short_names":["ninja"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":338,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F977-1F3FB","non_qualified":null,"image":"1f977-1f3fb.png","sheet_x":44,"sheet_y":5,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F977-1F3FC","non_qualified":null,"image":"1f977-1f3fc.png","sheet_x":44,"sheet_y":6,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F977-1F3FD","non_qualified":null,"image":"1f977-1f3fd.png","sheet_x":44,"sheet_y":7,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F977-1F3FE","non_qualified":null,"image":"1f977-1f3fe.png","sheet_x":44,"sheet_y":8,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F977-1F3FF","non_qualified":null,"image":"1f977-1f3ff.png","sheet_x":44,"sheet_y":9,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"DISGUISED FACE","unified":"1F978","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f978.png","sheet_x":44,"sheet_y":10,"short_name":"disguised_face","short_names":["disguised_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-hat","sort_order":69,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE HOLDING BACK TEARS","unified":"1F979","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f979.png","sheet_x":44,"sheet_y":11,"short_name":"face_holding_back_tears","short_names":["face_holding_back_tears"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":83,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"FACE WITH PLEADING EYES","unified":"1F97A","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f97a.png","sheet_x":44,"sheet_y":12,"short_name":"pleading_face","short_names":["pleading_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":82,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SARI","unified":"1F97B","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f97b.png","sheet_x":44,"sheet_y":13,"short_name":"sari","short_names":["sari"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1124,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LAB COAT","unified":"1F97C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f97c.png","sheet_x":44,"sheet_y":14,"short_name":"lab_coat","short_names":["lab_coat"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1113,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GOGGLES","unified":"1F97D","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f97d.png","sheet_x":44,"sheet_y":15,"short_name":"goggles","short_names":["goggles"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1112,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HIKING BOOT","unified":"1F97E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f97e.png","sheet_x":44,"sheet_y":16,"short_name":"hiking_boot","short_names":["hiking_boot"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1138,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FLAT SHOE","unified":"1F97F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f97f.png","sheet_x":44,"sheet_y":17,"short_name":"womans_flat_shoe","short_names":["womans_flat_shoe"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1139,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CRAB","unified":"1F980","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f980.png","sheet_x":44,"sheet_y":18,"short_name":"crab","short_names":["crab"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-marine","sort_order":760,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LION FACE","unified":"1F981","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f981.png","sheet_x":44,"sheet_y":19,"short_name":"lion_face","short_names":["lion_face"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":545,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SCORPION","unified":"1F982","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f982.png","sheet_x":44,"sheet_y":20,"short_name":"scorpion","short_names":["scorpion"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bug","sort_order":643,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TURKEY","unified":"1F983","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f983.png","sheet_x":44,"sheet_y":21,"short_name":"turkey","short_names":["turkey"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bird","sort_order":594,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"UNICORN FACE","unified":"1F984","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f984.png","sheet_x":44,"sheet_y":22,"short_name":"unicorn_face","short_names":["unicorn_face"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":551,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EAGLE","unified":"1F985","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f985.png","sheet_x":44,"sheet_y":23,"short_name":"eagle","short_names":["eagle"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bird","sort_order":603,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DUCK","unified":"1F986","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f986.png","sheet_x":44,"sheet_y":24,"short_name":"duck","short_names":["duck"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bird","sort_order":604,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BAT","unified":"1F987","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f987.png","sheet_x":44,"sheet_y":25,"short_name":"bat","short_names":["bat"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":583,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SHARK","unified":"1F988","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f988.png","sheet_x":44,"sheet_y":26,"short_name":"shark","short_names":["shark"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-marine","sort_order":628,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"OWL","unified":"1F989","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f989.png","sheet_x":44,"sheet_y":27,"short_name":"owl","short_names":["owl"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bird","sort_order":606,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FOX FACE","unified":"1F98A","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f98a.png","sheet_x":44,"sheet_y":28,"short_name":"fox_face","short_names":["fox_face"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":540,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BUTTERFLY","unified":"1F98B","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f98b.png","sheet_x":44,"sheet_y":29,"short_name":"butterfly","short_names":["butterfly"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bug","sort_order":633,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DEER","unified":"1F98C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f98c.png","sheet_x":44,"sheet_y":30,"short_name":"deer","short_names":["deer"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":553,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GORILLA","unified":"1F98D","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f98d.png","sheet_x":44,"sheet_y":31,"short_name":"gorilla","short_names":["gorilla"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":532,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LIZARD","unified":"1F98E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f98e.png","sheet_x":44,"sheet_y":32,"short_name":"lizard","short_names":["lizard"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-reptile","sort_order":615,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RHINOCEROS","unified":"1F98F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f98f.png","sheet_x":44,"sheet_y":33,"short_name":"rhinoceros","short_names":["rhinoceros"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":572,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SHRIMP","unified":"1F990","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f990.png","sheet_x":44,"sheet_y":34,"short_name":"shrimp","short_names":["shrimp"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-marine","sort_order":762,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUID","unified":"1F991","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f991.png","sheet_x":44,"sheet_y":35,"short_name":"squid","short_names":["squid"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-marine","sort_order":763,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GIRAFFE FACE","unified":"1F992","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f992.png","sheet_x":44,"sheet_y":36,"short_name":"giraffe_face","short_names":["giraffe_face"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":569,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ZEBRA FACE","unified":"1F993","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f993.png","sheet_x":44,"sheet_y":37,"short_name":"zebra_face","short_names":["zebra_face"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":552,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HEDGEHOG","unified":"1F994","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f994.png","sheet_x":44,"sheet_y":38,"short_name":"hedgehog","short_names":["hedgehog"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":582,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SAUROPOD","unified":"1F995","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f995.png","sheet_x":44,"sheet_y":39,"short_name":"sauropod","short_names":["sauropod"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-reptile","sort_order":619,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"T-REX","unified":"1F996","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f996.png","sheet_x":44,"sheet_y":40,"short_name":"t-rex","short_names":["t-rex"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-reptile","sort_order":620,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CRICKET","unified":"1F997","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f997.png","sheet_x":44,"sheet_y":41,"short_name":"cricket","short_names":["cricket"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bug","sort_order":639,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"KANGAROO","unified":"1F998","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f998.png","sheet_x":44,"sheet_y":42,"short_name":"kangaroo","short_names":["kangaroo"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":591,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LLAMA","unified":"1F999","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f999.png","sheet_x":44,"sheet_y":43,"short_name":"llama","short_names":["llama"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":568,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PEACOCK","unified":"1F99A","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f99a.png","sheet_x":44,"sheet_y":44,"short_name":"peacock","short_names":["peacock"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bird","sort_order":610,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HIPPOPOTAMUS","unified":"1F99B","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f99b.png","sheet_x":44,"sheet_y":45,"short_name":"hippopotamus","short_names":["hippopotamus"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":573,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PARROT","unified":"1F99C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f99c.png","sheet_x":44,"sheet_y":46,"short_name":"parrot","short_names":["parrot"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bird","sort_order":611,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RACCOON","unified":"1F99D","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f99d.png","sheet_x":44,"sheet_y":47,"short_name":"raccoon","short_names":["raccoon"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":541,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LOBSTER","unified":"1F99E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f99e.png","sheet_x":44,"sheet_y":48,"short_name":"lobster","short_names":["lobster"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-marine","sort_order":761,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MOSQUITO","unified":"1F99F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f99f.png","sheet_x":44,"sheet_y":49,"short_name":"mosquito","short_names":["mosquito"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bug","sort_order":644,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MICROBE","unified":"1F9A0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9a0.png","sheet_x":44,"sheet_y":50,"short_name":"microbe","short_names":["microbe"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bug","sort_order":647,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BADGER","unified":"1F9A1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9a1.png","sheet_x":44,"sheet_y":51,"short_name":"badger","short_names":["badger"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":592,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SWAN","unified":"1F9A2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9a2.png","sheet_x":44,"sheet_y":52,"short_name":"swan","short_names":["swan"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bird","sort_order":605,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MAMMOTH","unified":"1F9A3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9a3.png","sheet_x":44,"sheet_y":53,"short_name":"mammoth","short_names":["mammoth"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":571,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DODO","unified":"1F9A4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9a4.png","sheet_x":44,"sheet_y":54,"short_name":"dodo","short_names":["dodo"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bird","sort_order":607,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SLOTH","unified":"1F9A5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9a5.png","sheet_x":44,"sheet_y":55,"short_name":"sloth","short_names":["sloth"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":588,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"OTTER","unified":"1F9A6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9a6.png","sheet_x":44,"sheet_y":56,"short_name":"otter","short_names":["otter"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":589,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ORANGUTAN","unified":"1F9A7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9a7.png","sheet_x":44,"sheet_y":57,"short_name":"orangutan","short_names":["orangutan"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":533,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SKUNK","unified":"1F9A8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9a8.png","sheet_x":44,"sheet_y":58,"short_name":"skunk","short_names":["skunk"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":590,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FLAMINGO","unified":"1F9A9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9a9.png","sheet_x":44,"sheet_y":59,"short_name":"flamingo","short_names":["flamingo"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bird","sort_order":609,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"OYSTER","unified":"1F9AA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9aa.png","sheet_x":44,"sheet_y":60,"short_name":"oyster","short_names":["oyster"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-marine","sort_order":764,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BEAVER","unified":"1F9AB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9ab.png","sheet_x":45,"sheet_y":0,"short_name":"beaver","short_names":["beaver"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":581,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BISON","unified":"1F9AC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9ac.png","sheet_x":45,"sheet_y":1,"short_name":"bison","short_names":["bison"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":554,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SEAL","unified":"1F9AD","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9ad.png","sheet_x":45,"sheet_y":2,"short_name":"seal","short_names":["seal"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-marine","sort_order":624,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GUIDE DOG","unified":"1F9AE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9ae.png","sheet_x":45,"sheet_y":3,"short_name":"guide_dog","short_names":["guide_dog"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":536,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PROBING CANE","unified":"1F9AF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9af.png","sheet_x":45,"sheet_y":4,"short_name":"probing_cane","short_names":["probing_cane"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1312,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BONE","unified":"1F9B4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9b4.png","sheet_x":45,"sheet_y":5,"short_name":"bone","short_names":["bone"],"text":null,"texts":null,"category":"People & Body","subcategory":"body-parts","sort_order":217,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LEG","unified":"1F9B5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9b5.png","sheet_x":45,"sheet_y":6,"short_name":"leg","short_names":["leg"],"text":null,"texts":null,"category":"People & Body","subcategory":"body-parts","sort_order":208,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9B5-1F3FB","non_qualified":null,"image":"1f9b5-1f3fb.png","sheet_x":45,"sheet_y":7,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9B5-1F3FC","non_qualified":null,"image":"1f9b5-1f3fc.png","sheet_x":45,"sheet_y":8,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9B5-1F3FD","non_qualified":null,"image":"1f9b5-1f3fd.png","sheet_x":45,"sheet_y":9,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9B5-1F3FE","non_qualified":null,"image":"1f9b5-1f3fe.png","sheet_x":45,"sheet_y":10,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9B5-1F3FF","non_qualified":null,"image":"1f9b5-1f3ff.png","sheet_x":45,"sheet_y":11,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"FOOT","unified":"1F9B6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9b6.png","sheet_x":45,"sheet_y":12,"short_name":"foot","short_names":["foot"],"text":null,"texts":null,"category":"People & Body","subcategory":"body-parts","sort_order":209,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9B6-1F3FB","non_qualified":null,"image":"1f9b6-1f3fb.png","sheet_x":45,"sheet_y":13,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9B6-1F3FC","non_qualified":null,"image":"1f9b6-1f3fc.png","sheet_x":45,"sheet_y":14,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9B6-1F3FD","non_qualified":null,"image":"1f9b6-1f3fd.png","sheet_x":45,"sheet_y":15,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9B6-1F3FE","non_qualified":null,"image":"1f9b6-1f3fe.png","sheet_x":45,"sheet_y":16,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9B6-1F3FF","non_qualified":null,"image":"1f9b6-1f3ff.png","sheet_x":45,"sheet_y":17,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"TOOTH","unified":"1F9B7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9b7.png","sheet_x":45,"sheet_y":18,"short_name":"tooth","short_names":["tooth"],"text":null,"texts":null,"category":"People & Body","subcategory":"body-parts","sort_order":216,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WOMAN SUPERHERO","unified":"1F9B8-200D-2640-FE0F","non_qualified":"1F9B8-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9b8-200d-2640-fe0f.png","sheet_x":45,"sheet_y":19,"short_name":"female_superhero","short_names":["female_superhero"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":369,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9B8-1F3FB-200D-2640-FE0F","non_qualified":"1F9B8-1F3FB-200D-2640","image":"1f9b8-1f3fb-200d-2640-fe0f.png","sheet_x":45,"sheet_y":20,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9B8-1F3FC-200D-2640-FE0F","non_qualified":"1F9B8-1F3FC-200D-2640","image":"1f9b8-1f3fc-200d-2640-fe0f.png","sheet_x":45,"sheet_y":21,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9B8-1F3FD-200D-2640-FE0F","non_qualified":"1F9B8-1F3FD-200D-2640","image":"1f9b8-1f3fd-200d-2640-fe0f.png","sheet_x":45,"sheet_y":22,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9B8-1F3FE-200D-2640-FE0F","non_qualified":"1F9B8-1F3FE-200D-2640","image":"1f9b8-1f3fe-200d-2640-fe0f.png","sheet_x":45,"sheet_y":23,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9B8-1F3FF-200D-2640-FE0F","non_qualified":"1F9B8-1F3FF-200D-2640","image":"1f9b8-1f3ff-200d-2640-fe0f.png","sheet_x":45,"sheet_y":24,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN SUPERHERO","unified":"1F9B8-200D-2642-FE0F","non_qualified":"1F9B8-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9b8-200d-2642-fe0f.png","sheet_x":45,"sheet_y":25,"short_name":"male_superhero","short_names":["male_superhero"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":368,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9B8-1F3FB-200D-2642-FE0F","non_qualified":"1F9B8-1F3FB-200D-2642","image":"1f9b8-1f3fb-200d-2642-fe0f.png","sheet_x":45,"sheet_y":26,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9B8-1F3FC-200D-2642-FE0F","non_qualified":"1F9B8-1F3FC-200D-2642","image":"1f9b8-1f3fc-200d-2642-fe0f.png","sheet_x":45,"sheet_y":27,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9B8-1F3FD-200D-2642-FE0F","non_qualified":"1F9B8-1F3FD-200D-2642","image":"1f9b8-1f3fd-200d-2642-fe0f.png","sheet_x":45,"sheet_y":28,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9B8-1F3FE-200D-2642-FE0F","non_qualified":"1F9B8-1F3FE-200D-2642","image":"1f9b8-1f3fe-200d-2642-fe0f.png","sheet_x":45,"sheet_y":29,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9B8-1F3FF-200D-2642-FE0F","non_qualified":"1F9B8-1F3FF-200D-2642","image":"1f9b8-1f3ff-200d-2642-fe0f.png","sheet_x":45,"sheet_y":30,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"SUPERHERO","unified":"1F9B8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9b8.png","sheet_x":45,"sheet_y":31,"short_name":"superhero","short_names":["superhero"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":367,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9B8-1F3FB","non_qualified":null,"image":"1f9b8-1f3fb.png","sheet_x":45,"sheet_y":32,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9B8-1F3FC","non_qualified":null,"image":"1f9b8-1f3fc.png","sheet_x":45,"sheet_y":33,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9B8-1F3FD","non_qualified":null,"image":"1f9b8-1f3fd.png","sheet_x":45,"sheet_y":34,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9B8-1F3FE","non_qualified":null,"image":"1f9b8-1f3fe.png","sheet_x":45,"sheet_y":35,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9B8-1F3FF","non_qualified":null,"image":"1f9b8-1f3ff.png","sheet_x":45,"sheet_y":36,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN SUPERVILLAIN","unified":"1F9B9-200D-2640-FE0F","non_qualified":"1F9B9-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9b9-200d-2640-fe0f.png","sheet_x":45,"sheet_y":37,"short_name":"female_supervillain","short_names":["female_supervillain"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":372,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9B9-1F3FB-200D-2640-FE0F","non_qualified":"1F9B9-1F3FB-200D-2640","image":"1f9b9-1f3fb-200d-2640-fe0f.png","sheet_x":45,"sheet_y":38,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9B9-1F3FC-200D-2640-FE0F","non_qualified":"1F9B9-1F3FC-200D-2640","image":"1f9b9-1f3fc-200d-2640-fe0f.png","sheet_x":45,"sheet_y":39,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9B9-1F3FD-200D-2640-FE0F","non_qualified":"1F9B9-1F3FD-200D-2640","image":"1f9b9-1f3fd-200d-2640-fe0f.png","sheet_x":45,"sheet_y":40,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9B9-1F3FE-200D-2640-FE0F","non_qualified":"1F9B9-1F3FE-200D-2640","image":"1f9b9-1f3fe-200d-2640-fe0f.png","sheet_x":45,"sheet_y":41,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9B9-1F3FF-200D-2640-FE0F","non_qualified":"1F9B9-1F3FF-200D-2640","image":"1f9b9-1f3ff-200d-2640-fe0f.png","sheet_x":45,"sheet_y":42,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN SUPERVILLAIN","unified":"1F9B9-200D-2642-FE0F","non_qualified":"1F9B9-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9b9-200d-2642-fe0f.png","sheet_x":45,"sheet_y":43,"short_name":"male_supervillain","short_names":["male_supervillain"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":371,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9B9-1F3FB-200D-2642-FE0F","non_qualified":"1F9B9-1F3FB-200D-2642","image":"1f9b9-1f3fb-200d-2642-fe0f.png","sheet_x":45,"sheet_y":44,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9B9-1F3FC-200D-2642-FE0F","non_qualified":"1F9B9-1F3FC-200D-2642","image":"1f9b9-1f3fc-200d-2642-fe0f.png","sheet_x":45,"sheet_y":45,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9B9-1F3FD-200D-2642-FE0F","non_qualified":"1F9B9-1F3FD-200D-2642","image":"1f9b9-1f3fd-200d-2642-fe0f.png","sheet_x":45,"sheet_y":46,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9B9-1F3FE-200D-2642-FE0F","non_qualified":"1F9B9-1F3FE-200D-2642","image":"1f9b9-1f3fe-200d-2642-fe0f.png","sheet_x":45,"sheet_y":47,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9B9-1F3FF-200D-2642-FE0F","non_qualified":"1F9B9-1F3FF-200D-2642","image":"1f9b9-1f3ff-200d-2642-fe0f.png","sheet_x":45,"sheet_y":48,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"SUPERVILLAIN","unified":"1F9B9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9b9.png","sheet_x":45,"sheet_y":49,"short_name":"supervillain","short_names":["supervillain"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":370,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9B9-1F3FB","non_qualified":null,"image":"1f9b9-1f3fb.png","sheet_x":45,"sheet_y":50,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9B9-1F3FC","non_qualified":null,"image":"1f9b9-1f3fc.png","sheet_x":45,"sheet_y":51,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9B9-1F3FD","non_qualified":null,"image":"1f9b9-1f3fd.png","sheet_x":45,"sheet_y":52,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9B9-1F3FE","non_qualified":null,"image":"1f9b9-1f3fe.png","sheet_x":45,"sheet_y":53,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9B9-1F3FF","non_qualified":null,"image":"1f9b9-1f3ff.png","sheet_x":45,"sheet_y":54,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"SAFETY VEST","unified":"1F9BA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9ba.png","sheet_x":45,"sheet_y":55,"short_name":"safety_vest","short_names":["safety_vest"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1114,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EAR WITH HEARING AID","unified":"1F9BB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9bb.png","sheet_x":45,"sheet_y":56,"short_name":"ear_with_hearing_aid","short_names":["ear_with_hearing_aid"],"text":null,"texts":null,"category":"People & Body","subcategory":"body-parts","sort_order":211,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9BB-1F3FB","non_qualified":null,"image":"1f9bb-1f3fb.png","sheet_x":45,"sheet_y":57,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9BB-1F3FC","non_qualified":null,"image":"1f9bb-1f3fc.png","sheet_x":45,"sheet_y":58,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9BB-1F3FD","non_qualified":null,"image":"1f9bb-1f3fd.png","sheet_x":45,"sheet_y":59,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9BB-1F3FE","non_qualified":null,"image":"1f9bb-1f3fe.png","sheet_x":45,"sheet_y":60,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9BB-1F3FF","non_qualified":null,"image":"1f9bb-1f3ff.png","sheet_x":46,"sheet_y":0,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MOTORIZED WHEELCHAIR","unified":"1F9BC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9bc.png","sheet_x":46,"sheet_y":1,"short_name":"motorized_wheelchair","short_names":["motorized_wheelchair"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":905,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MANUAL WHEELCHAIR","unified":"1F9BD","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9bd.png","sheet_x":46,"sheet_y":2,"short_name":"manual_wheelchair","short_names":["manual_wheelchair"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":904,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MECHANICAL ARM","unified":"1F9BE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9be.png","sheet_x":46,"sheet_y":3,"short_name":"mechanical_arm","short_names":["mechanical_arm"],"text":null,"texts":null,"category":"People & Body","subcategory":"body-parts","sort_order":206,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MECHANICAL LEG","unified":"1F9BF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9bf.png","sheet_x":46,"sheet_y":4,"short_name":"mechanical_leg","short_names":["mechanical_leg"],"text":null,"texts":null,"category":"People & Body","subcategory":"body-parts","sort_order":207,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CHEESE WEDGE","unified":"1F9C0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9c0.png","sheet_x":46,"sheet_y":5,"short_name":"cheese_wedge","short_names":["cheese_wedge"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":717,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CUPCAKE","unified":"1F9C1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9c1.png","sheet_x":46,"sheet_y":6,"short_name":"cupcake","short_names":["cupcake"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-sweet","sort_order":772,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SALT SHAKER","unified":"1F9C2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9c2.png","sheet_x":46,"sheet_y":7,"short_name":"salt","short_names":["salt"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":741,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BEVERAGE BOX","unified":"1F9C3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9c3.png","sheet_x":46,"sheet_y":8,"short_name":"beverage_box","short_names":["beverage_box"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"drink","sort_order":796,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GARLIC","unified":"1F9C4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9c4.png","sheet_x":46,"sheet_y":9,"short_name":"garlic","short_names":["garlic"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-vegetable","sort_order":703,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ONION","unified":"1F9C5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9c5.png","sheet_x":46,"sheet_y":10,"short_name":"onion","short_names":["onion"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-vegetable","sort_order":704,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FALAFEL","unified":"1F9C6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9c6.png","sheet_x":46,"sheet_y":11,"short_name":"falafel","short_names":["falafel"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":731,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WAFFLE","unified":"1F9C7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9c7.png","sheet_x":46,"sheet_y":12,"short_name":"waffle","short_names":["waffle"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":716,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BUTTER","unified":"1F9C8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9c8.png","sheet_x":46,"sheet_y":13,"short_name":"butter","short_names":["butter"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":740,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MATE DRINK","unified":"1F9C9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9c9.png","sheet_x":46,"sheet_y":14,"short_name":"mate_drink","short_names":["mate_drink"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"drink","sort_order":797,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ICE CUBE","unified":"1F9CA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9ca.png","sheet_x":46,"sheet_y":15,"short_name":"ice_cube","short_names":["ice_cube"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"drink","sort_order":798,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BUBBLE TEA","unified":"1F9CB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9cb.png","sheet_x":46,"sheet_y":16,"short_name":"bubble_tea","short_names":["bubble_tea"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"drink","sort_order":795,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TROLL","unified":"1F9CC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9cc.png","sheet_x":46,"sheet_y":17,"short_name":"troll","short_names":["troll"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":394,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"WOMAN STANDING","unified":"1F9CD-200D-2640-FE0F","non_qualified":"1F9CD-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9cd-200d-2640-fe0f.png","sheet_x":46,"sheet_y":18,"short_name":"woman_standing","short_names":["woman_standing"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":406,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9CD-1F3FB-200D-2640-FE0F","non_qualified":"1F9CD-1F3FB-200D-2640","image":"1f9cd-1f3fb-200d-2640-fe0f.png","sheet_x":46,"sheet_y":19,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9CD-1F3FC-200D-2640-FE0F","non_qualified":"1F9CD-1F3FC-200D-2640","image":"1f9cd-1f3fc-200d-2640-fe0f.png","sheet_x":46,"sheet_y":20,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9CD-1F3FD-200D-2640-FE0F","non_qualified":"1F9CD-1F3FD-200D-2640","image":"1f9cd-1f3fd-200d-2640-fe0f.png","sheet_x":46,"sheet_y":21,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9CD-1F3FE-200D-2640-FE0F","non_qualified":"1F9CD-1F3FE-200D-2640","image":"1f9cd-1f3fe-200d-2640-fe0f.png","sheet_x":46,"sheet_y":22,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9CD-1F3FF-200D-2640-FE0F","non_qualified":"1F9CD-1F3FF-200D-2640","image":"1f9cd-1f3ff-200d-2640-fe0f.png","sheet_x":46,"sheet_y":23,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN STANDING","unified":"1F9CD-200D-2642-FE0F","non_qualified":"1F9CD-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9cd-200d-2642-fe0f.png","sheet_x":46,"sheet_y":24,"short_name":"man_standing","short_names":["man_standing"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":405,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9CD-1F3FB-200D-2642-FE0F","non_qualified":"1F9CD-1F3FB-200D-2642","image":"1f9cd-1f3fb-200d-2642-fe0f.png","sheet_x":46,"sheet_y":25,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9CD-1F3FC-200D-2642-FE0F","non_qualified":"1F9CD-1F3FC-200D-2642","image":"1f9cd-1f3fc-200d-2642-fe0f.png","sheet_x":46,"sheet_y":26,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9CD-1F3FD-200D-2642-FE0F","non_qualified":"1F9CD-1F3FD-200D-2642","image":"1f9cd-1f3fd-200d-2642-fe0f.png","sheet_x":46,"sheet_y":27,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9CD-1F3FE-200D-2642-FE0F","non_qualified":"1F9CD-1F3FE-200D-2642","image":"1f9cd-1f3fe-200d-2642-fe0f.png","sheet_x":46,"sheet_y":28,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9CD-1F3FF-200D-2642-FE0F","non_qualified":"1F9CD-1F3FF-200D-2642","image":"1f9cd-1f3ff-200d-2642-fe0f.png","sheet_x":46,"sheet_y":29,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"STANDING PERSON","unified":"1F9CD","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9cd.png","sheet_x":46,"sheet_y":30,"short_name":"standing_person","short_names":["standing_person"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":404,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9CD-1F3FB","non_qualified":null,"image":"1f9cd-1f3fb.png","sheet_x":46,"sheet_y":31,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9CD-1F3FC","non_qualified":null,"image":"1f9cd-1f3fc.png","sheet_x":46,"sheet_y":32,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9CD-1F3FD","non_qualified":null,"image":"1f9cd-1f3fd.png","sheet_x":46,"sheet_y":33,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9CD-1F3FE","non_qualified":null,"image":"1f9cd-1f3fe.png","sheet_x":46,"sheet_y":34,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9CD-1F3FF","non_qualified":null,"image":"1f9cd-1f3ff.png","sheet_x":46,"sheet_y":35,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN KNEELING","unified":"1F9CE-200D-2640-FE0F","non_qualified":"1F9CE-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9ce-200d-2640-fe0f.png","sheet_x":46,"sheet_y":36,"short_name":"woman_kneeling","short_names":["woman_kneeling"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":409,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9CE-1F3FB-200D-2640-FE0F","non_qualified":"1F9CE-1F3FB-200D-2640","image":"1f9ce-1f3fb-200d-2640-fe0f.png","sheet_x":46,"sheet_y":37,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9CE-1F3FC-200D-2640-FE0F","non_qualified":"1F9CE-1F3FC-200D-2640","image":"1f9ce-1f3fc-200d-2640-fe0f.png","sheet_x":46,"sheet_y":38,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9CE-1F3FD-200D-2640-FE0F","non_qualified":"1F9CE-1F3FD-200D-2640","image":"1f9ce-1f3fd-200d-2640-fe0f.png","sheet_x":46,"sheet_y":39,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9CE-1F3FE-200D-2640-FE0F","non_qualified":"1F9CE-1F3FE-200D-2640","image":"1f9ce-1f3fe-200d-2640-fe0f.png","sheet_x":46,"sheet_y":40,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9CE-1F3FF-200D-2640-FE0F","non_qualified":"1F9CE-1F3FF-200D-2640","image":"1f9ce-1f3ff-200d-2640-fe0f.png","sheet_x":46,"sheet_y":41,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN KNEELING","unified":"1F9CE-200D-2642-FE0F","non_qualified":"1F9CE-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9ce-200d-2642-fe0f.png","sheet_x":46,"sheet_y":42,"short_name":"man_kneeling","short_names":["man_kneeling"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":408,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9CE-1F3FB-200D-2642-FE0F","non_qualified":"1F9CE-1F3FB-200D-2642","image":"1f9ce-1f3fb-200d-2642-fe0f.png","sheet_x":46,"sheet_y":43,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9CE-1F3FC-200D-2642-FE0F","non_qualified":"1F9CE-1F3FC-200D-2642","image":"1f9ce-1f3fc-200d-2642-fe0f.png","sheet_x":46,"sheet_y":44,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9CE-1F3FD-200D-2642-FE0F","non_qualified":"1F9CE-1F3FD-200D-2642","image":"1f9ce-1f3fd-200d-2642-fe0f.png","sheet_x":46,"sheet_y":45,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9CE-1F3FE-200D-2642-FE0F","non_qualified":"1F9CE-1F3FE-200D-2642","image":"1f9ce-1f3fe-200d-2642-fe0f.png","sheet_x":46,"sheet_y":46,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9CE-1F3FF-200D-2642-FE0F","non_qualified":"1F9CE-1F3FF-200D-2642","image":"1f9ce-1f3ff-200d-2642-fe0f.png","sheet_x":46,"sheet_y":47,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"KNEELING PERSON","unified":"1F9CE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9ce.png","sheet_x":46,"sheet_y":48,"short_name":"kneeling_person","short_names":["kneeling_person"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":407,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9CE-1F3FB","non_qualified":null,"image":"1f9ce-1f3fb.png","sheet_x":46,"sheet_y":49,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9CE-1F3FC","non_qualified":null,"image":"1f9ce-1f3fc.png","sheet_x":46,"sheet_y":50,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9CE-1F3FD","non_qualified":null,"image":"1f9ce-1f3fd.png","sheet_x":46,"sheet_y":51,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9CE-1F3FE","non_qualified":null,"image":"1f9ce-1f3fe.png","sheet_x":46,"sheet_y":52,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9CE-1F3FF","non_qualified":null,"image":"1f9ce-1f3ff.png","sheet_x":46,"sheet_y":53,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"DEAF WOMAN","unified":"1F9CF-200D-2640-FE0F","non_qualified":"1F9CF-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9cf-200d-2640-fe0f.png","sheet_x":46,"sheet_y":54,"short_name":"deaf_woman","short_names":["deaf_woman"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":271,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9CF-1F3FB-200D-2640-FE0F","non_qualified":"1F9CF-1F3FB-200D-2640","image":"1f9cf-1f3fb-200d-2640-fe0f.png","sheet_x":46,"sheet_y":55,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9CF-1F3FC-200D-2640-FE0F","non_qualified":"1F9CF-1F3FC-200D-2640","image":"1f9cf-1f3fc-200d-2640-fe0f.png","sheet_x":46,"sheet_y":56,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9CF-1F3FD-200D-2640-FE0F","non_qualified":"1F9CF-1F3FD-200D-2640","image":"1f9cf-1f3fd-200d-2640-fe0f.png","sheet_x":46,"sheet_y":57,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9CF-1F3FE-200D-2640-FE0F","non_qualified":"1F9CF-1F3FE-200D-2640","image":"1f9cf-1f3fe-200d-2640-fe0f.png","sheet_x":46,"sheet_y":58,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9CF-1F3FF-200D-2640-FE0F","non_qualified":"1F9CF-1F3FF-200D-2640","image":"1f9cf-1f3ff-200d-2640-fe0f.png","sheet_x":46,"sheet_y":59,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"DEAF MAN","unified":"1F9CF-200D-2642-FE0F","non_qualified":"1F9CF-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9cf-200d-2642-fe0f.png","sheet_x":46,"sheet_y":60,"short_name":"deaf_man","short_names":["deaf_man"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":270,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9CF-1F3FB-200D-2642-FE0F","non_qualified":"1F9CF-1F3FB-200D-2642","image":"1f9cf-1f3fb-200d-2642-fe0f.png","sheet_x":47,"sheet_y":0,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9CF-1F3FC-200D-2642-FE0F","non_qualified":"1F9CF-1F3FC-200D-2642","image":"1f9cf-1f3fc-200d-2642-fe0f.png","sheet_x":47,"sheet_y":1,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9CF-1F3FD-200D-2642-FE0F","non_qualified":"1F9CF-1F3FD-200D-2642","image":"1f9cf-1f3fd-200d-2642-fe0f.png","sheet_x":47,"sheet_y":2,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9CF-1F3FE-200D-2642-FE0F","non_qualified":"1F9CF-1F3FE-200D-2642","image":"1f9cf-1f3fe-200d-2642-fe0f.png","sheet_x":47,"sheet_y":3,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9CF-1F3FF-200D-2642-FE0F","non_qualified":"1F9CF-1F3FF-200D-2642","image":"1f9cf-1f3ff-200d-2642-fe0f.png","sheet_x":47,"sheet_y":4,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"DEAF PERSON","unified":"1F9CF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9cf.png","sheet_x":47,"sheet_y":5,"short_name":"deaf_person","short_names":["deaf_person"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":269,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9CF-1F3FB","non_qualified":null,"image":"1f9cf-1f3fb.png","sheet_x":47,"sheet_y":6,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9CF-1F3FC","non_qualified":null,"image":"1f9cf-1f3fc.png","sheet_x":47,"sheet_y":7,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9CF-1F3FD","non_qualified":null,"image":"1f9cf-1f3fd.png","sheet_x":47,"sheet_y":8,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9CF-1F3FE","non_qualified":null,"image":"1f9cf-1f3fe.png","sheet_x":47,"sheet_y":9,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9CF-1F3FF","non_qualified":null,"image":"1f9cf-1f3ff.png","sheet_x":47,"sheet_y":10,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"FACE WITH MONOCLE","unified":"1F9D0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d0.png","sheet_x":47,"sheet_y":11,"short_name":"face_with_monocle","short_names":["face_with_monocle"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-glasses","sort_order":72,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FARMER","unified":"1F9D1-200D-1F33E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f33e.png","sheet_x":47,"sheet_y":12,"short_name":"farmer","short_names":["farmer"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":293,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F33E","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f33e.png","sheet_x":47,"sheet_y":13,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F33E","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f33e.png","sheet_x":47,"sheet_y":14,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F33E","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f33e.png","sheet_x":47,"sheet_y":15,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F33E","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f33e.png","sheet_x":47,"sheet_y":16,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F33E","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f33e.png","sheet_x":47,"sheet_y":17,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"COOK","unified":"1F9D1-200D-1F373","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f373.png","sheet_x":47,"sheet_y":18,"short_name":"cook","short_names":["cook"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":296,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F373","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f373.png","sheet_x":47,"sheet_y":19,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F373","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f373.png","sheet_x":47,"sheet_y":20,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F373","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f373.png","sheet_x":47,"sheet_y":21,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F373","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f373.png","sheet_x":47,"sheet_y":22,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F373","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f373.png","sheet_x":47,"sheet_y":23,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"PERSON FEEDING BABY","unified":"1F9D1-200D-1F37C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f37c.png","sheet_x":47,"sheet_y":24,"short_name":"person_feeding_baby","short_names":["person_feeding_baby"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":362,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F37C","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f37c.png","sheet_x":47,"sheet_y":25,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F37C","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f37c.png","sheet_x":47,"sheet_y":26,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F37C","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f37c.png","sheet_x":47,"sheet_y":27,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F37C","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f37c.png","sheet_x":47,"sheet_y":28,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F37C","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f37c.png","sheet_x":47,"sheet_y":29,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MX CLAUS","unified":"1F9D1-200D-1F384","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f384.png","sheet_x":47,"sheet_y":30,"short_name":"mx_claus","short_names":["mx_claus"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":366,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F384","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f384.png","sheet_x":47,"sheet_y":31,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F384","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f384.png","sheet_x":47,"sheet_y":32,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F384","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f384.png","sheet_x":47,"sheet_y":33,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F384","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f384.png","sheet_x":47,"sheet_y":34,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F384","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f384.png","sheet_x":47,"sheet_y":35,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"STUDENT","unified":"1F9D1-200D-1F393","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f393.png","sheet_x":47,"sheet_y":36,"short_name":"student","short_names":["student"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":284,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F393","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f393.png","sheet_x":47,"sheet_y":37,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F393","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f393.png","sheet_x":47,"sheet_y":38,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F393","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f393.png","sheet_x":47,"sheet_y":39,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F393","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f393.png","sheet_x":47,"sheet_y":40,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F393","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f393.png","sheet_x":47,"sheet_y":41,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"SINGER","unified":"1F9D1-200D-1F3A4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f3a4.png","sheet_x":47,"sheet_y":42,"short_name":"singer","short_names":["singer"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":314,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F3A4","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f3a4.png","sheet_x":47,"sheet_y":43,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F3A4","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f3a4.png","sheet_x":47,"sheet_y":44,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F3A4","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f3a4.png","sheet_x":47,"sheet_y":45,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F3A4","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f3a4.png","sheet_x":47,"sheet_y":46,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F3A4","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f3a4.png","sheet_x":47,"sheet_y":47,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"ARTIST","unified":"1F9D1-200D-1F3A8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f3a8.png","sheet_x":47,"sheet_y":48,"short_name":"artist","short_names":["artist"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":317,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F3A8","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f3a8.png","sheet_x":47,"sheet_y":49,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F3A8","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f3a8.png","sheet_x":47,"sheet_y":50,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F3A8","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f3a8.png","sheet_x":47,"sheet_y":51,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F3A8","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f3a8.png","sheet_x":47,"sheet_y":52,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F3A8","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f3a8.png","sheet_x":47,"sheet_y":53,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"TEACHER","unified":"1F9D1-200D-1F3EB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f3eb.png","sheet_x":47,"sheet_y":54,"short_name":"teacher","short_names":["teacher"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":287,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F3EB","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f3eb.png","sheet_x":47,"sheet_y":55,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F3EB","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f3eb.png","sheet_x":47,"sheet_y":56,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F3EB","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f3eb.png","sheet_x":47,"sheet_y":57,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F3EB","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f3eb.png","sheet_x":47,"sheet_y":58,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F3EB","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f3eb.png","sheet_x":47,"sheet_y":59,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"FACTORY WORKER","unified":"1F9D1-200D-1F3ED","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f3ed.png","sheet_x":47,"sheet_y":60,"short_name":"factory_worker","short_names":["factory_worker"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":302,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F3ED","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f3ed.png","sheet_x":48,"sheet_y":0,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F3ED","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f3ed.png","sheet_x":48,"sheet_y":1,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F3ED","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f3ed.png","sheet_x":48,"sheet_y":2,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F3ED","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f3ed.png","sheet_x":48,"sheet_y":3,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F3ED","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f3ed.png","sheet_x":48,"sheet_y":4,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"TECHNOLOGIST","unified":"1F9D1-200D-1F4BB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f4bb.png","sheet_x":48,"sheet_y":5,"short_name":"technologist","short_names":["technologist"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":311,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F4BB","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f4bb.png","sheet_x":48,"sheet_y":6,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F4BB","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f4bb.png","sheet_x":48,"sheet_y":7,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F4BB","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f4bb.png","sheet_x":48,"sheet_y":8,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F4BB","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f4bb.png","sheet_x":48,"sheet_y":9,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F4BB","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f4bb.png","sheet_x":48,"sheet_y":10,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"OFFICE WORKER","unified":"1F9D1-200D-1F4BC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f4bc.png","sheet_x":48,"sheet_y":11,"short_name":"office_worker","short_names":["office_worker"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":305,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F4BC","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f4bc.png","sheet_x":48,"sheet_y":12,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F4BC","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f4bc.png","sheet_x":48,"sheet_y":13,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F4BC","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f4bc.png","sheet_x":48,"sheet_y":14,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F4BC","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f4bc.png","sheet_x":48,"sheet_y":15,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F4BC","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f4bc.png","sheet_x":48,"sheet_y":16,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MECHANIC","unified":"1F9D1-200D-1F527","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f527.png","sheet_x":48,"sheet_y":17,"short_name":"mechanic","short_names":["mechanic"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":299,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F527","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f527.png","sheet_x":48,"sheet_y":18,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F527","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f527.png","sheet_x":48,"sheet_y":19,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F527","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f527.png","sheet_x":48,"sheet_y":20,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F527","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f527.png","sheet_x":48,"sheet_y":21,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F527","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f527.png","sheet_x":48,"sheet_y":22,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"SCIENTIST","unified":"1F9D1-200D-1F52C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f52c.png","sheet_x":48,"sheet_y":23,"short_name":"scientist","short_names":["scientist"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":308,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F52C","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f52c.png","sheet_x":48,"sheet_y":24,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F52C","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f52c.png","sheet_x":48,"sheet_y":25,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F52C","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f52c.png","sheet_x":48,"sheet_y":26,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F52C","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f52c.png","sheet_x":48,"sheet_y":27,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F52C","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f52c.png","sheet_x":48,"sheet_y":28,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"ASTRONAUT","unified":"1F9D1-200D-1F680","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f680.png","sheet_x":48,"sheet_y":29,"short_name":"astronaut","short_names":["astronaut"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":323,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F680","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f680.png","sheet_x":48,"sheet_y":30,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F680","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f680.png","sheet_x":48,"sheet_y":31,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F680","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f680.png","sheet_x":48,"sheet_y":32,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F680","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f680.png","sheet_x":48,"sheet_y":33,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F680","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f680.png","sheet_x":48,"sheet_y":34,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"FIREFIGHTER","unified":"1F9D1-200D-1F692","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f692.png","sheet_x":48,"sheet_y":35,"short_name":"firefighter","short_names":["firefighter"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":326,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F692","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f692.png","sheet_x":48,"sheet_y":36,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F692","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f692.png","sheet_x":48,"sheet_y":37,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F692","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f692.png","sheet_x":48,"sheet_y":38,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F692","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f692.png","sheet_x":48,"sheet_y":39,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F692","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f692.png","sheet_x":48,"sheet_y":40,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"PEOPLE HOLDING HANDS","unified":"1F9D1-200D-1F91D-200D-1F9D1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f91d-200d-1f9d1.png","sheet_x":48,"sheet_y":41,"short_name":"people_holding_hands","short_names":["people_holding_hands"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":482,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB-1F3FB":{"unified":"1F9D1-1F3FB-200D-1F91D-200D-1F9D1-1F3FB","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f91d-200d-1f9d1-1f3fb.png","sheet_x":48,"sheet_y":42,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FC":{"unified":"1F9D1-1F3FB-200D-1F91D-200D-1F9D1-1F3FC","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f91d-200d-1f9d1-1f3fc.png","sheet_x":48,"sheet_y":43,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FB-1F3FD":{"unified":"1F9D1-1F3FB-200D-1F91D-200D-1F9D1-1F3FD","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f91d-200d-1f9d1-1f3fd.png","sheet_x":48,"sheet_y":44,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FB-1F3FE":{"unified":"1F9D1-1F3FB-200D-1F91D-200D-1F9D1-1F3FE","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f91d-200d-1f9d1-1f3fe.png","sheet_x":48,"sheet_y":45,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FB-1F3FF":{"unified":"1F9D1-1F3FB-200D-1F91D-200D-1F9D1-1F3FF","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f91d-200d-1f9d1-1f3ff.png","sheet_x":48,"sheet_y":46,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FB":{"unified":"1F9D1-1F3FC-200D-1F91D-200D-1F9D1-1F3FB","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f91d-200d-1f9d1-1f3fb.png","sheet_x":48,"sheet_y":47,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FC":{"unified":"1F9D1-1F3FC-200D-1F91D-200D-1F9D1-1F3FC","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f91d-200d-1f9d1-1f3fc.png","sheet_x":48,"sheet_y":48,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FD":{"unified":"1F9D1-1F3FC-200D-1F91D-200D-1F9D1-1F3FD","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f91d-200d-1f9d1-1f3fd.png","sheet_x":48,"sheet_y":49,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FE":{"unified":"1F9D1-1F3FC-200D-1F91D-200D-1F9D1-1F3FE","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f91d-200d-1f9d1-1f3fe.png","sheet_x":48,"sheet_y":50,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC-1F3FF":{"unified":"1F9D1-1F3FC-200D-1F91D-200D-1F9D1-1F3FF","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f91d-200d-1f9d1-1f3ff.png","sheet_x":48,"sheet_y":51,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FB":{"unified":"1F9D1-1F3FD-200D-1F91D-200D-1F9D1-1F3FB","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f91d-200d-1f9d1-1f3fb.png","sheet_x":48,"sheet_y":52,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FC":{"unified":"1F9D1-1F3FD-200D-1F91D-200D-1F9D1-1F3FC","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f91d-200d-1f9d1-1f3fc.png","sheet_x":48,"sheet_y":53,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FD":{"unified":"1F9D1-1F3FD-200D-1F91D-200D-1F9D1-1F3FD","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f91d-200d-1f9d1-1f3fd.png","sheet_x":48,"sheet_y":54,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FE":{"unified":"1F9D1-1F3FD-200D-1F91D-200D-1F9D1-1F3FE","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f91d-200d-1f9d1-1f3fe.png","sheet_x":48,"sheet_y":55,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD-1F3FF":{"unified":"1F9D1-1F3FD-200D-1F91D-200D-1F9D1-1F3FF","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f91d-200d-1f9d1-1f3ff.png","sheet_x":48,"sheet_y":56,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE-1F3FB":{"unified":"1F9D1-1F3FE-200D-1F91D-200D-1F9D1-1F3FB","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f91d-200d-1f9d1-1f3fb.png","sheet_x":48,"sheet_y":57,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FC":{"unified":"1F9D1-1F3FE-200D-1F91D-200D-1F9D1-1F3FC","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f91d-200d-1f9d1-1f3fc.png","sheet_x":48,"sheet_y":58,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FD":{"unified":"1F9D1-1F3FE-200D-1F91D-200D-1F9D1-1F3FD","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f91d-200d-1f9d1-1f3fd.png","sheet_x":48,"sheet_y":59,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FE":{"unified":"1F9D1-1F3FE-200D-1F91D-200D-1F9D1-1F3FE","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f91d-200d-1f9d1-1f3fe.png","sheet_x":48,"sheet_y":60,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FF":{"unified":"1F9D1-1F3FE-200D-1F91D-200D-1F9D1-1F3FF","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f91d-200d-1f9d1-1f3ff.png","sheet_x":49,"sheet_y":0,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF-1F3FB":{"unified":"1F9D1-1F3FF-200D-1F91D-200D-1F9D1-1F3FB","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f91d-200d-1f9d1-1f3fb.png","sheet_x":49,"sheet_y":1,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FC":{"unified":"1F9D1-1F3FF-200D-1F91D-200D-1F9D1-1F3FC","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f91d-200d-1f9d1-1f3fc.png","sheet_x":49,"sheet_y":2,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FD":{"unified":"1F9D1-1F3FF-200D-1F91D-200D-1F9D1-1F3FD","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f91d-200d-1f9d1-1f3fd.png","sheet_x":49,"sheet_y":3,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FE":{"unified":"1F9D1-1F3FF-200D-1F91D-200D-1F9D1-1F3FE","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f91d-200d-1f9d1-1f3fe.png","sheet_x":49,"sheet_y":4,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FF":{"unified":"1F9D1-1F3FF-200D-1F91D-200D-1F9D1-1F3FF","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f91d-200d-1f9d1-1f3ff.png","sheet_x":49,"sheet_y":5,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"PERSON WITH WHITE CANE","unified":"1F9D1-200D-1F9AF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f9af.png","sheet_x":49,"sheet_y":6,"short_name":"person_with_probing_cane","short_names":["person_with_probing_cane"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":410,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F9AF","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f9af.png","sheet_x":49,"sheet_y":7,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F9AF","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f9af.png","sheet_x":49,"sheet_y":8,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F9AF","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f9af.png","sheet_x":49,"sheet_y":9,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F9AF","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f9af.png","sheet_x":49,"sheet_y":10,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F9AF","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f9af.png","sheet_x":49,"sheet_y":11,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"PERSON: RED HAIR","unified":"1F9D1-200D-1F9B0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f9b0.png","sheet_x":49,"sheet_y":12,"short_name":"red_haired_person","short_names":["red_haired_person"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":239,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F9B0","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f9b0.png","sheet_x":49,"sheet_y":13,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F9B0","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f9b0.png","sheet_x":49,"sheet_y":14,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F9B0","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f9b0.png","sheet_x":49,"sheet_y":15,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F9B0","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f9b0.png","sheet_x":49,"sheet_y":16,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F9B0","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f9b0.png","sheet_x":49,"sheet_y":17,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"PERSON: CURLY HAIR","unified":"1F9D1-200D-1F9B1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f9b1.png","sheet_x":49,"sheet_y":18,"short_name":"curly_haired_person","short_names":["curly_haired_person"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":241,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F9B1","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f9b1.png","sheet_x":49,"sheet_y":19,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F9B1","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f9b1.png","sheet_x":49,"sheet_y":20,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F9B1","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f9b1.png","sheet_x":49,"sheet_y":21,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F9B1","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f9b1.png","sheet_x":49,"sheet_y":22,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F9B1","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f9b1.png","sheet_x":49,"sheet_y":23,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"PERSON: BALD","unified":"1F9D1-200D-1F9B2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f9b2.png","sheet_x":49,"sheet_y":24,"short_name":"bald_person","short_names":["bald_person"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":245,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F9B2","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f9b2.png","sheet_x":49,"sheet_y":25,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F9B2","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f9b2.png","sheet_x":49,"sheet_y":26,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F9B2","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f9b2.png","sheet_x":49,"sheet_y":27,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F9B2","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f9b2.png","sheet_x":49,"sheet_y":28,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F9B2","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f9b2.png","sheet_x":49,"sheet_y":29,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false}}},{"name":"PERSON: WHITE HAIR","unified":"1F9D1-200D-1F9B3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f9b3.png","sheet_x":49,"sheet_y":30,"short_name":"white_haired_person","short_names":["white_haired_person"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":243,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F9B3","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f9b3.png","sheet_x":49,"sheet_y":31,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F9B3","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f9b3.png","sheet_x":49,"sheet_y":32,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F9B3","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f9b3.png","sheet_x":49,"sheet_y":33,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F9B3","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f9b3.png","sheet_x":49,"sheet_y":34,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F9B3","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f9b3.png","sheet_x":49,"sheet_y":35,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false}}},{"name":"PERSON IN MOTORIZED WHEELCHAIR","unified":"1F9D1-200D-1F9BC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f9bc.png","sheet_x":49,"sheet_y":36,"short_name":"person_in_motorized_wheelchair","short_names":["person_in_motorized_wheelchair"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":413,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F9BC","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f9bc.png","sheet_x":49,"sheet_y":37,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F9BC","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f9bc.png","sheet_x":49,"sheet_y":38,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F9BC","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f9bc.png","sheet_x":49,"sheet_y":39,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F9BC","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f9bc.png","sheet_x":49,"sheet_y":40,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F9BC","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f9bc.png","sheet_x":49,"sheet_y":41,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"PERSON IN MANUAL WHEELCHAIR","unified":"1F9D1-200D-1F9BD","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f9bd.png","sheet_x":49,"sheet_y":42,"short_name":"person_in_manual_wheelchair","short_names":["person_in_manual_wheelchair"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":416,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F9BD","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f9bd.png","sheet_x":49,"sheet_y":43,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F9BD","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f9bd.png","sheet_x":49,"sheet_y":44,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F9BD","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f9bd.png","sheet_x":49,"sheet_y":45,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F9BD","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f9bd.png","sheet_x":49,"sheet_y":46,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F9BD","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f9bd.png","sheet_x":49,"sheet_y":47,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"HEALTH WORKER","unified":"1F9D1-200D-2695-FE0F","non_qualified":"1F9D1-200D-2695","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-2695-fe0f.png","sheet_x":49,"sheet_y":48,"short_name":"health_worker","short_names":["health_worker"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":281,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-2695-FE0F","non_qualified":"1F9D1-1F3FB-200D-2695","image":"1f9d1-1f3fb-200d-2695-fe0f.png","sheet_x":49,"sheet_y":49,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-2695-FE0F","non_qualified":"1F9D1-1F3FC-200D-2695","image":"1f9d1-1f3fc-200d-2695-fe0f.png","sheet_x":49,"sheet_y":50,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-2695-FE0F","non_qualified":"1F9D1-1F3FD-200D-2695","image":"1f9d1-1f3fd-200d-2695-fe0f.png","sheet_x":49,"sheet_y":51,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-2695-FE0F","non_qualified":"1F9D1-1F3FE-200D-2695","image":"1f9d1-1f3fe-200d-2695-fe0f.png","sheet_x":49,"sheet_y":52,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-2695-FE0F","non_qualified":"1F9D1-1F3FF-200D-2695","image":"1f9d1-1f3ff-200d-2695-fe0f.png","sheet_x":49,"sheet_y":53,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"JUDGE","unified":"1F9D1-200D-2696-FE0F","non_qualified":"1F9D1-200D-2696","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-2696-fe0f.png","sheet_x":49,"sheet_y":54,"short_name":"judge","short_names":["judge"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":290,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-2696-FE0F","non_qualified":"1F9D1-1F3FB-200D-2696","image":"1f9d1-1f3fb-200d-2696-fe0f.png","sheet_x":49,"sheet_y":55,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-2696-FE0F","non_qualified":"1F9D1-1F3FC-200D-2696","image":"1f9d1-1f3fc-200d-2696-fe0f.png","sheet_x":49,"sheet_y":56,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-2696-FE0F","non_qualified":"1F9D1-1F3FD-200D-2696","image":"1f9d1-1f3fd-200d-2696-fe0f.png","sheet_x":49,"sheet_y":57,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-2696-FE0F","non_qualified":"1F9D1-1F3FE-200D-2696","image":"1f9d1-1f3fe-200d-2696-fe0f.png","sheet_x":49,"sheet_y":58,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-2696-FE0F","non_qualified":"1F9D1-1F3FF-200D-2696","image":"1f9d1-1f3ff-200d-2696-fe0f.png","sheet_x":49,"sheet_y":59,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"PILOT","unified":"1F9D1-200D-2708-FE0F","non_qualified":"1F9D1-200D-2708","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-2708-fe0f.png","sheet_x":49,"sheet_y":60,"short_name":"pilot","short_names":["pilot"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":320,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-2708-FE0F","non_qualified":"1F9D1-1F3FB-200D-2708","image":"1f9d1-1f3fb-200d-2708-fe0f.png","sheet_x":50,"sheet_y":0,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-2708-FE0F","non_qualified":"1F9D1-1F3FC-200D-2708","image":"1f9d1-1f3fc-200d-2708-fe0f.png","sheet_x":50,"sheet_y":1,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-2708-FE0F","non_qualified":"1F9D1-1F3FD-200D-2708","image":"1f9d1-1f3fd-200d-2708-fe0f.png","sheet_x":50,"sheet_y":2,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-2708-FE0F","non_qualified":"1F9D1-1F3FE-200D-2708","image":"1f9d1-1f3fe-200d-2708-fe0f.png","sheet_x":50,"sheet_y":3,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-2708-FE0F","non_qualified":"1F9D1-1F3FF-200D-2708","image":"1f9d1-1f3ff-200d-2708-fe0f.png","sheet_x":50,"sheet_y":4,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"ADULT","unified":"1F9D1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1.png","sheet_x":50,"sheet_y":5,"short_name":"adult","short_names":["adult"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":227,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB","non_qualified":null,"image":"1f9d1-1f3fb.png","sheet_x":50,"sheet_y":6,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC","non_qualified":null,"image":"1f9d1-1f3fc.png","sheet_x":50,"sheet_y":7,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD","non_qualified":null,"image":"1f9d1-1f3fd.png","sheet_x":50,"sheet_y":8,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE","non_qualified":null,"image":"1f9d1-1f3fe.png","sheet_x":50,"sheet_y":9,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF","non_qualified":null,"image":"1f9d1-1f3ff.png","sheet_x":50,"sheet_y":10,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"CHILD","unified":"1F9D2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d2.png","sheet_x":50,"sheet_y":11,"short_name":"child","short_names":["child"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":224,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D2-1F3FB","non_qualified":null,"image":"1f9d2-1f3fb.png","sheet_x":50,"sheet_y":12,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D2-1F3FC","non_qualified":null,"image":"1f9d2-1f3fc.png","sheet_x":50,"sheet_y":13,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D2-1F3FD","non_qualified":null,"image":"1f9d2-1f3fd.png","sheet_x":50,"sheet_y":14,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D2-1F3FE","non_qualified":null,"image":"1f9d2-1f3fe.png","sheet_x":50,"sheet_y":15,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D2-1F3FF","non_qualified":null,"image":"1f9d2-1f3ff.png","sheet_x":50,"sheet_y":16,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"OLDER ADULT","unified":"1F9D3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d3.png","sheet_x":50,"sheet_y":17,"short_name":"older_adult","short_names":["older_adult"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":248,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D3-1F3FB","non_qualified":null,"image":"1f9d3-1f3fb.png","sheet_x":50,"sheet_y":18,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D3-1F3FC","non_qualified":null,"image":"1f9d3-1f3fc.png","sheet_x":50,"sheet_y":19,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D3-1F3FD","non_qualified":null,"image":"1f9d3-1f3fd.png","sheet_x":50,"sheet_y":20,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D3-1F3FE","non_qualified":null,"image":"1f9d3-1f3fe.png","sheet_x":50,"sheet_y":21,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D3-1F3FF","non_qualified":null,"image":"1f9d3-1f3ff.png","sheet_x":50,"sheet_y":22,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN: BEARD","unified":"1F9D4-200D-2640-FE0F","non_qualified":"1F9D4-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d4-200d-2640-fe0f.png","sheet_x":50,"sheet_y":23,"short_name":"woman_with_beard","short_names":["woman_with_beard"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":232,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"1F9D4-1F3FB-200D-2640-FE0F","non_qualified":"1F9D4-1F3FB-200D-2640","image":"1f9d4-1f3fb-200d-2640-fe0f.png","sheet_x":50,"sheet_y":24,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC":{"unified":"1F9D4-1F3FC-200D-2640-FE0F","non_qualified":"1F9D4-1F3FC-200D-2640","image":"1f9d4-1f3fc-200d-2640-fe0f.png","sheet_x":50,"sheet_y":25,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD":{"unified":"1F9D4-1F3FD-200D-2640-FE0F","non_qualified":"1F9D4-1F3FD-200D-2640","image":"1f9d4-1f3fd-200d-2640-fe0f.png","sheet_x":50,"sheet_y":26,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE":{"unified":"1F9D4-1F3FE-200D-2640-FE0F","non_qualified":"1F9D4-1F3FE-200D-2640","image":"1f9d4-1f3fe-200d-2640-fe0f.png","sheet_x":50,"sheet_y":27,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF":{"unified":"1F9D4-1F3FF-200D-2640-FE0F","non_qualified":"1F9D4-1F3FF-200D-2640","image":"1f9d4-1f3ff-200d-2640-fe0f.png","sheet_x":50,"sheet_y":28,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false}}},{"name":"MAN: BEARD","unified":"1F9D4-200D-2642-FE0F","non_qualified":"1F9D4-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d4-200d-2642-fe0f.png","sheet_x":50,"sheet_y":29,"short_name":"man_with_beard","short_names":["man_with_beard"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":231,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"1F9D4-1F3FB-200D-2642-FE0F","non_qualified":"1F9D4-1F3FB-200D-2642","image":"1f9d4-1f3fb-200d-2642-fe0f.png","sheet_x":50,"sheet_y":30,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC":{"unified":"1F9D4-1F3FC-200D-2642-FE0F","non_qualified":"1F9D4-1F3FC-200D-2642","image":"1f9d4-1f3fc-200d-2642-fe0f.png","sheet_x":50,"sheet_y":31,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD":{"unified":"1F9D4-1F3FD-200D-2642-FE0F","non_qualified":"1F9D4-1F3FD-200D-2642","image":"1f9d4-1f3fd-200d-2642-fe0f.png","sheet_x":50,"sheet_y":32,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE":{"unified":"1F9D4-1F3FE-200D-2642-FE0F","non_qualified":"1F9D4-1F3FE-200D-2642","image":"1f9d4-1f3fe-200d-2642-fe0f.png","sheet_x":50,"sheet_y":33,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF":{"unified":"1F9D4-1F3FF-200D-2642-FE0F","non_qualified":"1F9D4-1F3FF-200D-2642","image":"1f9d4-1f3ff-200d-2642-fe0f.png","sheet_x":50,"sheet_y":34,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false}}},{"name":"BEARDED PERSON","unified":"1F9D4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d4.png","sheet_x":50,"sheet_y":35,"short_name":"bearded_person","short_names":["bearded_person"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":230,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D4-1F3FB","non_qualified":null,"image":"1f9d4-1f3fb.png","sheet_x":50,"sheet_y":36,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D4-1F3FC","non_qualified":null,"image":"1f9d4-1f3fc.png","sheet_x":50,"sheet_y":37,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D4-1F3FD","non_qualified":null,"image":"1f9d4-1f3fd.png","sheet_x":50,"sheet_y":38,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D4-1F3FE","non_qualified":null,"image":"1f9d4-1f3fe.png","sheet_x":50,"sheet_y":39,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D4-1F3FF","non_qualified":null,"image":"1f9d4-1f3ff.png","sheet_x":50,"sheet_y":40,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"PERSON WITH HEADSCARF","unified":"1F9D5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d5.png","sheet_x":50,"sheet_y":41,"short_name":"person_with_headscarf","short_names":["person_with_headscarf"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":349,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D5-1F3FB","non_qualified":null,"image":"1f9d5-1f3fb.png","sheet_x":50,"sheet_y":42,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D5-1F3FC","non_qualified":null,"image":"1f9d5-1f3fc.png","sheet_x":50,"sheet_y":43,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D5-1F3FD","non_qualified":null,"image":"1f9d5-1f3fd.png","sheet_x":50,"sheet_y":44,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D5-1F3FE","non_qualified":null,"image":"1f9d5-1f3fe.png","sheet_x":50,"sheet_y":45,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D5-1F3FF","non_qualified":null,"image":"1f9d5-1f3ff.png","sheet_x":50,"sheet_y":46,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN IN STEAMY ROOM","unified":"1F9D6-200D-2640-FE0F","non_qualified":"1F9D6-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d6-200d-2640-fe0f.png","sheet_x":50,"sheet_y":47,"short_name":"woman_in_steamy_room","short_names":["woman_in_steamy_room"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":430,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D6-1F3FB-200D-2640-FE0F","non_qualified":"1F9D6-1F3FB-200D-2640","image":"1f9d6-1f3fb-200d-2640-fe0f.png","sheet_x":50,"sheet_y":48,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D6-1F3FC-200D-2640-FE0F","non_qualified":"1F9D6-1F3FC-200D-2640","image":"1f9d6-1f3fc-200d-2640-fe0f.png","sheet_x":50,"sheet_y":49,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D6-1F3FD-200D-2640-FE0F","non_qualified":"1F9D6-1F3FD-200D-2640","image":"1f9d6-1f3fd-200d-2640-fe0f.png","sheet_x":50,"sheet_y":50,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D6-1F3FE-200D-2640-FE0F","non_qualified":"1F9D6-1F3FE-200D-2640","image":"1f9d6-1f3fe-200d-2640-fe0f.png","sheet_x":50,"sheet_y":51,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D6-1F3FF-200D-2640-FE0F","non_qualified":"1F9D6-1F3FF-200D-2640","image":"1f9d6-1f3ff-200d-2640-fe0f.png","sheet_x":50,"sheet_y":52,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN IN STEAMY ROOM","unified":"1F9D6-200D-2642-FE0F","non_qualified":"1F9D6-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d6-200d-2642-fe0f.png","sheet_x":50,"sheet_y":53,"short_name":"man_in_steamy_room","short_names":["man_in_steamy_room"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":429,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D6-1F3FB-200D-2642-FE0F","non_qualified":"1F9D6-1F3FB-200D-2642","image":"1f9d6-1f3fb-200d-2642-fe0f.png","sheet_x":50,"sheet_y":54,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9D6-1F3FB"},"1F3FC":{"unified":"1F9D6-1F3FC-200D-2642-FE0F","non_qualified":"1F9D6-1F3FC-200D-2642","image":"1f9d6-1f3fc-200d-2642-fe0f.png","sheet_x":50,"sheet_y":55,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9D6-1F3FC"},"1F3FD":{"unified":"1F9D6-1F3FD-200D-2642-FE0F","non_qualified":"1F9D6-1F3FD-200D-2642","image":"1f9d6-1f3fd-200d-2642-fe0f.png","sheet_x":50,"sheet_y":56,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9D6-1F3FD"},"1F3FE":{"unified":"1F9D6-1F3FE-200D-2642-FE0F","non_qualified":"1F9D6-1F3FE-200D-2642","image":"1f9d6-1f3fe-200d-2642-fe0f.png","sheet_x":50,"sheet_y":57,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9D6-1F3FE"},"1F3FF":{"unified":"1F9D6-1F3FF-200D-2642-FE0F","non_qualified":"1F9D6-1F3FF-200D-2642","image":"1f9d6-1f3ff-200d-2642-fe0f.png","sheet_x":50,"sheet_y":58,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9D6-1F3FF"}},"obsoletes":"1F9D6"},{"name":"PERSON IN STEAMY ROOM","unified":"1F9D6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d6.png","sheet_x":50,"sheet_y":59,"short_name":"person_in_steamy_room","short_names":["person_in_steamy_room"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":428,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D6-1F3FB","non_qualified":null,"image":"1f9d6-1f3fb.png","sheet_x":50,"sheet_y":60,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9D6-1F3FB-200D-2642-FE0F"},"1F3FC":{"unified":"1F9D6-1F3FC","non_qualified":null,"image":"1f9d6-1f3fc.png","sheet_x":51,"sheet_y":0,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9D6-1F3FC-200D-2642-FE0F"},"1F3FD":{"unified":"1F9D6-1F3FD","non_qualified":null,"image":"1f9d6-1f3fd.png","sheet_x":51,"sheet_y":1,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9D6-1F3FD-200D-2642-FE0F"},"1F3FE":{"unified":"1F9D6-1F3FE","non_qualified":null,"image":"1f9d6-1f3fe.png","sheet_x":51,"sheet_y":2,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9D6-1F3FE-200D-2642-FE0F"},"1F3FF":{"unified":"1F9D6-1F3FF","non_qualified":null,"image":"1f9d6-1f3ff.png","sheet_x":51,"sheet_y":3,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9D6-1F3FF-200D-2642-FE0F"}},"obsoleted_by":"1F9D6-200D-2642-FE0F"},{"name":"WOMAN CLIMBING","unified":"1F9D7-200D-2640-FE0F","non_qualified":"1F9D7-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d7-200d-2640-fe0f.png","sheet_x":51,"sheet_y":4,"short_name":"woman_climbing","short_names":["woman_climbing"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":433,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D7-1F3FB-200D-2640-FE0F","non_qualified":"1F9D7-1F3FB-200D-2640","image":"1f9d7-1f3fb-200d-2640-fe0f.png","sheet_x":51,"sheet_y":5,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9D7-1F3FB"},"1F3FC":{"unified":"1F9D7-1F3FC-200D-2640-FE0F","non_qualified":"1F9D7-1F3FC-200D-2640","image":"1f9d7-1f3fc-200d-2640-fe0f.png","sheet_x":51,"sheet_y":6,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9D7-1F3FC"},"1F3FD":{"unified":"1F9D7-1F3FD-200D-2640-FE0F","non_qualified":"1F9D7-1F3FD-200D-2640","image":"1f9d7-1f3fd-200d-2640-fe0f.png","sheet_x":51,"sheet_y":7,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9D7-1F3FD"},"1F3FE":{"unified":"1F9D7-1F3FE-200D-2640-FE0F","non_qualified":"1F9D7-1F3FE-200D-2640","image":"1f9d7-1f3fe-200d-2640-fe0f.png","sheet_x":51,"sheet_y":8,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9D7-1F3FE"},"1F3FF":{"unified":"1F9D7-1F3FF-200D-2640-FE0F","non_qualified":"1F9D7-1F3FF-200D-2640","image":"1f9d7-1f3ff-200d-2640-fe0f.png","sheet_x":51,"sheet_y":9,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9D7-1F3FF"}},"obsoletes":"1F9D7"},{"name":"MAN CLIMBING","unified":"1F9D7-200D-2642-FE0F","non_qualified":"1F9D7-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d7-200d-2642-fe0f.png","sheet_x":51,"sheet_y":10,"short_name":"man_climbing","short_names":["man_climbing"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":432,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D7-1F3FB-200D-2642-FE0F","non_qualified":"1F9D7-1F3FB-200D-2642","image":"1f9d7-1f3fb-200d-2642-fe0f.png","sheet_x":51,"sheet_y":11,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D7-1F3FC-200D-2642-FE0F","non_qualified":"1F9D7-1F3FC-200D-2642","image":"1f9d7-1f3fc-200d-2642-fe0f.png","sheet_x":51,"sheet_y":12,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D7-1F3FD-200D-2642-FE0F","non_qualified":"1F9D7-1F3FD-200D-2642","image":"1f9d7-1f3fd-200d-2642-fe0f.png","sheet_x":51,"sheet_y":13,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D7-1F3FE-200D-2642-FE0F","non_qualified":"1F9D7-1F3FE-200D-2642","image":"1f9d7-1f3fe-200d-2642-fe0f.png","sheet_x":51,"sheet_y":14,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D7-1F3FF-200D-2642-FE0F","non_qualified":"1F9D7-1F3FF-200D-2642","image":"1f9d7-1f3ff-200d-2642-fe0f.png","sheet_x":51,"sheet_y":15,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"PERSON CLIMBING","unified":"1F9D7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d7.png","sheet_x":51,"sheet_y":16,"short_name":"person_climbing","short_names":["person_climbing"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":431,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D7-1F3FB","non_qualified":null,"image":"1f9d7-1f3fb.png","sheet_x":51,"sheet_y":17,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9D7-1F3FB-200D-2640-FE0F"},"1F3FC":{"unified":"1F9D7-1F3FC","non_qualified":null,"image":"1f9d7-1f3fc.png","sheet_x":51,"sheet_y":18,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9D7-1F3FC-200D-2640-FE0F"},"1F3FD":{"unified":"1F9D7-1F3FD","non_qualified":null,"image":"1f9d7-1f3fd.png","sheet_x":51,"sheet_y":19,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9D7-1F3FD-200D-2640-FE0F"},"1F3FE":{"unified":"1F9D7-1F3FE","non_qualified":null,"image":"1f9d7-1f3fe.png","sheet_x":51,"sheet_y":20,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9D7-1F3FE-200D-2640-FE0F"},"1F3FF":{"unified":"1F9D7-1F3FF","non_qualified":null,"image":"1f9d7-1f3ff.png","sheet_x":51,"sheet_y":21,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9D7-1F3FF-200D-2640-FE0F"}},"obsoleted_by":"1F9D7-200D-2640-FE0F"},{"name":"WOMAN IN LOTUS POSITION","unified":"1F9D8-200D-2640-FE0F","non_qualified":"1F9D8-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d8-200d-2640-fe0f.png","sheet_x":51,"sheet_y":22,"short_name":"woman_in_lotus_position","short_names":["woman_in_lotus_position"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-resting","sort_order":479,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D8-1F3FB-200D-2640-FE0F","non_qualified":"1F9D8-1F3FB-200D-2640","image":"1f9d8-1f3fb-200d-2640-fe0f.png","sheet_x":51,"sheet_y":23,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9D8-1F3FB"},"1F3FC":{"unified":"1F9D8-1F3FC-200D-2640-FE0F","non_qualified":"1F9D8-1F3FC-200D-2640","image":"1f9d8-1f3fc-200d-2640-fe0f.png","sheet_x":51,"sheet_y":24,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9D8-1F3FC"},"1F3FD":{"unified":"1F9D8-1F3FD-200D-2640-FE0F","non_qualified":"1F9D8-1F3FD-200D-2640","image":"1f9d8-1f3fd-200d-2640-fe0f.png","sheet_x":51,"sheet_y":25,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9D8-1F3FD"},"1F3FE":{"unified":"1F9D8-1F3FE-200D-2640-FE0F","non_qualified":"1F9D8-1F3FE-200D-2640","image":"1f9d8-1f3fe-200d-2640-fe0f.png","sheet_x":51,"sheet_y":26,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9D8-1F3FE"},"1F3FF":{"unified":"1F9D8-1F3FF-200D-2640-FE0F","non_qualified":"1F9D8-1F3FF-200D-2640","image":"1f9d8-1f3ff-200d-2640-fe0f.png","sheet_x":51,"sheet_y":27,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9D8-1F3FF"}},"obsoletes":"1F9D8"},{"name":"MAN IN LOTUS POSITION","unified":"1F9D8-200D-2642-FE0F","non_qualified":"1F9D8-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d8-200d-2642-fe0f.png","sheet_x":51,"sheet_y":28,"short_name":"man_in_lotus_position","short_names":["man_in_lotus_position"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-resting","sort_order":478,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D8-1F3FB-200D-2642-FE0F","non_qualified":"1F9D8-1F3FB-200D-2642","image":"1f9d8-1f3fb-200d-2642-fe0f.png","sheet_x":51,"sheet_y":29,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D8-1F3FC-200D-2642-FE0F","non_qualified":"1F9D8-1F3FC-200D-2642","image":"1f9d8-1f3fc-200d-2642-fe0f.png","sheet_x":51,"sheet_y":30,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D8-1F3FD-200D-2642-FE0F","non_qualified":"1F9D8-1F3FD-200D-2642","image":"1f9d8-1f3fd-200d-2642-fe0f.png","sheet_x":51,"sheet_y":31,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D8-1F3FE-200D-2642-FE0F","non_qualified":"1F9D8-1F3FE-200D-2642","image":"1f9d8-1f3fe-200d-2642-fe0f.png","sheet_x":51,"sheet_y":32,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D8-1F3FF-200D-2642-FE0F","non_qualified":"1F9D8-1F3FF-200D-2642","image":"1f9d8-1f3ff-200d-2642-fe0f.png","sheet_x":51,"sheet_y":33,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"PERSON IN LOTUS POSITION","unified":"1F9D8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d8.png","sheet_x":51,"sheet_y":34,"short_name":"person_in_lotus_position","short_names":["person_in_lotus_position"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-resting","sort_order":477,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D8-1F3FB","non_qualified":null,"image":"1f9d8-1f3fb.png","sheet_x":51,"sheet_y":35,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9D8-1F3FB-200D-2640-FE0F"},"1F3FC":{"unified":"1F9D8-1F3FC","non_qualified":null,"image":"1f9d8-1f3fc.png","sheet_x":51,"sheet_y":36,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9D8-1F3FC-200D-2640-FE0F"},"1F3FD":{"unified":"1F9D8-1F3FD","non_qualified":null,"image":"1f9d8-1f3fd.png","sheet_x":51,"sheet_y":37,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9D8-1F3FD-200D-2640-FE0F"},"1F3FE":{"unified":"1F9D8-1F3FE","non_qualified":null,"image":"1f9d8-1f3fe.png","sheet_x":51,"sheet_y":38,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9D8-1F3FE-200D-2640-FE0F"},"1F3FF":{"unified":"1F9D8-1F3FF","non_qualified":null,"image":"1f9d8-1f3ff.png","sheet_x":51,"sheet_y":39,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9D8-1F3FF-200D-2640-FE0F"}},"obsoleted_by":"1F9D8-200D-2640-FE0F"},{"name":"WOMAN MAGE","unified":"1F9D9-200D-2640-FE0F","non_qualified":"1F9D9-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d9-200d-2640-fe0f.png","sheet_x":51,"sheet_y":40,"short_name":"female_mage","short_names":["female_mage"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":375,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D9-1F3FB-200D-2640-FE0F","non_qualified":"1F9D9-1F3FB-200D-2640","image":"1f9d9-1f3fb-200d-2640-fe0f.png","sheet_x":51,"sheet_y":41,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9D9-1F3FB"},"1F3FC":{"unified":"1F9D9-1F3FC-200D-2640-FE0F","non_qualified":"1F9D9-1F3FC-200D-2640","image":"1f9d9-1f3fc-200d-2640-fe0f.png","sheet_x":51,"sheet_y":42,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9D9-1F3FC"},"1F3FD":{"unified":"1F9D9-1F3FD-200D-2640-FE0F","non_qualified":"1F9D9-1F3FD-200D-2640","image":"1f9d9-1f3fd-200d-2640-fe0f.png","sheet_x":51,"sheet_y":43,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9D9-1F3FD"},"1F3FE":{"unified":"1F9D9-1F3FE-200D-2640-FE0F","non_qualified":"1F9D9-1F3FE-200D-2640","image":"1f9d9-1f3fe-200d-2640-fe0f.png","sheet_x":51,"sheet_y":44,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9D9-1F3FE"},"1F3FF":{"unified":"1F9D9-1F3FF-200D-2640-FE0F","non_qualified":"1F9D9-1F3FF-200D-2640","image":"1f9d9-1f3ff-200d-2640-fe0f.png","sheet_x":51,"sheet_y":45,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9D9-1F3FF"}},"obsoletes":"1F9D9"},{"name":"MAN MAGE","unified":"1F9D9-200D-2642-FE0F","non_qualified":"1F9D9-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d9-200d-2642-fe0f.png","sheet_x":51,"sheet_y":46,"short_name":"male_mage","short_names":["male_mage"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":374,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D9-1F3FB-200D-2642-FE0F","non_qualified":"1F9D9-1F3FB-200D-2642","image":"1f9d9-1f3fb-200d-2642-fe0f.png","sheet_x":51,"sheet_y":47,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D9-1F3FC-200D-2642-FE0F","non_qualified":"1F9D9-1F3FC-200D-2642","image":"1f9d9-1f3fc-200d-2642-fe0f.png","sheet_x":51,"sheet_y":48,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D9-1F3FD-200D-2642-FE0F","non_qualified":"1F9D9-1F3FD-200D-2642","image":"1f9d9-1f3fd-200d-2642-fe0f.png","sheet_x":51,"sheet_y":49,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D9-1F3FE-200D-2642-FE0F","non_qualified":"1F9D9-1F3FE-200D-2642","image":"1f9d9-1f3fe-200d-2642-fe0f.png","sheet_x":51,"sheet_y":50,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D9-1F3FF-200D-2642-FE0F","non_qualified":"1F9D9-1F3FF-200D-2642","image":"1f9d9-1f3ff-200d-2642-fe0f.png","sheet_x":51,"sheet_y":51,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAGE","unified":"1F9D9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d9.png","sheet_x":51,"sheet_y":52,"short_name":"mage","short_names":["mage"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":373,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D9-1F3FB","non_qualified":null,"image":"1f9d9-1f3fb.png","sheet_x":51,"sheet_y":53,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9D9-1F3FB-200D-2640-FE0F"},"1F3FC":{"unified":"1F9D9-1F3FC","non_qualified":null,"image":"1f9d9-1f3fc.png","sheet_x":51,"sheet_y":54,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9D9-1F3FC-200D-2640-FE0F"},"1F3FD":{"unified":"1F9D9-1F3FD","non_qualified":null,"image":"1f9d9-1f3fd.png","sheet_x":51,"sheet_y":55,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9D9-1F3FD-200D-2640-FE0F"},"1F3FE":{"unified":"1F9D9-1F3FE","non_qualified":null,"image":"1f9d9-1f3fe.png","sheet_x":51,"sheet_y":56,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9D9-1F3FE-200D-2640-FE0F"},"1F3FF":{"unified":"1F9D9-1F3FF","non_qualified":null,"image":"1f9d9-1f3ff.png","sheet_x":51,"sheet_y":57,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9D9-1F3FF-200D-2640-FE0F"}},"obsoleted_by":"1F9D9-200D-2640-FE0F"},{"name":"WOMAN FAIRY","unified":"1F9DA-200D-2640-FE0F","non_qualified":"1F9DA-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9da-200d-2640-fe0f.png","sheet_x":51,"sheet_y":58,"short_name":"female_fairy","short_names":["female_fairy"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":378,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9DA-1F3FB-200D-2640-FE0F","non_qualified":"1F9DA-1F3FB-200D-2640","image":"1f9da-1f3fb-200d-2640-fe0f.png","sheet_x":51,"sheet_y":59,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9DA-1F3FB"},"1F3FC":{"unified":"1F9DA-1F3FC-200D-2640-FE0F","non_qualified":"1F9DA-1F3FC-200D-2640","image":"1f9da-1f3fc-200d-2640-fe0f.png","sheet_x":51,"sheet_y":60,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9DA-1F3FC"},"1F3FD":{"unified":"1F9DA-1F3FD-200D-2640-FE0F","non_qualified":"1F9DA-1F3FD-200D-2640","image":"1f9da-1f3fd-200d-2640-fe0f.png","sheet_x":52,"sheet_y":0,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9DA-1F3FD"},"1F3FE":{"unified":"1F9DA-1F3FE-200D-2640-FE0F","non_qualified":"1F9DA-1F3FE-200D-2640","image":"1f9da-1f3fe-200d-2640-fe0f.png","sheet_x":52,"sheet_y":1,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9DA-1F3FE"},"1F3FF":{"unified":"1F9DA-1F3FF-200D-2640-FE0F","non_qualified":"1F9DA-1F3FF-200D-2640","image":"1f9da-1f3ff-200d-2640-fe0f.png","sheet_x":52,"sheet_y":2,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9DA-1F3FF"}},"obsoletes":"1F9DA"},{"name":"MAN FAIRY","unified":"1F9DA-200D-2642-FE0F","non_qualified":"1F9DA-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9da-200d-2642-fe0f.png","sheet_x":52,"sheet_y":3,"short_name":"male_fairy","short_names":["male_fairy"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":377,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9DA-1F3FB-200D-2642-FE0F","non_qualified":"1F9DA-1F3FB-200D-2642","image":"1f9da-1f3fb-200d-2642-fe0f.png","sheet_x":52,"sheet_y":4,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9DA-1F3FC-200D-2642-FE0F","non_qualified":"1F9DA-1F3FC-200D-2642","image":"1f9da-1f3fc-200d-2642-fe0f.png","sheet_x":52,"sheet_y":5,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9DA-1F3FD-200D-2642-FE0F","non_qualified":"1F9DA-1F3FD-200D-2642","image":"1f9da-1f3fd-200d-2642-fe0f.png","sheet_x":52,"sheet_y":6,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9DA-1F3FE-200D-2642-FE0F","non_qualified":"1F9DA-1F3FE-200D-2642","image":"1f9da-1f3fe-200d-2642-fe0f.png","sheet_x":52,"sheet_y":7,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9DA-1F3FF-200D-2642-FE0F","non_qualified":"1F9DA-1F3FF-200D-2642","image":"1f9da-1f3ff-200d-2642-fe0f.png","sheet_x":52,"sheet_y":8,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"FAIRY","unified":"1F9DA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9da.png","sheet_x":52,"sheet_y":9,"short_name":"fairy","short_names":["fairy"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":376,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9DA-1F3FB","non_qualified":null,"image":"1f9da-1f3fb.png","sheet_x":52,"sheet_y":10,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9DA-1F3FB-200D-2640-FE0F"},"1F3FC":{"unified":"1F9DA-1F3FC","non_qualified":null,"image":"1f9da-1f3fc.png","sheet_x":52,"sheet_y":11,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9DA-1F3FC-200D-2640-FE0F"},"1F3FD":{"unified":"1F9DA-1F3FD","non_qualified":null,"image":"1f9da-1f3fd.png","sheet_x":52,"sheet_y":12,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9DA-1F3FD-200D-2640-FE0F"},"1F3FE":{"unified":"1F9DA-1F3FE","non_qualified":null,"image":"1f9da-1f3fe.png","sheet_x":52,"sheet_y":13,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9DA-1F3FE-200D-2640-FE0F"},"1F3FF":{"unified":"1F9DA-1F3FF","non_qualified":null,"image":"1f9da-1f3ff.png","sheet_x":52,"sheet_y":14,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9DA-1F3FF-200D-2640-FE0F"}},"obsoleted_by":"1F9DA-200D-2640-FE0F"},{"name":"WOMAN VAMPIRE","unified":"1F9DB-200D-2640-FE0F","non_qualified":"1F9DB-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9db-200d-2640-fe0f.png","sheet_x":52,"sheet_y":15,"short_name":"female_vampire","short_names":["female_vampire"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":381,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9DB-1F3FB-200D-2640-FE0F","non_qualified":"1F9DB-1F3FB-200D-2640","image":"1f9db-1f3fb-200d-2640-fe0f.png","sheet_x":52,"sheet_y":16,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9DB-1F3FB"},"1F3FC":{"unified":"1F9DB-1F3FC-200D-2640-FE0F","non_qualified":"1F9DB-1F3FC-200D-2640","image":"1f9db-1f3fc-200d-2640-fe0f.png","sheet_x":52,"sheet_y":17,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9DB-1F3FC"},"1F3FD":{"unified":"1F9DB-1F3FD-200D-2640-FE0F","non_qualified":"1F9DB-1F3FD-200D-2640","image":"1f9db-1f3fd-200d-2640-fe0f.png","sheet_x":52,"sheet_y":18,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9DB-1F3FD"},"1F3FE":{"unified":"1F9DB-1F3FE-200D-2640-FE0F","non_qualified":"1F9DB-1F3FE-200D-2640","image":"1f9db-1f3fe-200d-2640-fe0f.png","sheet_x":52,"sheet_y":19,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9DB-1F3FE"},"1F3FF":{"unified":"1F9DB-1F3FF-200D-2640-FE0F","non_qualified":"1F9DB-1F3FF-200D-2640","image":"1f9db-1f3ff-200d-2640-fe0f.png","sheet_x":52,"sheet_y":20,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9DB-1F3FF"}},"obsoletes":"1F9DB"},{"name":"MAN VAMPIRE","unified":"1F9DB-200D-2642-FE0F","non_qualified":"1F9DB-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9db-200d-2642-fe0f.png","sheet_x":52,"sheet_y":21,"short_name":"male_vampire","short_names":["male_vampire"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":380,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9DB-1F3FB-200D-2642-FE0F","non_qualified":"1F9DB-1F3FB-200D-2642","image":"1f9db-1f3fb-200d-2642-fe0f.png","sheet_x":52,"sheet_y":22,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9DB-1F3FC-200D-2642-FE0F","non_qualified":"1F9DB-1F3FC-200D-2642","image":"1f9db-1f3fc-200d-2642-fe0f.png","sheet_x":52,"sheet_y":23,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9DB-1F3FD-200D-2642-FE0F","non_qualified":"1F9DB-1F3FD-200D-2642","image":"1f9db-1f3fd-200d-2642-fe0f.png","sheet_x":52,"sheet_y":24,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9DB-1F3FE-200D-2642-FE0F","non_qualified":"1F9DB-1F3FE-200D-2642","image":"1f9db-1f3fe-200d-2642-fe0f.png","sheet_x":52,"sheet_y":25,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9DB-1F3FF-200D-2642-FE0F","non_qualified":"1F9DB-1F3FF-200D-2642","image":"1f9db-1f3ff-200d-2642-fe0f.png","sheet_x":52,"sheet_y":26,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"VAMPIRE","unified":"1F9DB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9db.png","sheet_x":52,"sheet_y":27,"short_name":"vampire","short_names":["vampire"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":379,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9DB-1F3FB","non_qualified":null,"image":"1f9db-1f3fb.png","sheet_x":52,"sheet_y":28,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9DB-1F3FB-200D-2640-FE0F"},"1F3FC":{"unified":"1F9DB-1F3FC","non_qualified":null,"image":"1f9db-1f3fc.png","sheet_x":52,"sheet_y":29,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9DB-1F3FC-200D-2640-FE0F"},"1F3FD":{"unified":"1F9DB-1F3FD","non_qualified":null,"image":"1f9db-1f3fd.png","sheet_x":52,"sheet_y":30,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9DB-1F3FD-200D-2640-FE0F"},"1F3FE":{"unified":"1F9DB-1F3FE","non_qualified":null,"image":"1f9db-1f3fe.png","sheet_x":52,"sheet_y":31,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9DB-1F3FE-200D-2640-FE0F"},"1F3FF":{"unified":"1F9DB-1F3FF","non_qualified":null,"image":"1f9db-1f3ff.png","sheet_x":52,"sheet_y":32,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9DB-1F3FF-200D-2640-FE0F"}},"obsoleted_by":"1F9DB-200D-2640-FE0F"},{"name":"MERMAID","unified":"1F9DC-200D-2640-FE0F","non_qualified":"1F9DC-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9dc-200d-2640-fe0f.png","sheet_x":52,"sheet_y":33,"short_name":"mermaid","short_names":["mermaid"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":384,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9DC-1F3FB-200D-2640-FE0F","non_qualified":"1F9DC-1F3FB-200D-2640","image":"1f9dc-1f3fb-200d-2640-fe0f.png","sheet_x":52,"sheet_y":34,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9DC-1F3FC-200D-2640-FE0F","non_qualified":"1F9DC-1F3FC-200D-2640","image":"1f9dc-1f3fc-200d-2640-fe0f.png","sheet_x":52,"sheet_y":35,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9DC-1F3FD-200D-2640-FE0F","non_qualified":"1F9DC-1F3FD-200D-2640","image":"1f9dc-1f3fd-200d-2640-fe0f.png","sheet_x":52,"sheet_y":36,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9DC-1F3FE-200D-2640-FE0F","non_qualified":"1F9DC-1F3FE-200D-2640","image":"1f9dc-1f3fe-200d-2640-fe0f.png","sheet_x":52,"sheet_y":37,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9DC-1F3FF-200D-2640-FE0F","non_qualified":"1F9DC-1F3FF-200D-2640","image":"1f9dc-1f3ff-200d-2640-fe0f.png","sheet_x":52,"sheet_y":38,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MERMAN","unified":"1F9DC-200D-2642-FE0F","non_qualified":"1F9DC-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9dc-200d-2642-fe0f.png","sheet_x":52,"sheet_y":39,"short_name":"merman","short_names":["merman"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":383,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9DC-1F3FB-200D-2642-FE0F","non_qualified":"1F9DC-1F3FB-200D-2642","image":"1f9dc-1f3fb-200d-2642-fe0f.png","sheet_x":52,"sheet_y":40,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9DC-1F3FB"},"1F3FC":{"unified":"1F9DC-1F3FC-200D-2642-FE0F","non_qualified":"1F9DC-1F3FC-200D-2642","image":"1f9dc-1f3fc-200d-2642-fe0f.png","sheet_x":52,"sheet_y":41,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9DC-1F3FC"},"1F3FD":{"unified":"1F9DC-1F3FD-200D-2642-FE0F","non_qualified":"1F9DC-1F3FD-200D-2642","image":"1f9dc-1f3fd-200d-2642-fe0f.png","sheet_x":52,"sheet_y":42,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9DC-1F3FD"},"1F3FE":{"unified":"1F9DC-1F3FE-200D-2642-FE0F","non_qualified":"1F9DC-1F3FE-200D-2642","image":"1f9dc-1f3fe-200d-2642-fe0f.png","sheet_x":52,"sheet_y":43,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9DC-1F3FE"},"1F3FF":{"unified":"1F9DC-1F3FF-200D-2642-FE0F","non_qualified":"1F9DC-1F3FF-200D-2642","image":"1f9dc-1f3ff-200d-2642-fe0f.png","sheet_x":52,"sheet_y":44,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9DC-1F3FF"}},"obsoletes":"1F9DC"},{"name":"MERPERSON","unified":"1F9DC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9dc.png","sheet_x":52,"sheet_y":45,"short_name":"merperson","short_names":["merperson"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":382,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9DC-1F3FB","non_qualified":null,"image":"1f9dc-1f3fb.png","sheet_x":52,"sheet_y":46,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9DC-1F3FB-200D-2642-FE0F"},"1F3FC":{"unified":"1F9DC-1F3FC","non_qualified":null,"image":"1f9dc-1f3fc.png","sheet_x":52,"sheet_y":47,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9DC-1F3FC-200D-2642-FE0F"},"1F3FD":{"unified":"1F9DC-1F3FD","non_qualified":null,"image":"1f9dc-1f3fd.png","sheet_x":52,"sheet_y":48,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9DC-1F3FD-200D-2642-FE0F"},"1F3FE":{"unified":"1F9DC-1F3FE","non_qualified":null,"image":"1f9dc-1f3fe.png","sheet_x":52,"sheet_y":49,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9DC-1F3FE-200D-2642-FE0F"},"1F3FF":{"unified":"1F9DC-1F3FF","non_qualified":null,"image":"1f9dc-1f3ff.png","sheet_x":52,"sheet_y":50,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9DC-1F3FF-200D-2642-FE0F"}},"obsoleted_by":"1F9DC-200D-2642-FE0F"},{"name":"WOMAN ELF","unified":"1F9DD-200D-2640-FE0F","non_qualified":"1F9DD-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9dd-200d-2640-fe0f.png","sheet_x":52,"sheet_y":51,"short_name":"female_elf","short_names":["female_elf"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":387,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9DD-1F3FB-200D-2640-FE0F","non_qualified":"1F9DD-1F3FB-200D-2640","image":"1f9dd-1f3fb-200d-2640-fe0f.png","sheet_x":52,"sheet_y":52,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9DD-1F3FC-200D-2640-FE0F","non_qualified":"1F9DD-1F3FC-200D-2640","image":"1f9dd-1f3fc-200d-2640-fe0f.png","sheet_x":52,"sheet_y":53,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9DD-1F3FD-200D-2640-FE0F","non_qualified":"1F9DD-1F3FD-200D-2640","image":"1f9dd-1f3fd-200d-2640-fe0f.png","sheet_x":52,"sheet_y":54,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9DD-1F3FE-200D-2640-FE0F","non_qualified":"1F9DD-1F3FE-200D-2640","image":"1f9dd-1f3fe-200d-2640-fe0f.png","sheet_x":52,"sheet_y":55,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9DD-1F3FF-200D-2640-FE0F","non_qualified":"1F9DD-1F3FF-200D-2640","image":"1f9dd-1f3ff-200d-2640-fe0f.png","sheet_x":52,"sheet_y":56,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN ELF","unified":"1F9DD-200D-2642-FE0F","non_qualified":"1F9DD-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9dd-200d-2642-fe0f.png","sheet_x":52,"sheet_y":57,"short_name":"male_elf","short_names":["male_elf"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":386,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9DD-1F3FB-200D-2642-FE0F","non_qualified":"1F9DD-1F3FB-200D-2642","image":"1f9dd-1f3fb-200d-2642-fe0f.png","sheet_x":52,"sheet_y":58,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9DD-1F3FB"},"1F3FC":{"unified":"1F9DD-1F3FC-200D-2642-FE0F","non_qualified":"1F9DD-1F3FC-200D-2642","image":"1f9dd-1f3fc-200d-2642-fe0f.png","sheet_x":52,"sheet_y":59,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9DD-1F3FC"},"1F3FD":{"unified":"1F9DD-1F3FD-200D-2642-FE0F","non_qualified":"1F9DD-1F3FD-200D-2642","image":"1f9dd-1f3fd-200d-2642-fe0f.png","sheet_x":52,"sheet_y":60,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9DD-1F3FD"},"1F3FE":{"unified":"1F9DD-1F3FE-200D-2642-FE0F","non_qualified":"1F9DD-1F3FE-200D-2642","image":"1f9dd-1f3fe-200d-2642-fe0f.png","sheet_x":53,"sheet_y":0,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9DD-1F3FE"},"1F3FF":{"unified":"1F9DD-1F3FF-200D-2642-FE0F","non_qualified":"1F9DD-1F3FF-200D-2642","image":"1f9dd-1f3ff-200d-2642-fe0f.png","sheet_x":53,"sheet_y":1,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9DD-1F3FF"}},"obsoletes":"1F9DD"},{"name":"ELF","unified":"1F9DD","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9dd.png","sheet_x":53,"sheet_y":2,"short_name":"elf","short_names":["elf"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":385,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9DD-1F3FB","non_qualified":null,"image":"1f9dd-1f3fb.png","sheet_x":53,"sheet_y":3,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9DD-1F3FB-200D-2642-FE0F"},"1F3FC":{"unified":"1F9DD-1F3FC","non_qualified":null,"image":"1f9dd-1f3fc.png","sheet_x":53,"sheet_y":4,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9DD-1F3FC-200D-2642-FE0F"},"1F3FD":{"unified":"1F9DD-1F3FD","non_qualified":null,"image":"1f9dd-1f3fd.png","sheet_x":53,"sheet_y":5,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9DD-1F3FD-200D-2642-FE0F"},"1F3FE":{"unified":"1F9DD-1F3FE","non_qualified":null,"image":"1f9dd-1f3fe.png","sheet_x":53,"sheet_y":6,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9DD-1F3FE-200D-2642-FE0F"},"1F3FF":{"unified":"1F9DD-1F3FF","non_qualified":null,"image":"1f9dd-1f3ff.png","sheet_x":53,"sheet_y":7,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9DD-1F3FF-200D-2642-FE0F"}},"obsoleted_by":"1F9DD-200D-2642-FE0F"},{"name":"WOMAN GENIE","unified":"1F9DE-200D-2640-FE0F","non_qualified":"1F9DE-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9de-200d-2640-fe0f.png","sheet_x":53,"sheet_y":8,"short_name":"female_genie","short_names":["female_genie"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":390,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MAN GENIE","unified":"1F9DE-200D-2642-FE0F","non_qualified":"1F9DE-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9de-200d-2642-fe0f.png","sheet_x":53,"sheet_y":9,"short_name":"male_genie","short_names":["male_genie"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":389,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9DE"},{"name":"GENIE","unified":"1F9DE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9de.png","sheet_x":53,"sheet_y":10,"short_name":"genie","short_names":["genie"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":388,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9DE-200D-2642-FE0F"},{"name":"WOMAN ZOMBIE","unified":"1F9DF-200D-2640-FE0F","non_qualified":"1F9DF-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9df-200d-2640-fe0f.png","sheet_x":53,"sheet_y":11,"short_name":"female_zombie","short_names":["female_zombie"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":393,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MAN ZOMBIE","unified":"1F9DF-200D-2642-FE0F","non_qualified":"1F9DF-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9df-200d-2642-fe0f.png","sheet_x":53,"sheet_y":12,"short_name":"male_zombie","short_names":["male_zombie"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":392,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9DF"},{"name":"ZOMBIE","unified":"1F9DF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9df.png","sheet_x":53,"sheet_y":13,"short_name":"zombie","short_names":["zombie"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":391,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9DF-200D-2642-FE0F"},{"name":"BRAIN","unified":"1F9E0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9e0.png","sheet_x":53,"sheet_y":14,"short_name":"brain","short_names":["brain"],"text":null,"texts":null,"category":"People & Body","subcategory":"body-parts","sort_order":213,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ORANGE HEART","unified":"1F9E1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9e1.png","sheet_x":53,"sheet_y":15,"short_name":"orange_heart","short_names":["orange_heart"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":142,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BILLED CAP","unified":"1F9E2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9e2.png","sheet_x":53,"sheet_y":16,"short_name":"billed_cap","short_names":["billed_cap"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1148,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SCARF","unified":"1F9E3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9e3.png","sheet_x":53,"sheet_y":17,"short_name":"scarf","short_names":["scarf"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1118,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GLOVES","unified":"1F9E4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9e4.png","sheet_x":53,"sheet_y":18,"short_name":"gloves","short_names":["gloves"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1119,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"COAT","unified":"1F9E5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9e5.png","sheet_x":53,"sheet_y":19,"short_name":"coat","short_names":["coat"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1120,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SOCKS","unified":"1F9E6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9e6.png","sheet_x":53,"sheet_y":20,"short_name":"socks","short_names":["socks"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1121,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RED GIFT ENVELOPE","unified":"1F9E7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9e7.png","sheet_x":53,"sheet_y":21,"short_name":"red_envelope","short_names":["red_envelope"],"text":null,"texts":null,"category":"Activities","subcategory":"event","sort_order":1039,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FIRECRACKER","unified":"1F9E8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9e8.png","sheet_x":53,"sheet_y":22,"short_name":"firecracker","short_names":["firecracker"],"text":null,"texts":null,"category":"Activities","subcategory":"event","sort_order":1028,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"JIGSAW PUZZLE PIECE","unified":"1F9E9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9e9.png","sheet_x":53,"sheet_y":23,"short_name":"jigsaw","short_names":["jigsaw"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1090,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TEST TUBE","unified":"1F9EA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9ea.png","sheet_x":53,"sheet_y":24,"short_name":"test_tube","short_names":["test_tube"],"text":null,"texts":null,"category":"Objects","subcategory":"science","sort_order":1320,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PETRI DISH","unified":"1F9EB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9eb.png","sheet_x":53,"sheet_y":25,"short_name":"petri_dish","short_names":["petri_dish"],"text":null,"texts":null,"category":"Objects","subcategory":"science","sort_order":1321,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DNA DOUBLE HELIX","unified":"1F9EC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9ec.png","sheet_x":53,"sheet_y":26,"short_name":"dna","short_names":["dna"],"text":null,"texts":null,"category":"Objects","subcategory":"science","sort_order":1322,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"COMPASS","unified":"1F9ED","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9ed.png","sheet_x":53,"sheet_y":27,"short_name":"compass","short_names":["compass"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-map","sort_order":812,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ABACUS","unified":"1F9EE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9ee.png","sheet_x":53,"sheet_y":28,"short_name":"abacus","short_names":["abacus"],"text":null,"texts":null,"category":"Objects","subcategory":"computer","sort_order":1201,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FIRE EXTINGUISHER","unified":"1F9EF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9ef.png","sheet_x":53,"sheet_y":29,"short_name":"fire_extinguisher","short_names":["fire_extinguisher"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1356,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TOOLBOX","unified":"1F9F0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9f0.png","sheet_x":53,"sheet_y":30,"short_name":"toolbox","short_names":["toolbox"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1316,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BRICK","unified":"1F9F1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9f1.png","sheet_x":53,"sheet_y":31,"short_name":"bricks","short_names":["bricks"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":825,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MAGNET","unified":"1F9F2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9f2.png","sheet_x":53,"sheet_y":32,"short_name":"magnet","short_names":["magnet"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1317,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LUGGAGE","unified":"1F9F3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9f3.png","sheet_x":53,"sheet_y":33,"short_name":"luggage","short_names":["luggage"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"hotel","sort_order":945,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LOTION BOTTLE","unified":"1F9F4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9f4.png","sheet_x":53,"sheet_y":34,"short_name":"lotion_bottle","short_names":["lotion_bottle"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1346,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SPOOL OF THREAD","unified":"1F9F5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9f5.png","sheet_x":53,"sheet_y":35,"short_name":"thread","short_names":["thread"],"text":null,"texts":null,"category":"Activities","subcategory":"arts & crafts","sort_order":1106,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BALL OF YARN","unified":"1F9F6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9f6.png","sheet_x":53,"sheet_y":36,"short_name":"yarn","short_names":["yarn"],"text":null,"texts":null,"category":"Activities","subcategory":"arts & crafts","sort_order":1108,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SAFETY PIN","unified":"1F9F7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9f7.png","sheet_x":53,"sheet_y":37,"short_name":"safety_pin","short_names":["safety_pin"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1347,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TEDDY BEAR","unified":"1F9F8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9f8.png","sheet_x":53,"sheet_y":38,"short_name":"teddy_bear","short_names":["teddy_bear"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1091,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BROOM","unified":"1F9F9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9f9.png","sheet_x":53,"sheet_y":39,"short_name":"broom","short_names":["broom"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1348,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BASKET","unified":"1F9FA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9fa.png","sheet_x":53,"sheet_y":40,"short_name":"basket","short_names":["basket"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1349,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ROLL OF PAPER","unified":"1F9FB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9fb.png","sheet_x":53,"sheet_y":41,"short_name":"roll_of_paper","short_names":["roll_of_paper"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1350,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BAR OF SOAP","unified":"1F9FC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9fc.png","sheet_x":53,"sheet_y":42,"short_name":"soap","short_names":["soap"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1352,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SPONGE","unified":"1F9FD","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9fd.png","sheet_x":53,"sheet_y":43,"short_name":"sponge","short_names":["sponge"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1355,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RECEIPT","unified":"1F9FE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9fe.png","sheet_x":53,"sheet_y":44,"short_name":"receipt","short_names":["receipt"],"text":null,"texts":null,"category":"Objects","subcategory":"money","sort_order":1243,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NAZAR AMULET","unified":"1F9FF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9ff.png","sheet_x":53,"sheet_y":45,"short_name":"nazar_amulet","short_names":["nazar_amulet"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1084,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BALLET SHOES","unified":"1FA70","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa70.png","sheet_x":53,"sheet_y":46,"short_name":"ballet_shoes","short_names":["ballet_shoes"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1142,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ONE-PIECE SWIMSUIT","unified":"1FA71","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa71.png","sheet_x":53,"sheet_y":47,"short_name":"one-piece_swimsuit","short_names":["one-piece_swimsuit"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1125,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BRIEFS","unified":"1FA72","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa72.png","sheet_x":53,"sheet_y":48,"short_name":"briefs","short_names":["briefs"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1126,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SHORTS","unified":"1FA73","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa73.png","sheet_x":53,"sheet_y":49,"short_name":"shorts","short_names":["shorts"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1127,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"THONG SANDAL","unified":"1FA74","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa74.png","sheet_x":53,"sheet_y":50,"short_name":"thong_sandal","short_names":["thong_sandal"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1135,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DROP OF BLOOD","unified":"1FA78","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa78.png","sheet_x":53,"sheet_y":51,"short_name":"drop_of_blood","short_names":["drop_of_blood"],"text":null,"texts":null,"category":"Objects","subcategory":"medical","sort_order":1327,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ADHESIVE BANDAGE","unified":"1FA79","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa79.png","sheet_x":53,"sheet_y":52,"short_name":"adhesive_bandage","short_names":["adhesive_bandage"],"text":null,"texts":null,"category":"Objects","subcategory":"medical","sort_order":1329,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"STETHOSCOPE","unified":"1FA7A","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa7a.png","sheet_x":53,"sheet_y":53,"short_name":"stethoscope","short_names":["stethoscope"],"text":null,"texts":null,"category":"Objects","subcategory":"medical","sort_order":1331,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"X-RAY","unified":"1FA7B","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa7b.png","sheet_x":53,"sheet_y":54,"short_name":"x-ray","short_names":["x-ray"],"text":null,"texts":null,"category":"Objects","subcategory":"medical","sort_order":1332,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"CRUTCH","unified":"1FA7C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa7c.png","sheet_x":53,"sheet_y":55,"short_name":"crutch","short_names":["crutch"],"text":null,"texts":null,"category":"Objects","subcategory":"medical","sort_order":1330,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"YO-YO","unified":"1FA80","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa80.png","sheet_x":53,"sheet_y":56,"short_name":"yo-yo","short_names":["yo-yo"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1079,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"KITE","unified":"1FA81","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa81.png","sheet_x":53,"sheet_y":57,"short_name":"kite","short_names":["kite"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1080,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PARACHUTE","unified":"1FA82","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa82.png","sheet_x":53,"sheet_y":58,"short_name":"parachute","short_names":["parachute"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-air","sort_order":935,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BOOMERANG","unified":"1FA83","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa83.png","sheet_x":53,"sheet_y":59,"short_name":"boomerang","short_names":["boomerang"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1302,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MAGIC WAND","unified":"1FA84","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa84.png","sheet_x":53,"sheet_y":60,"short_name":"magic_wand","short_names":["magic_wand"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1083,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PINATA","unified":"1FA85","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa85.png","sheet_x":54,"sheet_y":0,"short_name":"pinata","short_names":["pinata"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1092,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NESTING DOLLS","unified":"1FA86","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa86.png","sheet_x":54,"sheet_y":1,"short_name":"nesting_dolls","short_names":["nesting_dolls"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1094,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RINGED PLANET","unified":"1FA90","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa90.png","sheet_x":54,"sheet_y":2,"short_name":"ringed_planet","short_names":["ringed_planet"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":993,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CHAIR","unified":"1FA91","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa91.png","sheet_x":54,"sheet_y":3,"short_name":"chair","short_names":["chair"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1339,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RAZOR","unified":"1FA92","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa92.png","sheet_x":54,"sheet_y":4,"short_name":"razor","short_names":["razor"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1345,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"AXE","unified":"1FA93","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa93.png","sheet_x":54,"sheet_y":5,"short_name":"axe","short_names":["axe"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1295,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DIYA LAMP","unified":"1FA94","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa94.png","sheet_x":54,"sheet_y":6,"short_name":"diya_lamp","short_names":["diya_lamp"],"text":null,"texts":null,"category":"Objects","subcategory":"light & video","sort_order":1217,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BANJO","unified":"1FA95","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa95.png","sheet_x":54,"sheet_y":7,"short_name":"banjo","short_names":["banjo"],"text":null,"texts":null,"category":"Objects","subcategory":"musical-instrument","sort_order":1179,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MILITARY HELMET","unified":"1FA96","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa96.png","sheet_x":54,"sheet_y":8,"short_name":"military_helmet","short_names":["military_helmet"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1149,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ACCORDION","unified":"1FA97","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa97.png","sheet_x":54,"sheet_y":9,"short_name":"accordion","short_names":["accordion"],"text":null,"texts":null,"category":"Objects","subcategory":"musical-instrument","sort_order":1174,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LONG DRUM","unified":"1FA98","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa98.png","sheet_x":54,"sheet_y":10,"short_name":"long_drum","short_names":["long_drum"],"text":null,"texts":null,"category":"Objects","subcategory":"musical-instrument","sort_order":1181,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"COIN","unified":"1FA99","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa99.png","sheet_x":54,"sheet_y":11,"short_name":"coin","short_names":["coin"],"text":null,"texts":null,"category":"Objects","subcategory":"money","sort_order":1236,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CARPENTRY SAW","unified":"1FA9A","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa9a.png","sheet_x":54,"sheet_y":12,"short_name":"carpentry_saw","short_names":["carpentry_saw"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1305,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SCREWDRIVER","unified":"1FA9B","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa9b.png","sheet_x":54,"sheet_y":13,"short_name":"screwdriver","short_names":["screwdriver"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1307,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LADDER","unified":"1FA9C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa9c.png","sheet_x":54,"sheet_y":14,"short_name":"ladder","short_names":["ladder"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1318,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HOOK","unified":"1FA9D","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa9d.png","sheet_x":54,"sheet_y":15,"short_name":"hook","short_names":["hook"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1315,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MIRROR","unified":"1FA9E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa9e.png","sheet_x":54,"sheet_y":16,"short_name":"mirror","short_names":["mirror"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1335,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WINDOW","unified":"1FA9F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa9f.png","sheet_x":54,"sheet_y":17,"short_name":"window","short_names":["window"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1336,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PLUNGER","unified":"1FAA0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faa0.png","sheet_x":54,"sheet_y":18,"short_name":"plunger","short_names":["plunger"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1341,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SEWING NEEDLE","unified":"1FAA1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faa1.png","sheet_x":54,"sheet_y":19,"short_name":"sewing_needle","short_names":["sewing_needle"],"text":null,"texts":null,"category":"Activities","subcategory":"arts & crafts","sort_order":1107,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"KNOT","unified":"1FAA2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faa2.png","sheet_x":54,"sheet_y":20,"short_name":"knot","short_names":["knot"],"text":null,"texts":null,"category":"Activities","subcategory":"arts & crafts","sort_order":1109,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BUCKET","unified":"1FAA3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faa3.png","sheet_x":54,"sheet_y":21,"short_name":"bucket","short_names":["bucket"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1351,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MOUSE TRAP","unified":"1FAA4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faa4.png","sheet_x":54,"sheet_y":22,"short_name":"mouse_trap","short_names":["mouse_trap"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1344,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TOOTHBRUSH","unified":"1FAA5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faa5.png","sheet_x":54,"sheet_y":23,"short_name":"toothbrush","short_names":["toothbrush"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1354,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HEADSTONE","unified":"1FAA6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faa6.png","sheet_x":54,"sheet_y":24,"short_name":"headstone","short_names":["headstone"],"text":null,"texts":null,"category":"Objects","subcategory":"other-object","sort_order":1360,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PLACARD","unified":"1FAA7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faa7.png","sheet_x":54,"sheet_y":25,"short_name":"placard","short_names":["placard"],"text":null,"texts":null,"category":"Objects","subcategory":"other-object","sort_order":1363,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ROCK","unified":"1FAA8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faa8.png","sheet_x":54,"sheet_y":26,"short_name":"rock","short_names":["rock"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":826,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MIRROR BALL","unified":"1FAA9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faa9.png","sheet_x":54,"sheet_y":27,"short_name":"mirror_ball","short_names":["mirror_ball"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1093,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"IDENTIFICATION CARD","unified":"1FAAA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faaa.png","sheet_x":54,"sheet_y":28,"short_name":"identification_card","short_names":["identification_card"],"text":null,"texts":null,"category":"Objects","subcategory":"other-object","sort_order":1364,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"LOW BATTERY","unified":"1FAAB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faab.png","sheet_x":54,"sheet_y":29,"short_name":"low_battery","short_names":["low_battery"],"text":null,"texts":null,"category":"Objects","subcategory":"computer","sort_order":1189,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"HAMSA","unified":"1FAAC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faac.png","sheet_x":54,"sheet_y":30,"short_name":"hamsa","short_names":["hamsa"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1085,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"FLY","unified":"1FAB0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fab0.png","sheet_x":54,"sheet_y":31,"short_name":"fly","short_names":["fly"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bug","sort_order":645,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WORM","unified":"1FAB1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fab1.png","sheet_x":54,"sheet_y":32,"short_name":"worm","short_names":["worm"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bug","sort_order":646,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BEETLE","unified":"1FAB2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fab2.png","sheet_x":54,"sheet_y":33,"short_name":"beetle","short_names":["beetle"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bug","sort_order":637,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"COCKROACH","unified":"1FAB3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fab3.png","sheet_x":54,"sheet_y":34,"short_name":"cockroach","short_names":["cockroach"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bug","sort_order":640,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"POTTED PLANT","unified":"1FAB4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fab4.png","sheet_x":54,"sheet_y":35,"short_name":"potted_plant","short_names":["potted_plant"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-other","sort_order":660,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WOOD","unified":"1FAB5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fab5.png","sheet_x":54,"sheet_y":36,"short_name":"wood","short_names":["wood"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":827,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FEATHER","unified":"1FAB6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fab6.png","sheet_x":54,"sheet_y":37,"short_name":"feather","short_names":["feather"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bird","sort_order":608,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LOTUS","unified":"1FAB7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fab7.png","sheet_x":54,"sheet_y":38,"short_name":"lotus","short_names":["lotus"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-flower","sort_order":651,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"CORAL","unified":"1FAB8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fab8.png","sheet_x":54,"sheet_y":39,"short_name":"coral","short_names":["coral"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-marine","sort_order":631,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"EMPTY NEST","unified":"1FAB9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fab9.png","sheet_x":54,"sheet_y":40,"short_name":"empty_nest","short_names":["empty_nest"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-other","sort_order":672,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"NEST WITH EGGS","unified":"1FABA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faba.png","sheet_x":54,"sheet_y":41,"short_name":"nest_with_eggs","short_names":["nest_with_eggs"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-other","sort_order":673,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"ANATOMICAL HEART","unified":"1FAC0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fac0.png","sheet_x":54,"sheet_y":42,"short_name":"anatomical_heart","short_names":["anatomical_heart"],"text":null,"texts":null,"category":"People & Body","subcategory":"body-parts","sort_order":214,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LUNGS","unified":"1FAC1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fac1.png","sheet_x":54,"sheet_y":43,"short_name":"lungs","short_names":["lungs"],"text":null,"texts":null,"category":"People & Body","subcategory":"body-parts","sort_order":215,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PEOPLE HUGGING","unified":"1FAC2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fac2.png","sheet_x":54,"sheet_y":44,"short_name":"people_hugging","short_names":["people_hugging"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-symbol","sort_order":523,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PREGNANT MAN","unified":"1FAC3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fac3.png","sheet_x":54,"sheet_y":45,"short_name":"pregnant_man","short_names":["pregnant_man"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":357,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"1FAC3-1F3FB","non_qualified":null,"image":"1fac3-1f3fb.png","sheet_x":54,"sheet_y":46,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC":{"unified":"1FAC3-1F3FC","non_qualified":null,"image":"1fac3-1f3fc.png","sheet_x":54,"sheet_y":47,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD":{"unified":"1FAC3-1F3FD","non_qualified":null,"image":"1fac3-1f3fd.png","sheet_x":54,"sheet_y":48,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE":{"unified":"1FAC3-1F3FE","non_qualified":null,"image":"1fac3-1f3fe.png","sheet_x":54,"sheet_y":49,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF":{"unified":"1FAC3-1F3FF","non_qualified":null,"image":"1fac3-1f3ff.png","sheet_x":54,"sheet_y":50,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false}}},{"name":"PREGNANT PERSON","unified":"1FAC4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fac4.png","sheet_x":54,"sheet_y":51,"short_name":"pregnant_person","short_names":["pregnant_person"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":358,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"1FAC4-1F3FB","non_qualified":null,"image":"1fac4-1f3fb.png","sheet_x":54,"sheet_y":52,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC":{"unified":"1FAC4-1F3FC","non_qualified":null,"image":"1fac4-1f3fc.png","sheet_x":54,"sheet_y":53,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD":{"unified":"1FAC4-1F3FD","non_qualified":null,"image":"1fac4-1f3fd.png","sheet_x":54,"sheet_y":54,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE":{"unified":"1FAC4-1F3FE","non_qualified":null,"image":"1fac4-1f3fe.png","sheet_x":54,"sheet_y":55,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF":{"unified":"1FAC4-1F3FF","non_qualified":null,"image":"1fac4-1f3ff.png","sheet_x":54,"sheet_y":56,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false}}},{"name":"PERSON WITH CROWN","unified":"1FAC5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fac5.png","sheet_x":54,"sheet_y":57,"short_name":"person_with_crown","short_names":["person_with_crown"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":342,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"1FAC5-1F3FB","non_qualified":null,"image":"1fac5-1f3fb.png","sheet_x":54,"sheet_y":58,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC":{"unified":"1FAC5-1F3FC","non_qualified":null,"image":"1fac5-1f3fc.png","sheet_x":54,"sheet_y":59,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD":{"unified":"1FAC5-1F3FD","non_qualified":null,"image":"1fac5-1f3fd.png","sheet_x":54,"sheet_y":60,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE":{"unified":"1FAC5-1F3FE","non_qualified":null,"image":"1fac5-1f3fe.png","sheet_x":55,"sheet_y":0,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF":{"unified":"1FAC5-1F3FF","non_qualified":null,"image":"1fac5-1f3ff.png","sheet_x":55,"sheet_y":1,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false}}},{"name":"BLUEBERRIES","unified":"1FAD0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fad0.png","sheet_x":55,"sheet_y":2,"short_name":"blueberries","short_names":["blueberries"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-fruit","sort_order":688,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BELL PEPPER","unified":"1FAD1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fad1.png","sheet_x":55,"sheet_y":3,"short_name":"bell_pepper","short_names":["bell_pepper"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-vegetable","sort_order":699,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"OLIVE","unified":"1FAD2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fad2.png","sheet_x":55,"sheet_y":4,"short_name":"olive","short_names":["olive"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-fruit","sort_order":691,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FLATBREAD","unified":"1FAD3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fad3.png","sheet_x":55,"sheet_y":5,"short_name":"flatbread","short_names":["flatbread"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":712,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TAMALE","unified":"1FAD4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fad4.png","sheet_x":55,"sheet_y":6,"short_name":"tamale","short_names":["tamale"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":729,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FONDUE","unified":"1FAD5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fad5.png","sheet_x":55,"sheet_y":7,"short_name":"fondue","short_names":["fondue"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":736,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TEAPOT","unified":"1FAD6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fad6.png","sheet_x":55,"sheet_y":8,"short_name":"teapot","short_names":["teapot"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"drink","sort_order":782,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"POURING LIQUID","unified":"1FAD7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fad7.png","sheet_x":55,"sheet_y":9,"short_name":"pouring_liquid","short_names":["pouring_liquid"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"drink","sort_order":793,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"BEANS","unified":"1FAD8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fad8.png","sheet_x":55,"sheet_y":10,"short_name":"beans","short_names":["beans"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-vegetable","sort_order":707,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"JAR","unified":"1FAD9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fad9.png","sheet_x":55,"sheet_y":11,"short_name":"jar","short_names":["jar"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"dishware","sort_order":804,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"MELTING FACE","unified":"1FAE0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fae0.png","sheet_x":55,"sheet_y":12,"short_name":"melting_face","short_names":["melting_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-smiling","sort_order":11,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"SALUTING FACE","unified":"1FAE1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fae1.png","sheet_x":55,"sheet_y":13,"short_name":"saluting_face","short_names":["saluting_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-hand","sort_order":36,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"FACE WITH OPEN EYES AND HAND OVER MOUTH","unified":"1FAE2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fae2.png","sheet_x":55,"sheet_y":14,"short_name":"face_with_open_eyes_and_hand_over_mouth","short_names":["face_with_open_eyes_and_hand_over_mouth"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-hand","sort_order":32,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"FACE WITH PEEKING EYE","unified":"1FAE3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fae3.png","sheet_x":55,"sheet_y":15,"short_name":"face_with_peeking_eye","short_names":["face_with_peeking_eye"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-hand","sort_order":33,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"FACE WITH DIAGONAL MOUTH","unified":"1FAE4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fae4.png","sheet_x":55,"sheet_y":16,"short_name":"face_with_diagonal_mouth","short_names":["face_with_diagonal_mouth"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":74,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"DOTTED LINE FACE","unified":"1FAE5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fae5.png","sheet_x":55,"sheet_y":17,"short_name":"dotted_line_face","short_names":["dotted_line_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-neutral-skeptical","sort_order":42,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"BITING LIP","unified":"1FAE6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fae6.png","sheet_x":55,"sheet_y":18,"short_name":"biting_lip","short_names":["biting_lip"],"text":null,"texts":null,"category":"People & Body","subcategory":"body-parts","sort_order":222,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"BUBBLES","unified":"1FAE7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fae7.png","sheet_x":55,"sheet_y":19,"short_name":"bubbles","short_names":["bubbles"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1353,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"HAND WITH INDEX FINGER AND THUMB CROSSED","unified":"1FAF0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faf0.png","sheet_x":55,"sheet_y":20,"short_name":"hand_with_index_finger_and_thumb_crossed","short_names":["hand_with_index_finger_and_thumb_crossed"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-partial","sort_order":178,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"1FAF0-1F3FB","non_qualified":null,"image":"1faf0-1f3fb.png","sheet_x":55,"sheet_y":21,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC":{"unified":"1FAF0-1F3FC","non_qualified":null,"image":"1faf0-1f3fc.png","sheet_x":55,"sheet_y":22,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD":{"unified":"1FAF0-1F3FD","non_qualified":null,"image":"1faf0-1f3fd.png","sheet_x":55,"sheet_y":23,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE":{"unified":"1FAF0-1F3FE","non_qualified":null,"image":"1faf0-1f3fe.png","sheet_x":55,"sheet_y":24,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF":{"unified":"1FAF0-1F3FF","non_qualified":null,"image":"1faf0-1f3ff.png","sheet_x":55,"sheet_y":25,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false}}},{"name":"RIGHTWARDS HAND","unified":"1FAF1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faf1.png","sheet_x":55,"sheet_y":26,"short_name":"rightwards_hand","short_names":["rightwards_hand"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-open","sort_order":169,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"1FAF1-1F3FB","non_qualified":null,"image":"1faf1-1f3fb.png","sheet_x":55,"sheet_y":27,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC":{"unified":"1FAF1-1F3FC","non_qualified":null,"image":"1faf1-1f3fc.png","sheet_x":55,"sheet_y":28,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD":{"unified":"1FAF1-1F3FD","non_qualified":null,"image":"1faf1-1f3fd.png","sheet_x":55,"sheet_y":29,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE":{"unified":"1FAF1-1F3FE","non_qualified":null,"image":"1faf1-1f3fe.png","sheet_x":55,"sheet_y":30,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF":{"unified":"1FAF1-1F3FF","non_qualified":null,"image":"1faf1-1f3ff.png","sheet_x":55,"sheet_y":31,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false}}},{"name":"LEFTWARDS HAND","unified":"1FAF2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faf2.png","sheet_x":55,"sheet_y":32,"short_name":"leftwards_hand","short_names":["leftwards_hand"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-open","sort_order":170,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"1FAF2-1F3FB","non_qualified":null,"image":"1faf2-1f3fb.png","sheet_x":55,"sheet_y":33,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC":{"unified":"1FAF2-1F3FC","non_qualified":null,"image":"1faf2-1f3fc.png","sheet_x":55,"sheet_y":34,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD":{"unified":"1FAF2-1F3FD","non_qualified":null,"image":"1faf2-1f3fd.png","sheet_x":55,"sheet_y":35,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE":{"unified":"1FAF2-1F3FE","non_qualified":null,"image":"1faf2-1f3fe.png","sheet_x":55,"sheet_y":36,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF":{"unified":"1FAF2-1F3FF","non_qualified":null,"image":"1faf2-1f3ff.png","sheet_x":55,"sheet_y":37,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false}}},{"name":"PALM DOWN HAND","unified":"1FAF3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faf3.png","sheet_x":55,"sheet_y":38,"short_name":"palm_down_hand","short_names":["palm_down_hand"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-open","sort_order":171,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"1FAF3-1F3FB","non_qualified":null,"image":"1faf3-1f3fb.png","sheet_x":55,"sheet_y":39,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC":{"unified":"1FAF3-1F3FC","non_qualified":null,"image":"1faf3-1f3fc.png","sheet_x":55,"sheet_y":40,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD":{"unified":"1FAF3-1F3FD","non_qualified":null,"image":"1faf3-1f3fd.png","sheet_x":55,"sheet_y":41,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE":{"unified":"1FAF3-1F3FE","non_qualified":null,"image":"1faf3-1f3fe.png","sheet_x":55,"sheet_y":42,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF":{"unified":"1FAF3-1F3FF","non_qualified":null,"image":"1faf3-1f3ff.png","sheet_x":55,"sheet_y":43,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false}}},{"name":"PALM UP HAND","unified":"1FAF4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faf4.png","sheet_x":55,"sheet_y":44,"short_name":"palm_up_hand","short_names":["palm_up_hand"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-open","sort_order":172,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"1FAF4-1F3FB","non_qualified":null,"image":"1faf4-1f3fb.png","sheet_x":55,"sheet_y":45,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC":{"unified":"1FAF4-1F3FC","non_qualified":null,"image":"1faf4-1f3fc.png","sheet_x":55,"sheet_y":46,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD":{"unified":"1FAF4-1F3FD","non_qualified":null,"image":"1faf4-1f3fd.png","sheet_x":55,"sheet_y":47,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE":{"unified":"1FAF4-1F3FE","non_qualified":null,"image":"1faf4-1f3fe.png","sheet_x":55,"sheet_y":48,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF":{"unified":"1FAF4-1F3FF","non_qualified":null,"image":"1faf4-1f3ff.png","sheet_x":55,"sheet_y":49,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false}}},{"name":"INDEX POINTING AT THE VIEWER","unified":"1FAF5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faf5.png","sheet_x":55,"sheet_y":50,"short_name":"index_pointing_at_the_viewer","short_names":["index_pointing_at_the_viewer"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-single-finger","sort_order":188,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"1FAF5-1F3FB","non_qualified":null,"image":"1faf5-1f3fb.png","sheet_x":55,"sheet_y":51,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC":{"unified":"1FAF5-1F3FC","non_qualified":null,"image":"1faf5-1f3fc.png","sheet_x":55,"sheet_y":52,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD":{"unified":"1FAF5-1F3FD","non_qualified":null,"image":"1faf5-1f3fd.png","sheet_x":55,"sheet_y":53,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE":{"unified":"1FAF5-1F3FE","non_qualified":null,"image":"1faf5-1f3fe.png","sheet_x":55,"sheet_y":54,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF":{"unified":"1FAF5-1F3FF","non_qualified":null,"image":"1faf5-1f3ff.png","sheet_x":55,"sheet_y":55,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false}}},{"name":"HEART HANDS","unified":"1FAF6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faf6.png","sheet_x":55,"sheet_y":56,"short_name":"heart_hands","short_names":["heart_hands"],"text":null,"texts":null,"category":"People & Body","subcategory":"hands","sort_order":197,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"1FAF6-1F3FB","non_qualified":null,"image":"1faf6-1f3fb.png","sheet_x":55,"sheet_y":57,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FC":{"unified":"1FAF6-1F3FC","non_qualified":null,"image":"1faf6-1f3fc.png","sheet_x":55,"sheet_y":58,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FD":{"unified":"1FAF6-1F3FD","non_qualified":null,"image":"1faf6-1f3fd.png","sheet_x":55,"sheet_y":59,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FE":{"unified":"1FAF6-1F3FE","non_qualified":null,"image":"1faf6-1f3fe.png","sheet_x":55,"sheet_y":60,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},"1F3FF":{"unified":"1FAF6-1F3FF","non_qualified":null,"image":"1faf6-1f3ff.png","sheet_x":56,"sheet_y":0,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false}}},{"name":"DOUBLE EXCLAMATION MARK","unified":"203C-FE0F","non_qualified":"203C","docomo":"E704","au":"EB30","softbank":null,"google":"FEB06","image":"203c-fe0f.png","sheet_x":56,"sheet_y":1,"short_name":"bangbang","short_names":["bangbang"],"text":null,"texts":null,"category":"Symbols","subcategory":"punctuation","sort_order":1470,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EXCLAMATION QUESTION MARK","unified":"2049-FE0F","non_qualified":"2049","docomo":"E703","au":"EB2F","softbank":null,"google":"FEB05","image":"2049-fe0f.png","sheet_x":56,"sheet_y":2,"short_name":"interrobang","short_names":["interrobang"],"text":null,"texts":null,"category":"Symbols","subcategory":"punctuation","sort_order":1471,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TRADE MARK SIGN","unified":"2122-FE0F","non_qualified":"2122","docomo":"E732","au":"E54E","softbank":"E537","google":"FEB2A","image":"2122-fe0f.png","sheet_x":56,"sheet_y":3,"short_name":"tm","short_names":["tm"],"text":null,"texts":null,"category":"Symbols","subcategory":"other-symbol","sort_order":1499,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"INFORMATION SOURCE","unified":"2139-FE0F","non_qualified":"2139","docomo":null,"au":"E533","softbank":null,"google":"FEB47","image":"2139-fe0f.png","sheet_x":56,"sheet_y":4,"short_name":"information_source","short_names":["information_source"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1524,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LEFT RIGHT ARROW","unified":"2194-FE0F","non_qualified":"2194","docomo":"E73C","au":"EB7A","softbank":null,"google":"FEAF6","image":"2194-fe0f.png","sheet_x":56,"sheet_y":5,"short_name":"left_right_arrow","short_names":["left_right_arrow"],"text":null,"texts":null,"category":"Symbols","subcategory":"arrow","sort_order":1400,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"UP DOWN ARROW","unified":"2195-FE0F","non_qualified":"2195","docomo":"E73D","au":"EB7B","softbank":null,"google":"FEAF7","image":"2195-fe0f.png","sheet_x":56,"sheet_y":6,"short_name":"arrow_up_down","short_names":["arrow_up_down"],"text":null,"texts":null,"category":"Symbols","subcategory":"arrow","sort_order":1399,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NORTH WEST ARROW","unified":"2196-FE0F","non_qualified":"2196","docomo":"E697","au":"E54C","softbank":"E237","google":"FEAF2","image":"2196-fe0f.png","sheet_x":56,"sheet_y":7,"short_name":"arrow_upper_left","short_names":["arrow_upper_left"],"text":null,"texts":null,"category":"Symbols","subcategory":"arrow","sort_order":1398,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NORTH EAST ARROW","unified":"2197-FE0F","non_qualified":"2197","docomo":"E678","au":"E555","softbank":"E236","google":"FEAF0","image":"2197-fe0f.png","sheet_x":56,"sheet_y":8,"short_name":"arrow_upper_right","short_names":["arrow_upper_right"],"text":null,"texts":null,"category":"Symbols","subcategory":"arrow","sort_order":1392,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SOUTH EAST ARROW","unified":"2198-FE0F","non_qualified":"2198","docomo":"E696","au":"E54D","softbank":"E238","google":"FEAF1","image":"2198-fe0f.png","sheet_x":56,"sheet_y":9,"short_name":"arrow_lower_right","short_names":["arrow_lower_right"],"text":null,"texts":null,"category":"Symbols","subcategory":"arrow","sort_order":1394,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SOUTH WEST ARROW","unified":"2199-FE0F","non_qualified":"2199","docomo":"E6A5","au":"E556","softbank":"E239","google":"FEAF3","image":"2199-fe0f.png","sheet_x":56,"sheet_y":10,"short_name":"arrow_lower_left","short_names":["arrow_lower_left"],"text":null,"texts":null,"category":"Symbols","subcategory":"arrow","sort_order":1396,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LEFTWARDS ARROW WITH HOOK","unified":"21A9-FE0F","non_qualified":"21A9","docomo":"E6DA","au":"E55D","softbank":null,"google":"FEB83","image":"21a9-fe0f.png","sheet_x":56,"sheet_y":11,"short_name":"leftwards_arrow_with_hook","short_names":["leftwards_arrow_with_hook"],"text":null,"texts":null,"category":"Symbols","subcategory":"arrow","sort_order":1401,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RIGHTWARDS ARROW WITH HOOK","unified":"21AA-FE0F","non_qualified":"21AA","docomo":null,"au":"E55C","softbank":null,"google":"FEB88","image":"21aa-fe0f.png","sheet_x":56,"sheet_y":12,"short_name":"arrow_right_hook","short_names":["arrow_right_hook"],"text":null,"texts":null,"category":"Symbols","subcategory":"arrow","sort_order":1402,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WATCH","unified":"231A","non_qualified":null,"docomo":"E71F","au":"E57A","softbank":null,"google":"FE01D","image":"231a.png","sheet_x":56,"sheet_y":13,"short_name":"watch","short_names":["watch"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":948,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HOURGLASS","unified":"231B","non_qualified":null,"docomo":"E71C","au":"E57B","softbank":null,"google":"FE01C","image":"231b.png","sheet_x":56,"sheet_y":14,"short_name":"hourglass","short_names":["hourglass"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":946,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"KEYBOARD","unified":"2328-FE0F","non_qualified":"2328","docomo":null,"au":null,"softbank":null,"google":null,"image":"2328-fe0f.png","sheet_x":56,"sheet_y":15,"short_name":"keyboard","short_names":["keyboard"],"text":null,"texts":null,"category":"Objects","subcategory":"computer","sort_order":1194,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EJECT BUTTON","unified":"23CF-FE0F","non_qualified":"23CF","docomo":null,"au":null,"softbank":null,"google":null,"image":"23cf-fe0f.png","sheet_x":56,"sheet_y":16,"short_name":"eject","short_names":["eject"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1454,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK RIGHT-POINTING DOUBLE TRIANGLE","unified":"23E9","non_qualified":null,"docomo":null,"au":"E530","softbank":"E23C","google":"FEAFE","image":"23e9.png","sheet_x":56,"sheet_y":17,"short_name":"fast_forward","short_names":["fast_forward"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1441,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK LEFT-POINTING DOUBLE TRIANGLE","unified":"23EA","non_qualified":null,"docomo":null,"au":"E52F","softbank":"E23D","google":"FEAFF","image":"23ea.png","sheet_x":56,"sheet_y":18,"short_name":"rewind","short_names":["rewind"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1445,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK UP-POINTING DOUBLE TRIANGLE","unified":"23EB","non_qualified":null,"docomo":null,"au":"E545","softbank":null,"google":"FEB03","image":"23eb.png","sheet_x":56,"sheet_y":19,"short_name":"arrow_double_up","short_names":["arrow_double_up"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1448,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK DOWN-POINTING DOUBLE TRIANGLE","unified":"23EC","non_qualified":null,"docomo":null,"au":"E544","softbank":null,"google":"FEB02","image":"23ec.png","sheet_x":56,"sheet_y":20,"short_name":"arrow_double_down","short_names":["arrow_double_down"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1450,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NEXT TRACK BUTTON","unified":"23ED-FE0F","non_qualified":"23ED","docomo":null,"au":null,"softbank":null,"google":null,"image":"23ed-fe0f.png","sheet_x":56,"sheet_y":21,"short_name":"black_right_pointing_double_triangle_with_vertical_bar","short_names":["black_right_pointing_double_triangle_with_vertical_bar"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1442,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LAST TRACK BUTTON","unified":"23EE-FE0F","non_qualified":"23EE","docomo":null,"au":null,"softbank":null,"google":null,"image":"23ee-fe0f.png","sheet_x":56,"sheet_y":22,"short_name":"black_left_pointing_double_triangle_with_vertical_bar","short_names":["black_left_pointing_double_triangle_with_vertical_bar"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1446,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PLAY OR PAUSE BUTTON","unified":"23EF-FE0F","non_qualified":"23EF","docomo":null,"au":null,"softbank":null,"google":null,"image":"23ef-fe0f.png","sheet_x":56,"sheet_y":23,"short_name":"black_right_pointing_triangle_with_double_vertical_bar","short_names":["black_right_pointing_triangle_with_double_vertical_bar"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1443,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ALARM CLOCK","unified":"23F0","non_qualified":null,"docomo":"E6BA","au":"E594","softbank":null,"google":"FE02A","image":"23f0.png","sheet_x":56,"sheet_y":24,"short_name":"alarm_clock","short_names":["alarm_clock"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":949,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"STOPWATCH","unified":"23F1-FE0F","non_qualified":"23F1","docomo":null,"au":null,"softbank":null,"google":null,"image":"23f1-fe0f.png","sheet_x":56,"sheet_y":25,"short_name":"stopwatch","short_names":["stopwatch"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":950,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TIMER CLOCK","unified":"23F2-FE0F","non_qualified":"23F2","docomo":null,"au":null,"softbank":null,"google":null,"image":"23f2-fe0f.png","sheet_x":56,"sheet_y":26,"short_name":"timer_clock","short_names":["timer_clock"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":951,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HOURGLASS WITH FLOWING SAND","unified":"23F3","non_qualified":null,"docomo":"E71C","au":"E47C","softbank":null,"google":"FE01B","image":"23f3.png","sheet_x":56,"sheet_y":27,"short_name":"hourglass_flowing_sand","short_names":["hourglass_flowing_sand"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":947,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PAUSE BUTTON","unified":"23F8-FE0F","non_qualified":"23F8","docomo":null,"au":null,"softbank":null,"google":null,"image":"23f8-fe0f.png","sheet_x":56,"sheet_y":28,"short_name":"double_vertical_bar","short_names":["double_vertical_bar"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1451,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"STOP BUTTON","unified":"23F9-FE0F","non_qualified":"23F9","docomo":null,"au":null,"softbank":null,"google":null,"image":"23f9-fe0f.png","sheet_x":56,"sheet_y":29,"short_name":"black_square_for_stop","short_names":["black_square_for_stop"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1452,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RECORD BUTTON","unified":"23FA-FE0F","non_qualified":"23FA","docomo":null,"au":null,"softbank":null,"google":null,"image":"23fa-fe0f.png","sheet_x":56,"sheet_y":30,"short_name":"black_circle_for_record","short_names":["black_circle_for_record"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1453,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CIRCLED LATIN CAPITAL LETTER M","unified":"24C2-FE0F","non_qualified":"24C2","docomo":"E65C","au":"E5BC","softbank":null,"google":"FE7E1","image":"24c2-fe0f.png","sheet_x":56,"sheet_y":31,"short_name":"m","short_names":["m"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1526,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK SMALL SQUARE","unified":"25AA-FE0F","non_qualified":"25AA","docomo":null,"au":"E532","softbank":null,"google":"FEB6E","image":"25aa-fe0f.png","sheet_x":56,"sheet_y":32,"short_name":"black_small_square","short_names":["black_small_square"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1574,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WHITE SMALL SQUARE","unified":"25AB-FE0F","non_qualified":"25AB","docomo":null,"au":"E531","softbank":null,"google":"FEB6D","image":"25ab-fe0f.png","sheet_x":56,"sheet_y":33,"short_name":"white_small_square","short_names":["white_small_square"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1575,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK RIGHT-POINTING TRIANGLE","unified":"25B6-FE0F","non_qualified":"25B6","docomo":null,"au":"E52E","softbank":"E23A","google":"FEAFC","image":"25b6-fe0f.png","sheet_x":56,"sheet_y":34,"short_name":"arrow_forward","short_names":["arrow_forward"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1440,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK LEFT-POINTING TRIANGLE","unified":"25C0-FE0F","non_qualified":"25C0","docomo":null,"au":"E52D","softbank":"E23B","google":"FEAFD","image":"25c0-fe0f.png","sheet_x":56,"sheet_y":35,"short_name":"arrow_backward","short_names":["arrow_backward"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1444,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WHITE MEDIUM SQUARE","unified":"25FB-FE0F","non_qualified":"25FB","docomo":null,"au":"E538","softbank":null,"google":"FEB71","image":"25fb-fe0f.png","sheet_x":56,"sheet_y":36,"short_name":"white_medium_square","short_names":["white_medium_square"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1571,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK MEDIUM SQUARE","unified":"25FC-FE0F","non_qualified":"25FC","docomo":null,"au":"E539","softbank":null,"google":"FEB72","image":"25fc-fe0f.png","sheet_x":56,"sheet_y":37,"short_name":"black_medium_square","short_names":["black_medium_square"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1570,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WHITE MEDIUM SMALL SQUARE","unified":"25FD","non_qualified":null,"docomo":null,"au":"E534","softbank":null,"google":"FEB6F","image":"25fd.png","sheet_x":56,"sheet_y":38,"short_name":"white_medium_small_square","short_names":["white_medium_small_square"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1573,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK MEDIUM SMALL SQUARE","unified":"25FE","non_qualified":null,"docomo":null,"au":"E535","softbank":null,"google":"FEB70","image":"25fe.png","sheet_x":56,"sheet_y":39,"short_name":"black_medium_small_square","short_names":["black_medium_small_square"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1572,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK SUN WITH RAYS","unified":"2600-FE0F","non_qualified":"2600","docomo":"E63E","au":"E488","softbank":"E04A","google":"FE000","image":"2600-fe0f.png","sheet_x":56,"sheet_y":40,"short_name":"sunny","short_names":["sunny"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":990,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOUD","unified":"2601-FE0F","non_qualified":"2601","docomo":"E63F","au":"E48D","softbank":"E049","google":"FE001","image":"2601-fe0f.png","sheet_x":56,"sheet_y":41,"short_name":"cloud","short_names":["cloud"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":998,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"UMBRELLA","unified":"2602-FE0F","non_qualified":"2602","docomo":null,"au":null,"softbank":null,"google":null,"image":"2602-fe0f.png","sheet_x":56,"sheet_y":42,"short_name":"umbrella","short_names":["umbrella"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1013,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SNOWMAN","unified":"2603-FE0F","non_qualified":"2603","docomo":null,"au":null,"softbank":null,"google":null,"image":"2603-fe0f.png","sheet_x":56,"sheet_y":43,"short_name":"snowman","short_names":["snowman"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1018,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"COMET","unified":"2604-FE0F","non_qualified":"2604","docomo":null,"au":null,"softbank":null,"google":null,"image":"2604-fe0f.png","sheet_x":56,"sheet_y":44,"short_name":"comet","short_names":["comet"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1020,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK TELEPHONE","unified":"260E-FE0F","non_qualified":"260E","docomo":"E687","au":"E596","softbank":"E009","google":"FE523","image":"260e-fe0f.png","sheet_x":56,"sheet_y":45,"short_name":"phone","short_names":["phone","telephone"],"text":null,"texts":null,"category":"Objects","subcategory":"phone","sort_order":1184,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BALLOT BOX WITH CHECK","unified":"2611-FE0F","non_qualified":"2611","docomo":null,"au":"EB02","softbank":null,"google":"FEB8B","image":"2611-fe0f.png","sheet_x":56,"sheet_y":46,"short_name":"ballot_box_with_check","short_names":["ballot_box_with_check"],"text":null,"texts":null,"category":"Symbols","subcategory":"other-symbol","sort_order":1487,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"UMBRELLA WITH RAIN DROPS","unified":"2614","non_qualified":null,"docomo":"E640","au":"E48C","softbank":"E04B","google":"FE002","image":"2614.png","sheet_x":56,"sheet_y":47,"short_name":"umbrella_with_rain_drops","short_names":["umbrella_with_rain_drops"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1014,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HOT BEVERAGE","unified":"2615","non_qualified":null,"docomo":"E670","au":"E597","softbank":"E045","google":"FE981","image":"2615.png","sheet_x":56,"sheet_y":48,"short_name":"coffee","short_names":["coffee"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"drink","sort_order":781,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SHAMROCK","unified":"2618-FE0F","non_qualified":"2618","docomo":null,"au":null,"softbank":null,"google":null,"image":"2618-fe0f.png","sheet_x":56,"sheet_y":49,"short_name":"shamrock","short_names":["shamrock"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-other","sort_order":667,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WHITE UP POINTING INDEX","unified":"261D-FE0F","non_qualified":"261D","docomo":null,"au":"E4F6","softbank":"E00F","google":"FEB98","image":"261d-fe0f.png","sheet_x":56,"sheet_y":50,"short_name":"point_up","short_names":["point_up"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-single-finger","sort_order":187,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"261D-1F3FB","non_qualified":null,"image":"261d-1f3fb.png","sheet_x":56,"sheet_y":51,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"261D-1F3FC","non_qualified":null,"image":"261d-1f3fc.png","sheet_x":56,"sheet_y":52,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"261D-1F3FD","non_qualified":null,"image":"261d-1f3fd.png","sheet_x":56,"sheet_y":53,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"261D-1F3FE","non_qualified":null,"image":"261d-1f3fe.png","sheet_x":56,"sheet_y":54,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"261D-1F3FF","non_qualified":null,"image":"261d-1f3ff.png","sheet_x":56,"sheet_y":55,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"SKULL AND CROSSBONES","unified":"2620-FE0F","non_qualified":"2620","docomo":null,"au":null,"softbank":null,"google":null,"image":"2620-fe0f.png","sheet_x":56,"sheet_y":56,"short_name":"skull_and_crossbones","short_names":["skull_and_crossbones"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-negative","sort_order":106,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RADIOACTIVE","unified":"2622-FE0F","non_qualified":"2622","docomo":null,"au":null,"softbank":null,"google":null,"image":"2622-fe0f.png","sheet_x":56,"sheet_y":57,"short_name":"radioactive_sign","short_names":["radioactive_sign"],"text":null,"texts":null,"category":"Symbols","subcategory":"warning","sort_order":1389,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BIOHAZARD","unified":"2623-FE0F","non_qualified":"2623","docomo":null,"au":null,"softbank":null,"google":null,"image":"2623-fe0f.png","sheet_x":56,"sheet_y":58,"short_name":"biohazard_sign","short_names":["biohazard_sign"],"text":null,"texts":null,"category":"Symbols","subcategory":"warning","sort_order":1390,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ORTHODOX CROSS","unified":"2626-FE0F","non_qualified":"2626","docomo":null,"au":null,"softbank":null,"google":null,"image":"2626-fe0f.png","sheet_x":56,"sheet_y":59,"short_name":"orthodox_cross","short_names":["orthodox_cross"],"text":null,"texts":null,"category":"Symbols","subcategory":"religion","sort_order":1419,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"STAR AND CRESCENT","unified":"262A-FE0F","non_qualified":"262A","docomo":null,"au":null,"softbank":null,"google":null,"image":"262a-fe0f.png","sheet_x":56,"sheet_y":60,"short_name":"star_and_crescent","short_names":["star_and_crescent"],"text":null,"texts":null,"category":"Symbols","subcategory":"religion","sort_order":1420,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PEACE SYMBOL","unified":"262E-FE0F","non_qualified":"262E","docomo":null,"au":null,"softbank":null,"google":null,"image":"262e-fe0f.png","sheet_x":57,"sheet_y":0,"short_name":"peace_symbol","short_names":["peace_symbol"],"text":null,"texts":null,"category":"Symbols","subcategory":"religion","sort_order":1421,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"YIN YANG","unified":"262F-FE0F","non_qualified":"262F","docomo":null,"au":null,"softbank":null,"google":null,"image":"262f-fe0f.png","sheet_x":57,"sheet_y":1,"short_name":"yin_yang","short_names":["yin_yang"],"text":null,"texts":null,"category":"Symbols","subcategory":"religion","sort_order":1417,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WHEEL OF DHARMA","unified":"2638-FE0F","non_qualified":"2638","docomo":null,"au":null,"softbank":null,"google":null,"image":"2638-fe0f.png","sheet_x":57,"sheet_y":2,"short_name":"wheel_of_dharma","short_names":["wheel_of_dharma"],"text":null,"texts":null,"category":"Symbols","subcategory":"religion","sort_order":1416,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FROWNING FACE","unified":"2639-FE0F","non_qualified":"2639","docomo":null,"au":null,"softbank":null,"google":null,"image":"2639-fe0f.png","sheet_x":57,"sheet_y":3,"short_name":"white_frowning_face","short_names":["white_frowning_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":77,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WHITE SMILING FACE","unified":"263A-FE0F","non_qualified":"263A","docomo":"E6F0","au":"E4FB","softbank":"E414","google":"FE336","image":"263a-fe0f.png","sheet_x":57,"sheet_y":4,"short_name":"relaxed","short_names":["relaxed"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-affection","sort_order":20,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FEMALE SIGN","unified":"2640-FE0F","non_qualified":"2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"2640-fe0f.png","sheet_x":57,"sheet_y":5,"short_name":"female_sign","short_names":["female_sign"],"text":null,"texts":null,"category":"Symbols","subcategory":"gender","sort_order":1461,"added_in":"4.0","has_img_apple":false,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MALE SIGN","unified":"2642-FE0F","non_qualified":"2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"2642-fe0f.png","sheet_x":57,"sheet_y":6,"short_name":"male_sign","short_names":["male_sign"],"text":null,"texts":null,"category":"Symbols","subcategory":"gender","sort_order":1462,"added_in":"4.0","has_img_apple":false,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ARIES","unified":"2648","non_qualified":null,"docomo":"E646","au":"E48F","softbank":"E23F","google":"FE02B","image":"2648.png","sheet_x":57,"sheet_y":7,"short_name":"aries","short_names":["aries"],"text":null,"texts":null,"category":"Symbols","subcategory":"zodiac","sort_order":1424,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TAURUS","unified":"2649","non_qualified":null,"docomo":"E647","au":"E490","softbank":"E240","google":"FE02C","image":"2649.png","sheet_x":57,"sheet_y":8,"short_name":"taurus","short_names":["taurus"],"text":null,"texts":null,"category":"Symbols","subcategory":"zodiac","sort_order":1425,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GEMINI","unified":"264A","non_qualified":null,"docomo":"E648","au":"E491","softbank":"E241","google":"FE02D","image":"264a.png","sheet_x":57,"sheet_y":9,"short_name":"gemini","short_names":["gemini"],"text":null,"texts":null,"category":"Symbols","subcategory":"zodiac","sort_order":1426,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CANCER","unified":"264B","non_qualified":null,"docomo":"E649","au":"E492","softbank":"E242","google":"FE02E","image":"264b.png","sheet_x":57,"sheet_y":10,"short_name":"cancer","short_names":["cancer"],"text":null,"texts":null,"category":"Symbols","subcategory":"zodiac","sort_order":1427,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LEO","unified":"264C","non_qualified":null,"docomo":"E64A","au":"E493","softbank":"E243","google":"FE02F","image":"264c.png","sheet_x":57,"sheet_y":11,"short_name":"leo","short_names":["leo"],"text":null,"texts":null,"category":"Symbols","subcategory":"zodiac","sort_order":1428,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"VIRGO","unified":"264D","non_qualified":null,"docomo":"E64B","au":"E494","softbank":"E244","google":"FE030","image":"264d.png","sheet_x":57,"sheet_y":12,"short_name":"virgo","short_names":["virgo"],"text":null,"texts":null,"category":"Symbols","subcategory":"zodiac","sort_order":1429,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LIBRA","unified":"264E","non_qualified":null,"docomo":"E64C","au":"E495","softbank":"E245","google":"FE031","image":"264e.png","sheet_x":57,"sheet_y":13,"short_name":"libra","short_names":["libra"],"text":null,"texts":null,"category":"Symbols","subcategory":"zodiac","sort_order":1430,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SCORPIUS","unified":"264F","non_qualified":null,"docomo":"E64D","au":"E496","softbank":"E246","google":"FE032","image":"264f.png","sheet_x":57,"sheet_y":14,"short_name":"scorpius","short_names":["scorpius"],"text":null,"texts":null,"category":"Symbols","subcategory":"zodiac","sort_order":1431,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SAGITTARIUS","unified":"2650","non_qualified":null,"docomo":"E64E","au":"E497","softbank":"E247","google":"FE033","image":"2650.png","sheet_x":57,"sheet_y":15,"short_name":"sagittarius","short_names":["sagittarius"],"text":null,"texts":null,"category":"Symbols","subcategory":"zodiac","sort_order":1432,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CAPRICORN","unified":"2651","non_qualified":null,"docomo":"E64F","au":"E498","softbank":"E248","google":"FE034","image":"2651.png","sheet_x":57,"sheet_y":16,"short_name":"capricorn","short_names":["capricorn"],"text":null,"texts":null,"category":"Symbols","subcategory":"zodiac","sort_order":1433,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"AQUARIUS","unified":"2652","non_qualified":null,"docomo":"E650","au":"E499","softbank":"E249","google":"FE035","image":"2652.png","sheet_x":57,"sheet_y":17,"short_name":"aquarius","short_names":["aquarius"],"text":null,"texts":null,"category":"Symbols","subcategory":"zodiac","sort_order":1434,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PISCES","unified":"2653","non_qualified":null,"docomo":"E651","au":"E49A","softbank":"E24A","google":"FE036","image":"2653.png","sheet_x":57,"sheet_y":18,"short_name":"pisces","short_names":["pisces"],"text":null,"texts":null,"category":"Symbols","subcategory":"zodiac","sort_order":1435,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CHESS PAWN","unified":"265F-FE0F","non_qualified":"265F","docomo":null,"au":null,"softbank":null,"google":null,"image":"265f-fe0f.png","sheet_x":57,"sheet_y":19,"short_name":"chess_pawn","short_names":["chess_pawn"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1099,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK SPADE SUIT","unified":"2660-FE0F","non_qualified":"2660","docomo":"E68E","au":"E5A1","softbank":"E20E","google":"FEB1B","image":"2660-fe0f.png","sheet_x":57,"sheet_y":20,"short_name":"spades","short_names":["spades"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1095,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK CLUB SUIT","unified":"2663-FE0F","non_qualified":"2663","docomo":"E690","au":"E5A3","softbank":"E20F","google":"FEB1D","image":"2663-fe0f.png","sheet_x":57,"sheet_y":21,"short_name":"clubs","short_names":["clubs"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1098,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK HEART SUIT","unified":"2665-FE0F","non_qualified":"2665","docomo":"E68D","au":"EAA5","softbank":"E20C","google":"FEB1A","image":"2665-fe0f.png","sheet_x":57,"sheet_y":22,"short_name":"hearts","short_names":["hearts"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1096,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK DIAMOND SUIT","unified":"2666-FE0F","non_qualified":"2666","docomo":"E68F","au":"E5A2","softbank":"E20D","google":"FEB1C","image":"2666-fe0f.png","sheet_x":57,"sheet_y":23,"short_name":"diamonds","short_names":["diamonds"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1097,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HOT SPRINGS","unified":"2668-FE0F","non_qualified":"2668","docomo":"E6F7","au":"E4BC","softbank":"E123","google":"FE7FA","image":"2668-fe0f.png","sheet_x":57,"sheet_y":24,"short_name":"hotsprings","short_names":["hotsprings"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-other","sort_order":865,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK UNIVERSAL RECYCLING SYMBOL","unified":"267B-FE0F","non_qualified":"267B","docomo":"E735","au":"EB79","softbank":null,"google":"FEB2C","image":"267b-fe0f.png","sheet_x":57,"sheet_y":25,"short_name":"recycle","short_names":["recycle"],"text":null,"texts":null,"category":"Symbols","subcategory":"other-symbol","sort_order":1480,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"INFINITY","unified":"267E-FE0F","non_qualified":"267E","docomo":null,"au":null,"softbank":null,"google":null,"image":"267e-fe0f.png","sheet_x":57,"sheet_y":26,"short_name":"infinity","short_names":["infinity"],"text":null,"texts":null,"category":"Symbols","subcategory":"math","sort_order":1469,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WHEELCHAIR SYMBOL","unified":"267F","non_qualified":null,"docomo":"E69B","au":"E47F","softbank":"E20A","google":"FEB20","image":"267f.png","sheet_x":57,"sheet_y":27,"short_name":"wheelchair","short_names":["wheelchair"],"text":null,"texts":null,"category":"Symbols","subcategory":"transport-sign","sort_order":1368,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HAMMER AND PICK","unified":"2692-FE0F","non_qualified":"2692","docomo":null,"au":null,"softbank":null,"google":null,"image":"2692-fe0f.png","sheet_x":57,"sheet_y":28,"short_name":"hammer_and_pick","short_names":["hammer_and_pick"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1297,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ANCHOR","unified":"2693","non_qualified":null,"docomo":"E661","au":"E4A9","softbank":null,"google":"FE4C1","image":"2693.png","sheet_x":57,"sheet_y":29,"short_name":"anchor","short_names":["anchor"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-water","sort_order":922,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CROSSED SWORDS","unified":"2694-FE0F","non_qualified":"2694","docomo":null,"au":null,"softbank":null,"google":null,"image":"2694-fe0f.png","sheet_x":57,"sheet_y":30,"short_name":"crossed_swords","short_names":["crossed_swords"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1300,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MEDICAL SYMBOL","unified":"2695-FE0F","non_qualified":"2695","docomo":null,"au":null,"softbank":null,"google":null,"image":"2695-fe0f.png","sheet_x":57,"sheet_y":31,"short_name":"medical_symbol","short_names":["medical_symbol","staff_of_aesculapius"],"text":null,"texts":null,"category":"Symbols","subcategory":"other-symbol","sort_order":1479,"added_in":"4.0","has_img_apple":false,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BALANCE SCALE","unified":"2696-FE0F","non_qualified":"2696","docomo":null,"au":null,"softbank":null,"google":null,"image":"2696-fe0f.png","sheet_x":57,"sheet_y":32,"short_name":"scales","short_names":["scales"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1311,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ALEMBIC","unified":"2697-FE0F","non_qualified":"2697","docomo":null,"au":null,"softbank":null,"google":null,"image":"2697-fe0f.png","sheet_x":57,"sheet_y":33,"short_name":"alembic","short_names":["alembic"],"text":null,"texts":null,"category":"Objects","subcategory":"science","sort_order":1319,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GEAR","unified":"2699-FE0F","non_qualified":"2699","docomo":null,"au":null,"softbank":null,"google":null,"image":"2699-fe0f.png","sheet_x":57,"sheet_y":34,"short_name":"gear","short_names":["gear"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1309,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ATOM SYMBOL","unified":"269B-FE0F","non_qualified":"269B","docomo":null,"au":null,"softbank":null,"google":null,"image":"269b-fe0f.png","sheet_x":57,"sheet_y":35,"short_name":"atom_symbol","short_names":["atom_symbol"],"text":null,"texts":null,"category":"Symbols","subcategory":"religion","sort_order":1413,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FLEUR-DE-LIS","unified":"269C-FE0F","non_qualified":"269C","docomo":null,"au":null,"softbank":null,"google":null,"image":"269c-fe0f.png","sheet_x":57,"sheet_y":36,"short_name":"fleur_de_lis","short_names":["fleur_de_lis"],"text":null,"texts":null,"category":"Symbols","subcategory":"other-symbol","sort_order":1481,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WARNING SIGN","unified":"26A0-FE0F","non_qualified":"26A0","docomo":"E737","au":"E481","softbank":"E252","google":"FEB23","image":"26a0-fe0f.png","sheet_x":57,"sheet_y":37,"short_name":"warning","short_names":["warning"],"text":null,"texts":null,"category":"Symbols","subcategory":"warning","sort_order":1378,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HIGH VOLTAGE SIGN","unified":"26A1","non_qualified":null,"docomo":"E642","au":"E487","softbank":"E13D","google":"FE004","image":"26a1.png","sheet_x":57,"sheet_y":38,"short_name":"zap","short_names":["zap"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1016,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TRANSGENDER SYMBOL","unified":"26A7-FE0F","non_qualified":"26A7","docomo":null,"au":null,"softbank":null,"google":null,"image":"26a7-fe0f.png","sheet_x":57,"sheet_y":39,"short_name":"transgender_symbol","short_names":["transgender_symbol"],"text":null,"texts":null,"category":"Symbols","subcategory":"gender","sort_order":1463,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MEDIUM WHITE CIRCLE","unified":"26AA","non_qualified":null,"docomo":"E69C","au":"E53A","softbank":null,"google":"FEB65","image":"26aa.png","sheet_x":57,"sheet_y":40,"short_name":"white_circle","short_names":["white_circle"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1560,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MEDIUM BLACK CIRCLE","unified":"26AB","non_qualified":null,"docomo":"E69C","au":"E53B","softbank":null,"google":"FEB66","image":"26ab.png","sheet_x":57,"sheet_y":41,"short_name":"black_circle","short_names":["black_circle"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1559,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"COFFIN","unified":"26B0-FE0F","non_qualified":"26B0","docomo":null,"au":null,"softbank":null,"google":null,"image":"26b0-fe0f.png","sheet_x":57,"sheet_y":42,"short_name":"coffin","short_names":["coffin"],"text":null,"texts":null,"category":"Objects","subcategory":"other-object","sort_order":1359,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FUNERAL URN","unified":"26B1-FE0F","non_qualified":"26B1","docomo":null,"au":null,"softbank":null,"google":null,"image":"26b1-fe0f.png","sheet_x":57,"sheet_y":43,"short_name":"funeral_urn","short_names":["funeral_urn"],"text":null,"texts":null,"category":"Objects","subcategory":"other-object","sort_order":1361,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SOCCER BALL","unified":"26BD","non_qualified":null,"docomo":"E656","au":"E4B6","softbank":"E018","google":"FE7D4","image":"26bd.png","sheet_x":57,"sheet_y":44,"short_name":"soccer","short_names":["soccer"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1051,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BASEBALL","unified":"26BE","non_qualified":null,"docomo":"E653","au":"E4BA","softbank":"E016","google":"FE7D1","image":"26be.png","sheet_x":57,"sheet_y":45,"short_name":"baseball","short_names":["baseball"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1052,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SNOWMAN WITHOUT SNOW","unified":"26C4","non_qualified":null,"docomo":"E641","au":"E485","softbank":"E048","google":"FE003","image":"26c4.png","sheet_x":57,"sheet_y":46,"short_name":"snowman_without_snow","short_names":["snowman_without_snow"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1019,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SUN BEHIND CLOUD","unified":"26C5","non_qualified":null,"docomo":"E63E-E63F","au":"E48E","softbank":null,"google":"FE00F","image":"26c5.png","sheet_x":57,"sheet_y":47,"short_name":"partly_sunny","short_names":["partly_sunny"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":999,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOUD WITH LIGHTNING AND RAIN","unified":"26C8-FE0F","non_qualified":"26C8","docomo":null,"au":null,"softbank":null,"google":null,"image":"26c8-fe0f.png","sheet_x":57,"sheet_y":48,"short_name":"thunder_cloud_and_rain","short_names":["thunder_cloud_and_rain"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1000,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"OPHIUCHUS","unified":"26CE","non_qualified":null,"docomo":null,"au":"E49B","softbank":"E24B","google":"FE037","image":"26ce.png","sheet_x":57,"sheet_y":49,"short_name":"ophiuchus","short_names":["ophiuchus"],"text":null,"texts":null,"category":"Symbols","subcategory":"zodiac","sort_order":1436,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PICK","unified":"26CF-FE0F","non_qualified":"26CF","docomo":null,"au":null,"softbank":null,"google":null,"image":"26cf-fe0f.png","sheet_x":57,"sheet_y":50,"short_name":"pick","short_names":["pick"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1296,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RESCUE WORKER\u2019S HELMET","unified":"26D1-FE0F","non_qualified":"26D1","docomo":null,"au":null,"softbank":null,"google":null,"image":"26d1-fe0f.png","sheet_x":57,"sheet_y":51,"short_name":"helmet_with_white_cross","short_names":["helmet_with_white_cross"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1150,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CHAINS","unified":"26D3-FE0F","non_qualified":"26D3","docomo":null,"au":null,"softbank":null,"google":null,"image":"26d3-fe0f.png","sheet_x":57,"sheet_y":52,"short_name":"chains","short_names":["chains"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1314,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NO ENTRY","unified":"26D4","non_qualified":null,"docomo":"E72F","au":"E484","softbank":null,"google":"FEB26","image":"26d4.png","sheet_x":57,"sheet_y":53,"short_name":"no_entry","short_names":["no_entry"],"text":null,"texts":null,"category":"Symbols","subcategory":"warning","sort_order":1380,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SHINTO SHRINE","unified":"26E9-FE0F","non_qualified":"26E9","docomo":null,"au":null,"softbank":null,"google":null,"image":"26e9-fe0f.png","sheet_x":57,"sheet_y":54,"short_name":"shinto_shrine","short_names":["shinto_shrine"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-religious","sort_order":853,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CHURCH","unified":"26EA","non_qualified":null,"docomo":null,"au":"E5BB","softbank":"E037","google":"FE4BB","image":"26ea.png","sheet_x":57,"sheet_y":55,"short_name":"church","short_names":["church"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-religious","sort_order":849,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MOUNTAIN","unified":"26F0-FE0F","non_qualified":"26F0","docomo":null,"au":null,"softbank":null,"google":null,"image":"26f0-fe0f.png","sheet_x":57,"sheet_y":56,"short_name":"mountain","short_names":["mountain"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-geographic","sort_order":814,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"UMBRELLA ON GROUND","unified":"26F1-FE0F","non_qualified":"26F1","docomo":null,"au":null,"softbank":null,"google":null,"image":"26f1-fe0f.png","sheet_x":57,"sheet_y":57,"short_name":"umbrella_on_ground","short_names":["umbrella_on_ground"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1015,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FOUNTAIN","unified":"26F2","non_qualified":null,"docomo":null,"au":"E5CF","softbank":"E121","google":"FE4BC","image":"26f2.png","sheet_x":57,"sheet_y":58,"short_name":"fountain","short_names":["fountain"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-other","sort_order":855,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FLAG IN HOLE","unified":"26F3","non_qualified":null,"docomo":"E654","au":"E599","softbank":"E014","google":"FE7D2","image":"26f3.png","sheet_x":57,"sheet_y":59,"short_name":"golf","short_names":["golf"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1070,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FERRY","unified":"26F4-FE0F","non_qualified":"26F4","docomo":null,"au":null,"softbank":null,"google":null,"image":"26f4-fe0f.png","sheet_x":57,"sheet_y":60,"short_name":"ferry","short_names":["ferry"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-water","sort_order":928,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SAILBOAT","unified":"26F5","non_qualified":null,"docomo":"E6A3","au":"E4B4","softbank":"E01C","google":"FE7EA","image":"26f5.png","sheet_x":58,"sheet_y":0,"short_name":"boat","short_names":["boat","sailboat"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-water","sort_order":924,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SKIER","unified":"26F7-FE0F","non_qualified":"26F7","docomo":null,"au":null,"softbank":null,"google":null,"image":"26f7-fe0f.png","sheet_x":58,"sheet_y":1,"short_name":"skier","short_names":["skier"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":436,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ICE SKATE","unified":"26F8-FE0F","non_qualified":"26F8","docomo":null,"au":null,"softbank":null,"google":null,"image":"26f8-fe0f.png","sheet_x":58,"sheet_y":2,"short_name":"ice_skate","short_names":["ice_skate"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1071,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WOMAN BOUNCING BALL","unified":"26F9-FE0F-200D-2640-FE0F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"26f9-fe0f-200d-2640-fe0f.png","sheet_x":58,"sheet_y":3,"short_name":"woman-bouncing-ball","short_names":["woman-bouncing-ball"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":452,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"26F9-1F3FB-200D-2640-FE0F","non_qualified":"26F9-1F3FB-200D-2640","image":"26f9-1f3fb-200d-2640-fe0f.png","sheet_x":58,"sheet_y":4,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"26F9-1F3FC-200D-2640-FE0F","non_qualified":"26F9-1F3FC-200D-2640","image":"26f9-1f3fc-200d-2640-fe0f.png","sheet_x":58,"sheet_y":5,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"26F9-1F3FD-200D-2640-FE0F","non_qualified":"26F9-1F3FD-200D-2640","image":"26f9-1f3fd-200d-2640-fe0f.png","sheet_x":58,"sheet_y":6,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"26F9-1F3FE-200D-2640-FE0F","non_qualified":"26F9-1F3FE-200D-2640","image":"26f9-1f3fe-200d-2640-fe0f.png","sheet_x":58,"sheet_y":7,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"26F9-1F3FF-200D-2640-FE0F","non_qualified":"26F9-1F3FF-200D-2640","image":"26f9-1f3ff-200d-2640-fe0f.png","sheet_x":58,"sheet_y":8,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN BOUNCING BALL","unified":"26F9-FE0F-200D-2642-FE0F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"26f9-fe0f-200d-2642-fe0f.png","sheet_x":58,"sheet_y":9,"short_name":"man-bouncing-ball","short_names":["man-bouncing-ball"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":451,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"26F9-1F3FB-200D-2642-FE0F","non_qualified":"26F9-1F3FB-200D-2642","image":"26f9-1f3fb-200d-2642-fe0f.png","sheet_x":58,"sheet_y":10,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"26F9-1F3FC-200D-2642-FE0F","non_qualified":"26F9-1F3FC-200D-2642","image":"26f9-1f3fc-200d-2642-fe0f.png","sheet_x":58,"sheet_y":11,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"26F9-1F3FD-200D-2642-FE0F","non_qualified":"26F9-1F3FD-200D-2642","image":"26f9-1f3fd-200d-2642-fe0f.png","sheet_x":58,"sheet_y":12,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"26F9-1F3FE-200D-2642-FE0F","non_qualified":"26F9-1F3FE-200D-2642","image":"26f9-1f3fe-200d-2642-fe0f.png","sheet_x":58,"sheet_y":13,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"26F9-1F3FF-200D-2642-FE0F","non_qualified":"26F9-1F3FF-200D-2642","image":"26f9-1f3ff-200d-2642-fe0f.png","sheet_x":58,"sheet_y":14,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"26F9-FE0F"},{"name":"PERSON BOUNCING BALL","unified":"26F9-FE0F","non_qualified":"26F9","docomo":null,"au":null,"softbank":null,"google":null,"image":"26f9-fe0f.png","sheet_x":58,"sheet_y":15,"short_name":"person_with_ball","short_names":["person_with_ball"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":450,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"26F9-1F3FB","non_qualified":null,"image":"26f9-1f3fb.png","sheet_x":58,"sheet_y":16,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"26F9-1F3FC","non_qualified":null,"image":"26f9-1f3fc.png","sheet_x":58,"sheet_y":17,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"26F9-1F3FD","non_qualified":null,"image":"26f9-1f3fd.png","sheet_x":58,"sheet_y":18,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"26F9-1F3FE","non_qualified":null,"image":"26f9-1f3fe.png","sheet_x":58,"sheet_y":19,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"26F9-1F3FF","non_qualified":null,"image":"26f9-1f3ff.png","sheet_x":58,"sheet_y":20,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"26F9-FE0F-200D-2642-FE0F"},{"name":"TENT","unified":"26FA","non_qualified":null,"docomo":null,"au":"E5D0","softbank":"E122","google":"FE7FB","image":"26fa.png","sheet_x":58,"sheet_y":21,"short_name":"tent","short_names":["tent"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-other","sort_order":856,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FUEL PUMP","unified":"26FD","non_qualified":null,"docomo":"E66B","au":"E571","softbank":"E03A","google":"FE7F5","image":"26fd.png","sheet_x":58,"sheet_y":22,"short_name":"fuelpump","short_names":["fuelpump"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":915,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK SCISSORS","unified":"2702-FE0F","non_qualified":"2702","docomo":"E675","au":"E516","softbank":"E313","google":"FE53E","image":"2702-fe0f.png","sheet_x":58,"sheet_y":23,"short_name":"scissors","short_names":["scissors"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1284,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WHITE HEAVY CHECK MARK","unified":"2705","non_qualified":null,"docomo":null,"au":"E55E","softbank":null,"google":"FEB4A","image":"2705.png","sheet_x":58,"sheet_y":24,"short_name":"white_check_mark","short_names":["white_check_mark"],"text":null,"texts":null,"category":"Symbols","subcategory":"other-symbol","sort_order":1486,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"AIRPLANE","unified":"2708-FE0F","non_qualified":"2708","docomo":"E662","au":"E4B3","softbank":"E01D","google":"FE7E9","image":"2708-fe0f.png","sheet_x":58,"sheet_y":25,"short_name":"airplane","short_names":["airplane"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-air","sort_order":931,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ENVELOPE","unified":"2709-FE0F","non_qualified":"2709","docomo":"E6D3","au":"E521","softbank":null,"google":"FE529","image":"2709-fe0f.png","sheet_x":58,"sheet_y":26,"short_name":"email","short_names":["email","envelope"],"text":null,"texts":null,"category":"Objects","subcategory":"mail","sort_order":1245,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RAISED FIST","unified":"270A","non_qualified":null,"docomo":"E693","au":"EB83","softbank":"E010","google":"FEB93","image":"270a.png","sheet_x":58,"sheet_y":27,"short_name":"fist","short_names":["fist"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-closed","sort_order":191,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"270A-1F3FB","non_qualified":null,"image":"270a-1f3fb.png","sheet_x":58,"sheet_y":28,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"270A-1F3FC","non_qualified":null,"image":"270a-1f3fc.png","sheet_x":58,"sheet_y":29,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"270A-1F3FD","non_qualified":null,"image":"270a-1f3fd.png","sheet_x":58,"sheet_y":30,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"270A-1F3FE","non_qualified":null,"image":"270a-1f3fe.png","sheet_x":58,"sheet_y":31,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"270A-1F3FF","non_qualified":null,"image":"270a-1f3ff.png","sheet_x":58,"sheet_y":32,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"RAISED HAND","unified":"270B","non_qualified":null,"docomo":"E695","au":"E5A7","softbank":"E012","google":"FEB95","image":"270b.png","sheet_x":58,"sheet_y":33,"short_name":"hand","short_names":["hand","raised_hand"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-open","sort_order":167,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"270B-1F3FB","non_qualified":null,"image":"270b-1f3fb.png","sheet_x":58,"sheet_y":34,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"270B-1F3FC","non_qualified":null,"image":"270b-1f3fc.png","sheet_x":58,"sheet_y":35,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"270B-1F3FD","non_qualified":null,"image":"270b-1f3fd.png","sheet_x":58,"sheet_y":36,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"270B-1F3FE","non_qualified":null,"image":"270b-1f3fe.png","sheet_x":58,"sheet_y":37,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"270B-1F3FF","non_qualified":null,"image":"270b-1f3ff.png","sheet_x":58,"sheet_y":38,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"VICTORY HAND","unified":"270C-FE0F","non_qualified":"270C","docomo":"E694","au":"E5A6","softbank":"E011","google":"FEB94","image":"270c-fe0f.png","sheet_x":58,"sheet_y":39,"short_name":"v","short_names":["v"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-partial","sort_order":176,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"270C-1F3FB","non_qualified":null,"image":"270c-1f3fb.png","sheet_x":58,"sheet_y":40,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"270C-1F3FC","non_qualified":null,"image":"270c-1f3fc.png","sheet_x":58,"sheet_y":41,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"270C-1F3FD","non_qualified":null,"image":"270c-1f3fd.png","sheet_x":58,"sheet_y":42,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"270C-1F3FE","non_qualified":null,"image":"270c-1f3fe.png","sheet_x":58,"sheet_y":43,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"270C-1F3FF","non_qualified":null,"image":"270c-1f3ff.png","sheet_x":58,"sheet_y":44,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WRITING HAND","unified":"270D-FE0F","non_qualified":"270D","docomo":null,"au":null,"softbank":null,"google":null,"image":"270d-fe0f.png","sheet_x":58,"sheet_y":45,"short_name":"writing_hand","short_names":["writing_hand"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-prop","sort_order":202,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"270D-1F3FB","non_qualified":null,"image":"270d-1f3fb.png","sheet_x":58,"sheet_y":46,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"270D-1F3FC","non_qualified":null,"image":"270d-1f3fc.png","sheet_x":58,"sheet_y":47,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"270D-1F3FD","non_qualified":null,"image":"270d-1f3fd.png","sheet_x":58,"sheet_y":48,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"270D-1F3FE","non_qualified":null,"image":"270d-1f3fe.png","sheet_x":58,"sheet_y":49,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"270D-1F3FF","non_qualified":null,"image":"270d-1f3ff.png","sheet_x":58,"sheet_y":50,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"PENCIL","unified":"270F-FE0F","non_qualified":"270F","docomo":"E719","au":"E4A1","softbank":null,"google":"FE539","image":"270f-fe0f.png","sheet_x":58,"sheet_y":51,"short_name":"pencil2","short_names":["pencil2"],"text":null,"texts":null,"category":"Objects","subcategory":"writing","sort_order":1258,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK NIB","unified":"2712-FE0F","non_qualified":"2712","docomo":"E6AE","au":"EB03","softbank":null,"google":"FE536","image":"2712-fe0f.png","sheet_x":58,"sheet_y":52,"short_name":"black_nib","short_names":["black_nib"],"text":null,"texts":null,"category":"Objects","subcategory":"writing","sort_order":1259,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HEAVY CHECK MARK","unified":"2714-FE0F","non_qualified":"2714","docomo":null,"au":"E557","softbank":null,"google":"FEB49","image":"2714-fe0f.png","sheet_x":58,"sheet_y":53,"short_name":"heavy_check_mark","short_names":["heavy_check_mark"],"text":null,"texts":null,"category":"Symbols","subcategory":"other-symbol","sort_order":1488,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HEAVY MULTIPLICATION X","unified":"2716-FE0F","non_qualified":"2716","docomo":null,"au":"E54F","softbank":null,"google":"FEB53","image":"2716-fe0f.png","sheet_x":58,"sheet_y":54,"short_name":"heavy_multiplication_x","short_names":["heavy_multiplication_x"],"text":null,"texts":null,"category":"Symbols","subcategory":"math","sort_order":1464,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LATIN CROSS","unified":"271D-FE0F","non_qualified":"271D","docomo":null,"au":null,"softbank":null,"google":null,"image":"271d-fe0f.png","sheet_x":58,"sheet_y":55,"short_name":"latin_cross","short_names":["latin_cross"],"text":null,"texts":null,"category":"Symbols","subcategory":"religion","sort_order":1418,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"STAR OF DAVID","unified":"2721-FE0F","non_qualified":"2721","docomo":null,"au":null,"softbank":null,"google":null,"image":"2721-fe0f.png","sheet_x":58,"sheet_y":56,"short_name":"star_of_david","short_names":["star_of_david"],"text":null,"texts":null,"category":"Symbols","subcategory":"religion","sort_order":1415,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SPARKLES","unified":"2728","non_qualified":null,"docomo":"E6FA","au":"EAAB","softbank":"E32E","google":"FEB60","image":"2728.png","sheet_x":58,"sheet_y":57,"short_name":"sparkles","short_names":["sparkles"],"text":null,"texts":null,"category":"Activities","subcategory":"event","sort_order":1029,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EIGHT SPOKED ASTERISK","unified":"2733-FE0F","non_qualified":"2733","docomo":"E6F8","au":"E53E","softbank":"E206","google":"FEB62","image":"2733-fe0f.png","sheet_x":58,"sheet_y":58,"short_name":"eight_spoked_asterisk","short_names":["eight_spoked_asterisk"],"text":null,"texts":null,"category":"Symbols","subcategory":"other-symbol","sort_order":1494,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EIGHT POINTED BLACK STAR","unified":"2734-FE0F","non_qualified":"2734","docomo":"E6F8","au":"E479","softbank":"E205","google":"FEB61","image":"2734-fe0f.png","sheet_x":58,"sheet_y":59,"short_name":"eight_pointed_black_star","short_names":["eight_pointed_black_star"],"text":null,"texts":null,"category":"Symbols","subcategory":"other-symbol","sort_order":1495,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SNOWFLAKE","unified":"2744-FE0F","non_qualified":"2744","docomo":null,"au":"E48A","softbank":null,"google":"FE00E","image":"2744-fe0f.png","sheet_x":58,"sheet_y":60,"short_name":"snowflake","short_names":["snowflake"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1017,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SPARKLE","unified":"2747-FE0F","non_qualified":"2747","docomo":"E6FA","au":"E46C","softbank":null,"google":"FEB77","image":"2747-fe0f.png","sheet_x":59,"sheet_y":0,"short_name":"sparkle","short_names":["sparkle"],"text":null,"texts":null,"category":"Symbols","subcategory":"other-symbol","sort_order":1496,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CROSS MARK","unified":"274C","non_qualified":null,"docomo":null,"au":"E550","softbank":"E333","google":"FEB45","image":"274c.png","sheet_x":59,"sheet_y":1,"short_name":"x","short_names":["x"],"text":null,"texts":null,"category":"Symbols","subcategory":"other-symbol","sort_order":1489,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NEGATIVE SQUARED CROSS MARK","unified":"274E","non_qualified":null,"docomo":null,"au":"E551","softbank":null,"google":"FEB46","image":"274e.png","sheet_x":59,"sheet_y":2,"short_name":"negative_squared_cross_mark","short_names":["negative_squared_cross_mark"],"text":null,"texts":null,"category":"Symbols","subcategory":"other-symbol","sort_order":1490,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK QUESTION MARK ORNAMENT","unified":"2753","non_qualified":null,"docomo":null,"au":"E483","softbank":"E020","google":"FEB09","image":"2753.png","sheet_x":59,"sheet_y":3,"short_name":"question","short_names":["question"],"text":null,"texts":null,"category":"Symbols","subcategory":"punctuation","sort_order":1472,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WHITE QUESTION MARK ORNAMENT","unified":"2754","non_qualified":null,"docomo":null,"au":"E483","softbank":"E336","google":"FEB0A","image":"2754.png","sheet_x":59,"sheet_y":4,"short_name":"grey_question","short_names":["grey_question"],"text":null,"texts":null,"category":"Symbols","subcategory":"punctuation","sort_order":1473,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WHITE EXCLAMATION MARK ORNAMENT","unified":"2755","non_qualified":null,"docomo":"E702","au":"E482","softbank":"E337","google":"FEB0B","image":"2755.png","sheet_x":59,"sheet_y":5,"short_name":"grey_exclamation","short_names":["grey_exclamation"],"text":null,"texts":null,"category":"Symbols","subcategory":"punctuation","sort_order":1474,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HEAVY EXCLAMATION MARK SYMBOL","unified":"2757","non_qualified":null,"docomo":"E702","au":"E482","softbank":"E021","google":"FEB04","image":"2757.png","sheet_x":59,"sheet_y":6,"short_name":"exclamation","short_names":["exclamation","heavy_exclamation_mark"],"text":null,"texts":null,"category":"Symbols","subcategory":"punctuation","sort_order":1475,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HEART EXCLAMATION","unified":"2763-FE0F","non_qualified":"2763","docomo":null,"au":null,"softbank":null,"google":null,"image":"2763-fe0f.png","sheet_x":59,"sheet_y":7,"short_name":"heavy_heart_exclamation_mark_ornament","short_names":["heavy_heart_exclamation_mark_ornament"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":137,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HEART ON FIRE","unified":"2764-FE0F-200D-1F525","non_qualified":"2764-200D-1F525","docomo":null,"au":null,"softbank":null,"google":null,"image":"2764-fe0f-200d-1f525.png","sheet_x":59,"sheet_y":8,"short_name":"heart_on_fire","short_names":["heart_on_fire"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":139,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"MENDING HEART","unified":"2764-FE0F-200D-1FA79","non_qualified":"2764-200D-1FA79","docomo":null,"au":null,"softbank":null,"google":null,"image":"2764-fe0f-200d-1fa79.png","sheet_x":59,"sheet_y":9,"short_name":"mending_heart","short_names":["mending_heart"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":140,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"HEAVY BLACK HEART","unified":"2764-FE0F","non_qualified":"2764","docomo":"E6EC","au":"E595","softbank":"E022","google":"FEB0C","image":"2764-fe0f.png","sheet_x":59,"sheet_y":10,"short_name":"heart","short_names":["heart"],"text":"<3","texts":["<3"],"category":"Smileys & Emotion","subcategory":"emotion","sort_order":141,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HEAVY PLUS SIGN","unified":"2795","non_qualified":null,"docomo":null,"au":"E53C","softbank":null,"google":"FEB51","image":"2795.png","sheet_x":59,"sheet_y":11,"short_name":"heavy_plus_sign","short_names":["heavy_plus_sign"],"text":null,"texts":null,"category":"Symbols","subcategory":"math","sort_order":1465,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HEAVY MINUS SIGN","unified":"2796","non_qualified":null,"docomo":null,"au":"E53D","softbank":null,"google":"FEB52","image":"2796.png","sheet_x":59,"sheet_y":12,"short_name":"heavy_minus_sign","short_names":["heavy_minus_sign"],"text":null,"texts":null,"category":"Symbols","subcategory":"math","sort_order":1466,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HEAVY DIVISION SIGN","unified":"2797","non_qualified":null,"docomo":null,"au":"E554","softbank":null,"google":"FEB54","image":"2797.png","sheet_x":59,"sheet_y":13,"short_name":"heavy_division_sign","short_names":["heavy_division_sign"],"text":null,"texts":null,"category":"Symbols","subcategory":"math","sort_order":1467,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK RIGHTWARDS ARROW","unified":"27A1-FE0F","non_qualified":"27A1","docomo":null,"au":"E552","softbank":"E234","google":"FEAFA","image":"27a1-fe0f.png","sheet_x":59,"sheet_y":14,"short_name":"arrow_right","short_names":["arrow_right"],"text":null,"texts":null,"category":"Symbols","subcategory":"arrow","sort_order":1393,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CURLY LOOP","unified":"27B0","non_qualified":null,"docomo":"E70A","au":"EB31","softbank":null,"google":"FEB08","image":"27b0.png","sheet_x":59,"sheet_y":15,"short_name":"curly_loop","short_names":["curly_loop"],"text":null,"texts":null,"category":"Symbols","subcategory":"other-symbol","sort_order":1491,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DOUBLE CURLY LOOP","unified":"27BF","non_qualified":null,"docomo":"E6DF","au":null,"softbank":"E211","google":"FE82B","image":"27bf.png","sheet_x":59,"sheet_y":16,"short_name":"loop","short_names":["loop"],"text":null,"texts":null,"category":"Symbols","subcategory":"other-symbol","sort_order":1492,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ARROW POINTING RIGHTWARDS THEN CURVING UPWARDS","unified":"2934-FE0F","non_qualified":"2934","docomo":"E6F5","au":"EB2D","softbank":null,"google":"FEAF4","image":"2934-fe0f.png","sheet_x":59,"sheet_y":17,"short_name":"arrow_heading_up","short_names":["arrow_heading_up"],"text":null,"texts":null,"category":"Symbols","subcategory":"arrow","sort_order":1403,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ARROW POINTING RIGHTWARDS THEN CURVING DOWNWARDS","unified":"2935-FE0F","non_qualified":"2935","docomo":"E700","au":"EB2E","softbank":null,"google":"FEAF5","image":"2935-fe0f.png","sheet_x":59,"sheet_y":18,"short_name":"arrow_heading_down","short_names":["arrow_heading_down"],"text":null,"texts":null,"category":"Symbols","subcategory":"arrow","sort_order":1404,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LEFTWARDS BLACK ARROW","unified":"2B05-FE0F","non_qualified":"2B05","docomo":null,"au":"E553","softbank":"E235","google":"FEAFB","image":"2b05-fe0f.png","sheet_x":59,"sheet_y":19,"short_name":"arrow_left","short_names":["arrow_left"],"text":null,"texts":null,"category":"Symbols","subcategory":"arrow","sort_order":1397,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"UPWARDS BLACK ARROW","unified":"2B06-FE0F","non_qualified":"2B06","docomo":null,"au":"E53F","softbank":"E232","google":"FEAF8","image":"2b06-fe0f.png","sheet_x":59,"sheet_y":20,"short_name":"arrow_up","short_names":["arrow_up"],"text":null,"texts":null,"category":"Symbols","subcategory":"arrow","sort_order":1391,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DOWNWARDS BLACK ARROW","unified":"2B07-FE0F","non_qualified":"2B07","docomo":null,"au":"E540","softbank":"E233","google":"FEAF9","image":"2b07-fe0f.png","sheet_x":59,"sheet_y":21,"short_name":"arrow_down","short_names":["arrow_down"],"text":null,"texts":null,"category":"Symbols","subcategory":"arrow","sort_order":1395,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK LARGE SQUARE","unified":"2B1B","non_qualified":null,"docomo":null,"au":"E549","softbank":null,"google":"FEB6C","image":"2b1b.png","sheet_x":59,"sheet_y":22,"short_name":"black_large_square","short_names":["black_large_square"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1568,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WHITE LARGE SQUARE","unified":"2B1C","non_qualified":null,"docomo":null,"au":"E548","softbank":null,"google":"FEB6B","image":"2b1c.png","sheet_x":59,"sheet_y":23,"short_name":"white_large_square","short_names":["white_large_square"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1569,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WHITE MEDIUM STAR","unified":"2B50","non_qualified":null,"docomo":null,"au":"E48B","softbank":"E32F","google":"FEB68","image":"2b50.png","sheet_x":59,"sheet_y":24,"short_name":"star","short_names":["star"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":994,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HEAVY LARGE CIRCLE","unified":"2B55","non_qualified":null,"docomo":"E6A0","au":"EAAD","softbank":"E332","google":"FEB44","image":"2b55.png","sheet_x":59,"sheet_y":25,"short_name":"o","short_names":["o"],"text":null,"texts":null,"category":"Symbols","subcategory":"other-symbol","sort_order":1485,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WAVY DASH","unified":"3030-FE0F","non_qualified":"3030","docomo":"E709","au":null,"softbank":null,"google":"FEB07","image":"3030-fe0f.png","sheet_x":59,"sheet_y":26,"short_name":"wavy_dash","short_names":["wavy_dash"],"text":null,"texts":null,"category":"Symbols","subcategory":"punctuation","sort_order":1476,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PART ALTERNATION MARK","unified":"303D-FE0F","non_qualified":"303D","docomo":null,"au":null,"softbank":"E12C","google":"FE81B","image":"303d-fe0f.png","sheet_x":59,"sheet_y":27,"short_name":"part_alternation_mark","short_names":["part_alternation_mark"],"text":null,"texts":null,"category":"Symbols","subcategory":"other-symbol","sort_order":1493,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CIRCLED IDEOGRAPH CONGRATULATION","unified":"3297-FE0F","non_qualified":"3297","docomo":null,"au":"EA99","softbank":"E30D","google":"FEB43","image":"3297-fe0f.png","sheet_x":59,"sheet_y":28,"short_name":"congratulations","short_names":["congratulations"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1548,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CIRCLED IDEOGRAPH SECRET","unified":"3299-FE0F","non_qualified":"3299","docomo":"E734","au":"E4F1","softbank":"E315","google":"FEB2B","image":"3299-fe0f.png","sheet_x":59,"sheet_y":29,"short_name":"secret","short_names":["secret"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1549,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}] \ No newline at end of file +[{"name":"HASH KEY","unified":"0023-FE0F-20E3","non_qualified":"0023-20E3","docomo":"E6E0","au":"EB84","softbank":"E210","google":"FE82C","image":"0023-fe0f-20e3.png","sheet_x":0,"sheet_y":0,"short_name":"hash","short_names":["hash"],"text":null,"texts":null,"category":"Symbols","subcategory":"keycap","sort_order":1549,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"KEYCAP: *","unified":"002A-FE0F-20E3","non_qualified":"002A-20E3","docomo":null,"au":null,"softbank":null,"google":null,"image":"002a-fe0f-20e3.png","sheet_x":0,"sheet_y":1,"short_name":"keycap_star","short_names":["keycap_star"],"text":null,"texts":null,"category":"Symbols","subcategory":"keycap","sort_order":1550,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"KEYCAP 0","unified":"0030-FE0F-20E3","non_qualified":"0030-20E3","docomo":"E6EB","au":"E5AC","softbank":"E225","google":"FE837","image":"0030-fe0f-20e3.png","sheet_x":0,"sheet_y":2,"short_name":"zero","short_names":["zero"],"text":null,"texts":null,"category":"Symbols","subcategory":"keycap","sort_order":1551,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"KEYCAP 1","unified":"0031-FE0F-20E3","non_qualified":"0031-20E3","docomo":"E6E2","au":"E522","softbank":"E21C","google":"FE82E","image":"0031-fe0f-20e3.png","sheet_x":0,"sheet_y":3,"short_name":"one","short_names":["one"],"text":null,"texts":null,"category":"Symbols","subcategory":"keycap","sort_order":1552,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"KEYCAP 2","unified":"0032-FE0F-20E3","non_qualified":"0032-20E3","docomo":"E6E3","au":"E523","softbank":"E21D","google":"FE82F","image":"0032-fe0f-20e3.png","sheet_x":0,"sheet_y":4,"short_name":"two","short_names":["two"],"text":null,"texts":null,"category":"Symbols","subcategory":"keycap","sort_order":1553,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"KEYCAP 3","unified":"0033-FE0F-20E3","non_qualified":"0033-20E3","docomo":"E6E4","au":"E524","softbank":"E21E","google":"FE830","image":"0033-fe0f-20e3.png","sheet_x":0,"sheet_y":5,"short_name":"three","short_names":["three"],"text":null,"texts":null,"category":"Symbols","subcategory":"keycap","sort_order":1554,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"KEYCAP 4","unified":"0034-FE0F-20E3","non_qualified":"0034-20E3","docomo":"E6E5","au":"E525","softbank":"E21F","google":"FE831","image":"0034-fe0f-20e3.png","sheet_x":0,"sheet_y":6,"short_name":"four","short_names":["four"],"text":null,"texts":null,"category":"Symbols","subcategory":"keycap","sort_order":1555,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"KEYCAP 5","unified":"0035-FE0F-20E3","non_qualified":"0035-20E3","docomo":"E6E6","au":"E526","softbank":"E220","google":"FE832","image":"0035-fe0f-20e3.png","sheet_x":0,"sheet_y":7,"short_name":"five","short_names":["five"],"text":null,"texts":null,"category":"Symbols","subcategory":"keycap","sort_order":1556,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"KEYCAP 6","unified":"0036-FE0F-20E3","non_qualified":"0036-20E3","docomo":"E6E7","au":"E527","softbank":"E221","google":"FE833","image":"0036-fe0f-20e3.png","sheet_x":0,"sheet_y":8,"short_name":"six","short_names":["six"],"text":null,"texts":null,"category":"Symbols","subcategory":"keycap","sort_order":1557,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"KEYCAP 7","unified":"0037-FE0F-20E3","non_qualified":"0037-20E3","docomo":"E6E8","au":"E528","softbank":"E222","google":"FE834","image":"0037-fe0f-20e3.png","sheet_x":0,"sheet_y":9,"short_name":"seven","short_names":["seven"],"text":null,"texts":null,"category":"Symbols","subcategory":"keycap","sort_order":1558,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"KEYCAP 8","unified":"0038-FE0F-20E3","non_qualified":"0038-20E3","docomo":"E6E9","au":"E529","softbank":"E223","google":"FE835","image":"0038-fe0f-20e3.png","sheet_x":0,"sheet_y":10,"short_name":"eight","short_names":["eight"],"text":null,"texts":null,"category":"Symbols","subcategory":"keycap","sort_order":1559,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"KEYCAP 9","unified":"0039-FE0F-20E3","non_qualified":"0039-20E3","docomo":"E6EA","au":"E52A","softbank":"E224","google":"FE836","image":"0039-fe0f-20e3.png","sheet_x":0,"sheet_y":11,"short_name":"nine","short_names":["nine"],"text":null,"texts":null,"category":"Symbols","subcategory":"keycap","sort_order":1560,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"COPYRIGHT SIGN","unified":"00A9-FE0F","non_qualified":"00A9","docomo":"E731","au":"E558","softbank":"E24E","google":"FEB29","image":"00a9-fe0f.png","sheet_x":0,"sheet_y":12,"short_name":"copyright","short_names":["copyright"],"text":null,"texts":null,"category":"Symbols","subcategory":"other-symbol","sort_order":1546,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"REGISTERED SIGN","unified":"00AE-FE0F","non_qualified":"00AE","docomo":"E736","au":"E559","softbank":"E24F","google":"FEB2D","image":"00ae-fe0f.png","sheet_x":0,"sheet_y":13,"short_name":"registered","short_names":["registered"],"text":null,"texts":null,"category":"Symbols","subcategory":"other-symbol","sort_order":1547,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"MAHJONG TILE RED DRAGON","unified":"1F004","non_qualified":null,"docomo":null,"au":"E5D1","softbank":"E12D","google":"FE80B","image":"1f004.png","sheet_x":0,"sheet_y":14,"short_name":"mahjong","short_names":["mahjong"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1141,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PLAYING CARD BLACK JOKER","unified":"1F0CF","non_qualified":null,"docomo":null,"au":"EB6F","softbank":null,"google":"FE812","image":"1f0cf.png","sheet_x":0,"sheet_y":15,"short_name":"black_joker","short_names":["black_joker"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1140,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NEGATIVE SQUARED LATIN CAPITAL LETTER A","unified":"1F170-FE0F","non_qualified":"1F170","docomo":null,"au":"EB26","softbank":"E532","google":"FE50B","image":"1f170-fe0f.png","sheet_x":0,"sheet_y":16,"short_name":"a","short_names":["a"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1567,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NEGATIVE SQUARED LATIN CAPITAL LETTER B","unified":"1F171-FE0F","non_qualified":"1F171","docomo":null,"au":"EB27","softbank":"E533","google":"FE50C","image":"1f171-fe0f.png","sheet_x":0,"sheet_y":17,"short_name":"b","short_names":["b"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1569,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NEGATIVE SQUARED LATIN CAPITAL LETTER O","unified":"1F17E-FE0F","non_qualified":"1F17E","docomo":null,"au":"EB28","softbank":"E535","google":"FE50E","image":"1f17e-fe0f.png","sheet_x":0,"sheet_y":18,"short_name":"o2","short_names":["o2"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1578,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NEGATIVE SQUARED LATIN CAPITAL LETTER P","unified":"1F17F-FE0F","non_qualified":"1F17F","docomo":"E66C","au":"E4A6","softbank":"E14F","google":"FE7F6","image":"1f17f-fe0f.png","sheet_x":0,"sheet_y":19,"short_name":"parking","short_names":["parking"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1580,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NEGATIVE SQUARED AB","unified":"1F18E","non_qualified":null,"docomo":null,"au":"EB29","softbank":"E534","google":"FE50D","image":"1f18e.png","sheet_x":0,"sheet_y":20,"short_name":"ab","short_names":["ab"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1568,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED CL","unified":"1F191","non_qualified":null,"docomo":"E6DB","au":"E5AB","softbank":null,"google":"FEB84","image":"1f191.png","sheet_x":0,"sheet_y":21,"short_name":"cl","short_names":["cl"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1570,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED COOL","unified":"1F192","non_qualified":null,"docomo":null,"au":"EA85","softbank":"E214","google":"FEB38","image":"1f192.png","sheet_x":0,"sheet_y":22,"short_name":"cool","short_names":["cool"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1571,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED FREE","unified":"1F193","non_qualified":null,"docomo":"E6D7","au":"E578","softbank":null,"google":"FEB21","image":"1f193.png","sheet_x":0,"sheet_y":23,"short_name":"free","short_names":["free"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1572,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED ID","unified":"1F194","non_qualified":null,"docomo":"E6D8","au":"EA88","softbank":"E229","google":"FEB81","image":"1f194.png","sheet_x":0,"sheet_y":24,"short_name":"id","short_names":["id"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1574,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED NEW","unified":"1F195","non_qualified":null,"docomo":"E6DD","au":"E5B5","softbank":"E212","google":"FEB36","image":"1f195.png","sheet_x":0,"sheet_y":25,"short_name":"new","short_names":["new"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1576,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED NG","unified":"1F196","non_qualified":null,"docomo":"E72F","au":null,"softbank":null,"google":"FEB28","image":"1f196.png","sheet_x":0,"sheet_y":26,"short_name":"ng","short_names":["ng"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1577,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED OK","unified":"1F197","non_qualified":null,"docomo":"E70B","au":"E5AD","softbank":"E24D","google":"FEB27","image":"1f197.png","sheet_x":0,"sheet_y":27,"short_name":"ok","short_names":["ok"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1579,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED SOS","unified":"1F198","non_qualified":null,"docomo":null,"au":"E4E8","softbank":null,"google":"FEB4F","image":"1f198.png","sheet_x":0,"sheet_y":28,"short_name":"sos","short_names":["sos"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1581,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED UP WITH EXCLAMATION MARK","unified":"1F199","non_qualified":null,"docomo":null,"au":"E50F","softbank":"E213","google":"FEB37","image":"1f199.png","sheet_x":0,"sheet_y":29,"short_name":"up","short_names":["up"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1582,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED VS","unified":"1F19A","non_qualified":null,"docomo":null,"au":"E5D2","softbank":"E12E","google":"FEB32","image":"1f19a.png","sheet_x":0,"sheet_y":30,"short_name":"vs","short_names":["vs"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1583,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Ascension Island Flag","unified":"1F1E6-1F1E8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e6-1f1e8.png","sheet_x":0,"sheet_y":31,"short_name":"flag-ac","short_names":["flag-ac"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1643,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Andorra Flag","unified":"1F1E6-1F1E9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e6-1f1e9.png","sheet_x":0,"sheet_y":32,"short_name":"flag-ad","short_names":["flag-ad"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1644,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"United Arab Emirates Flag","unified":"1F1E6-1F1EA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e6-1f1ea.png","sheet_x":0,"sheet_y":33,"short_name":"flag-ae","short_names":["flag-ae"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1645,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Afghanistan Flag","unified":"1F1E6-1F1EB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e6-1f1eb.png","sheet_x":0,"sheet_y":34,"short_name":"flag-af","short_names":["flag-af"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1646,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Antigua & Barbuda Flag","unified":"1F1E6-1F1EC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e6-1f1ec.png","sheet_x":0,"sheet_y":35,"short_name":"flag-ag","short_names":["flag-ag"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1647,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Anguilla Flag","unified":"1F1E6-1F1EE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e6-1f1ee.png","sheet_x":0,"sheet_y":36,"short_name":"flag-ai","short_names":["flag-ai"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1648,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Albania Flag","unified":"1F1E6-1F1F1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e6-1f1f1.png","sheet_x":0,"sheet_y":37,"short_name":"flag-al","short_names":["flag-al"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1649,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Armenia Flag","unified":"1F1E6-1F1F2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e6-1f1f2.png","sheet_x":0,"sheet_y":38,"short_name":"flag-am","short_names":["flag-am"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1650,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Angola Flag","unified":"1F1E6-1F1F4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e6-1f1f4.png","sheet_x":0,"sheet_y":39,"short_name":"flag-ao","short_names":["flag-ao"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1651,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Antarctica Flag","unified":"1F1E6-1F1F6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e6-1f1f6.png","sheet_x":0,"sheet_y":40,"short_name":"flag-aq","short_names":["flag-aq"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1652,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Argentina Flag","unified":"1F1E6-1F1F7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e6-1f1f7.png","sheet_x":0,"sheet_y":41,"short_name":"flag-ar","short_names":["flag-ar"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1653,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"American Samoa Flag","unified":"1F1E6-1F1F8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e6-1f1f8.png","sheet_x":0,"sheet_y":42,"short_name":"flag-as","short_names":["flag-as"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1654,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Austria Flag","unified":"1F1E6-1F1F9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e6-1f1f9.png","sheet_x":0,"sheet_y":43,"short_name":"flag-at","short_names":["flag-at"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1655,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Australia Flag","unified":"1F1E6-1F1FA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e6-1f1fa.png","sheet_x":0,"sheet_y":44,"short_name":"flag-au","short_names":["flag-au"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1656,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Aruba Flag","unified":"1F1E6-1F1FC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e6-1f1fc.png","sheet_x":0,"sheet_y":45,"short_name":"flag-aw","short_names":["flag-aw"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1657,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"\u00c5land Islands Flag","unified":"1F1E6-1F1FD","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e6-1f1fd.png","sheet_x":0,"sheet_y":46,"short_name":"flag-ax","short_names":["flag-ax"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1658,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Azerbaijan Flag","unified":"1F1E6-1F1FF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e6-1f1ff.png","sheet_x":0,"sheet_y":47,"short_name":"flag-az","short_names":["flag-az"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1659,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Bosnia & Herzegovina Flag","unified":"1F1E7-1F1E6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e7-1f1e6.png","sheet_x":0,"sheet_y":48,"short_name":"flag-ba","short_names":["flag-ba"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1660,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Barbados Flag","unified":"1F1E7-1F1E7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e7-1f1e7.png","sheet_x":0,"sheet_y":49,"short_name":"flag-bb","short_names":["flag-bb"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1661,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Bangladesh Flag","unified":"1F1E7-1F1E9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e7-1f1e9.png","sheet_x":0,"sheet_y":50,"short_name":"flag-bd","short_names":["flag-bd"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1662,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Belgium Flag","unified":"1F1E7-1F1EA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e7-1f1ea.png","sheet_x":0,"sheet_y":51,"short_name":"flag-be","short_names":["flag-be"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1663,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Burkina Faso Flag","unified":"1F1E7-1F1EB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e7-1f1eb.png","sheet_x":0,"sheet_y":52,"short_name":"flag-bf","short_names":["flag-bf"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1664,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Bulgaria Flag","unified":"1F1E7-1F1EC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e7-1f1ec.png","sheet_x":0,"sheet_y":53,"short_name":"flag-bg","short_names":["flag-bg"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1665,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Bahrain Flag","unified":"1F1E7-1F1ED","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e7-1f1ed.png","sheet_x":0,"sheet_y":54,"short_name":"flag-bh","short_names":["flag-bh"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1666,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Burundi Flag","unified":"1F1E7-1F1EE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e7-1f1ee.png","sheet_x":0,"sheet_y":55,"short_name":"flag-bi","short_names":["flag-bi"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1667,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Benin Flag","unified":"1F1E7-1F1EF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e7-1f1ef.png","sheet_x":0,"sheet_y":56,"short_name":"flag-bj","short_names":["flag-bj"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1668,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"St. Barth\u00e9lemy Flag","unified":"1F1E7-1F1F1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e7-1f1f1.png","sheet_x":0,"sheet_y":57,"short_name":"flag-bl","short_names":["flag-bl"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1669,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Bermuda Flag","unified":"1F1E7-1F1F2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e7-1f1f2.png","sheet_x":0,"sheet_y":58,"short_name":"flag-bm","short_names":["flag-bm"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1670,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Brunei Flag","unified":"1F1E7-1F1F3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e7-1f1f3.png","sheet_x":0,"sheet_y":59,"short_name":"flag-bn","short_names":["flag-bn"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1671,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Bolivia Flag","unified":"1F1E7-1F1F4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e7-1f1f4.png","sheet_x":0,"sheet_y":60,"short_name":"flag-bo","short_names":["flag-bo"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1672,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Caribbean Netherlands Flag","unified":"1F1E7-1F1F6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e7-1f1f6.png","sheet_x":0,"sheet_y":61,"short_name":"flag-bq","short_names":["flag-bq"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1673,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Brazil Flag","unified":"1F1E7-1F1F7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e7-1f1f7.png","sheet_x":1,"sheet_y":0,"short_name":"flag-br","short_names":["flag-br"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1674,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Bahamas Flag","unified":"1F1E7-1F1F8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e7-1f1f8.png","sheet_x":1,"sheet_y":1,"short_name":"flag-bs","short_names":["flag-bs"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1675,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Bhutan Flag","unified":"1F1E7-1F1F9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e7-1f1f9.png","sheet_x":1,"sheet_y":2,"short_name":"flag-bt","short_names":["flag-bt"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1676,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Bouvet Island Flag","unified":"1F1E7-1F1FB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e7-1f1fb.png","sheet_x":1,"sheet_y":3,"short_name":"flag-bv","short_names":["flag-bv"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1677,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Botswana Flag","unified":"1F1E7-1F1FC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e7-1f1fc.png","sheet_x":1,"sheet_y":4,"short_name":"flag-bw","short_names":["flag-bw"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1678,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Belarus Flag","unified":"1F1E7-1F1FE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e7-1f1fe.png","sheet_x":1,"sheet_y":5,"short_name":"flag-by","short_names":["flag-by"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1679,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Belize Flag","unified":"1F1E7-1F1FF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e7-1f1ff.png","sheet_x":1,"sheet_y":6,"short_name":"flag-bz","short_names":["flag-bz"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1680,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Canada Flag","unified":"1F1E8-1F1E6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e8-1f1e6.png","sheet_x":1,"sheet_y":7,"short_name":"flag-ca","short_names":["flag-ca"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1681,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Cocos (Keeling) Islands Flag","unified":"1F1E8-1F1E8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e8-1f1e8.png","sheet_x":1,"sheet_y":8,"short_name":"flag-cc","short_names":["flag-cc"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1682,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Congo - Kinshasa Flag","unified":"1F1E8-1F1E9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e8-1f1e9.png","sheet_x":1,"sheet_y":9,"short_name":"flag-cd","short_names":["flag-cd"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1683,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Central African Republic Flag","unified":"1F1E8-1F1EB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e8-1f1eb.png","sheet_x":1,"sheet_y":10,"short_name":"flag-cf","short_names":["flag-cf"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1684,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Congo - Brazzaville Flag","unified":"1F1E8-1F1EC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e8-1f1ec.png","sheet_x":1,"sheet_y":11,"short_name":"flag-cg","short_names":["flag-cg"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1685,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Switzerland Flag","unified":"1F1E8-1F1ED","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e8-1f1ed.png","sheet_x":1,"sheet_y":12,"short_name":"flag-ch","short_names":["flag-ch"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1686,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"C\u00f4te d\u2019Ivoire Flag","unified":"1F1E8-1F1EE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e8-1f1ee.png","sheet_x":1,"sheet_y":13,"short_name":"flag-ci","short_names":["flag-ci"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1687,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Cook Islands Flag","unified":"1F1E8-1F1F0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e8-1f1f0.png","sheet_x":1,"sheet_y":14,"short_name":"flag-ck","short_names":["flag-ck"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1688,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Chile Flag","unified":"1F1E8-1F1F1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e8-1f1f1.png","sheet_x":1,"sheet_y":15,"short_name":"flag-cl","short_names":["flag-cl"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1689,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Cameroon Flag","unified":"1F1E8-1F1F2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e8-1f1f2.png","sheet_x":1,"sheet_y":16,"short_name":"flag-cm","short_names":["flag-cm"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1690,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"China Flag","unified":"1F1E8-1F1F3","non_qualified":null,"docomo":null,"au":"EB11","softbank":"E513","google":"FE4ED","image":"1f1e8-1f1f3.png","sheet_x":1,"sheet_y":17,"short_name":"cn","short_names":["cn","flag-cn"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1691,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Colombia Flag","unified":"1F1E8-1F1F4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e8-1f1f4.png","sheet_x":1,"sheet_y":18,"short_name":"flag-co","short_names":["flag-co"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1692,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Clipperton Island Flag","unified":"1F1E8-1F1F5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e8-1f1f5.png","sheet_x":1,"sheet_y":19,"short_name":"flag-cp","short_names":["flag-cp"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1693,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Costa Rica Flag","unified":"1F1E8-1F1F7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e8-1f1f7.png","sheet_x":1,"sheet_y":20,"short_name":"flag-cr","short_names":["flag-cr"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1694,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Cuba Flag","unified":"1F1E8-1F1FA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e8-1f1fa.png","sheet_x":1,"sheet_y":21,"short_name":"flag-cu","short_names":["flag-cu"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1695,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Cape Verde Flag","unified":"1F1E8-1F1FB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e8-1f1fb.png","sheet_x":1,"sheet_y":22,"short_name":"flag-cv","short_names":["flag-cv"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1696,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Cura\u00e7ao Flag","unified":"1F1E8-1F1FC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e8-1f1fc.png","sheet_x":1,"sheet_y":23,"short_name":"flag-cw","short_names":["flag-cw"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1697,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Christmas Island Flag","unified":"1F1E8-1F1FD","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e8-1f1fd.png","sheet_x":1,"sheet_y":24,"short_name":"flag-cx","short_names":["flag-cx"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1698,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Cyprus Flag","unified":"1F1E8-1F1FE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e8-1f1fe.png","sheet_x":1,"sheet_y":25,"short_name":"flag-cy","short_names":["flag-cy"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1699,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Czechia Flag","unified":"1F1E8-1F1FF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e8-1f1ff.png","sheet_x":1,"sheet_y":26,"short_name":"flag-cz","short_names":["flag-cz"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1700,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Germany Flag","unified":"1F1E9-1F1EA","non_qualified":null,"docomo":null,"au":"EB0E","softbank":"E50E","google":"FE4E8","image":"1f1e9-1f1ea.png","sheet_x":1,"sheet_y":27,"short_name":"de","short_names":["de","flag-de"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1701,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Diego Garcia Flag","unified":"1F1E9-1F1EC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e9-1f1ec.png","sheet_x":1,"sheet_y":28,"short_name":"flag-dg","short_names":["flag-dg"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1702,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Djibouti Flag","unified":"1F1E9-1F1EF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e9-1f1ef.png","sheet_x":1,"sheet_y":29,"short_name":"flag-dj","short_names":["flag-dj"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1703,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Denmark Flag","unified":"1F1E9-1F1F0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e9-1f1f0.png","sheet_x":1,"sheet_y":30,"short_name":"flag-dk","short_names":["flag-dk"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1704,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Dominica Flag","unified":"1F1E9-1F1F2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e9-1f1f2.png","sheet_x":1,"sheet_y":31,"short_name":"flag-dm","short_names":["flag-dm"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1705,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Dominican Republic Flag","unified":"1F1E9-1F1F4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e9-1f1f4.png","sheet_x":1,"sheet_y":32,"short_name":"flag-do","short_names":["flag-do"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1706,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Algeria Flag","unified":"1F1E9-1F1FF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1e9-1f1ff.png","sheet_x":1,"sheet_y":33,"short_name":"flag-dz","short_names":["flag-dz"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1707,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Ceuta & Melilla Flag","unified":"1F1EA-1F1E6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ea-1f1e6.png","sheet_x":1,"sheet_y":34,"short_name":"flag-ea","short_names":["flag-ea"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1708,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Ecuador Flag","unified":"1F1EA-1F1E8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ea-1f1e8.png","sheet_x":1,"sheet_y":35,"short_name":"flag-ec","short_names":["flag-ec"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1709,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Estonia Flag","unified":"1F1EA-1F1EA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ea-1f1ea.png","sheet_x":1,"sheet_y":36,"short_name":"flag-ee","short_names":["flag-ee"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1710,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Egypt Flag","unified":"1F1EA-1F1EC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ea-1f1ec.png","sheet_x":1,"sheet_y":37,"short_name":"flag-eg","short_names":["flag-eg"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1711,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Western Sahara Flag","unified":"1F1EA-1F1ED","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ea-1f1ed.png","sheet_x":1,"sheet_y":38,"short_name":"flag-eh","short_names":["flag-eh"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1712,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Eritrea Flag","unified":"1F1EA-1F1F7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ea-1f1f7.png","sheet_x":1,"sheet_y":39,"short_name":"flag-er","short_names":["flag-er"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1713,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Spain Flag","unified":"1F1EA-1F1F8","non_qualified":null,"docomo":null,"au":"E5D5","softbank":"E511","google":"FE4EB","image":"1f1ea-1f1f8.png","sheet_x":1,"sheet_y":40,"short_name":"es","short_names":["es","flag-es"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1714,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Ethiopia Flag","unified":"1F1EA-1F1F9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ea-1f1f9.png","sheet_x":1,"sheet_y":41,"short_name":"flag-et","short_names":["flag-et"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1715,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"European Union Flag","unified":"1F1EA-1F1FA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ea-1f1fa.png","sheet_x":1,"sheet_y":42,"short_name":"flag-eu","short_names":["flag-eu"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1716,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Finland Flag","unified":"1F1EB-1F1EE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1eb-1f1ee.png","sheet_x":1,"sheet_y":43,"short_name":"flag-fi","short_names":["flag-fi"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1717,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Fiji Flag","unified":"1F1EB-1F1EF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1eb-1f1ef.png","sheet_x":1,"sheet_y":44,"short_name":"flag-fj","short_names":["flag-fj"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1718,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Falkland Islands Flag","unified":"1F1EB-1F1F0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1eb-1f1f0.png","sheet_x":1,"sheet_y":45,"short_name":"flag-fk","short_names":["flag-fk"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1719,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Micronesia Flag","unified":"1F1EB-1F1F2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1eb-1f1f2.png","sheet_x":1,"sheet_y":46,"short_name":"flag-fm","short_names":["flag-fm"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1720,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Faroe Islands Flag","unified":"1F1EB-1F1F4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1eb-1f1f4.png","sheet_x":1,"sheet_y":47,"short_name":"flag-fo","short_names":["flag-fo"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1721,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"France Flag","unified":"1F1EB-1F1F7","non_qualified":null,"docomo":null,"au":"EAFA","softbank":"E50D","google":"FE4E7","image":"1f1eb-1f1f7.png","sheet_x":1,"sheet_y":48,"short_name":"fr","short_names":["fr","flag-fr"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1722,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Gabon Flag","unified":"1F1EC-1F1E6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ec-1f1e6.png","sheet_x":1,"sheet_y":49,"short_name":"flag-ga","short_names":["flag-ga"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1723,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"United Kingdom Flag","unified":"1F1EC-1F1E7","non_qualified":null,"docomo":null,"au":"EB10","softbank":"E510","google":"FE4EA","image":"1f1ec-1f1e7.png","sheet_x":1,"sheet_y":50,"short_name":"gb","short_names":["gb","uk","flag-gb"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1724,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Grenada Flag","unified":"1F1EC-1F1E9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ec-1f1e9.png","sheet_x":1,"sheet_y":51,"short_name":"flag-gd","short_names":["flag-gd"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1725,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Georgia Flag","unified":"1F1EC-1F1EA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ec-1f1ea.png","sheet_x":1,"sheet_y":52,"short_name":"flag-ge","short_names":["flag-ge"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1726,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"French Guiana Flag","unified":"1F1EC-1F1EB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ec-1f1eb.png","sheet_x":1,"sheet_y":53,"short_name":"flag-gf","short_names":["flag-gf"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1727,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Guernsey Flag","unified":"1F1EC-1F1EC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ec-1f1ec.png","sheet_x":1,"sheet_y":54,"short_name":"flag-gg","short_names":["flag-gg"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1728,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Ghana Flag","unified":"1F1EC-1F1ED","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ec-1f1ed.png","sheet_x":1,"sheet_y":55,"short_name":"flag-gh","short_names":["flag-gh"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1729,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Gibraltar Flag","unified":"1F1EC-1F1EE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ec-1f1ee.png","sheet_x":1,"sheet_y":56,"short_name":"flag-gi","short_names":["flag-gi"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1730,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Greenland Flag","unified":"1F1EC-1F1F1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ec-1f1f1.png","sheet_x":1,"sheet_y":57,"short_name":"flag-gl","short_names":["flag-gl"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1731,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Gambia Flag","unified":"1F1EC-1F1F2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ec-1f1f2.png","sheet_x":1,"sheet_y":58,"short_name":"flag-gm","short_names":["flag-gm"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1732,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Guinea Flag","unified":"1F1EC-1F1F3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ec-1f1f3.png","sheet_x":1,"sheet_y":59,"short_name":"flag-gn","short_names":["flag-gn"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1733,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Guadeloupe Flag","unified":"1F1EC-1F1F5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ec-1f1f5.png","sheet_x":1,"sheet_y":60,"short_name":"flag-gp","short_names":["flag-gp"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1734,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Equatorial Guinea Flag","unified":"1F1EC-1F1F6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ec-1f1f6.png","sheet_x":1,"sheet_y":61,"short_name":"flag-gq","short_names":["flag-gq"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1735,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Greece Flag","unified":"1F1EC-1F1F7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ec-1f1f7.png","sheet_x":2,"sheet_y":0,"short_name":"flag-gr","short_names":["flag-gr"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1736,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"South Georgia & South Sandwich Islands Flag","unified":"1F1EC-1F1F8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ec-1f1f8.png","sheet_x":2,"sheet_y":1,"short_name":"flag-gs","short_names":["flag-gs"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1737,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Guatemala Flag","unified":"1F1EC-1F1F9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ec-1f1f9.png","sheet_x":2,"sheet_y":2,"short_name":"flag-gt","short_names":["flag-gt"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1738,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Guam Flag","unified":"1F1EC-1F1FA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ec-1f1fa.png","sheet_x":2,"sheet_y":3,"short_name":"flag-gu","short_names":["flag-gu"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1739,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Guinea-Bissau Flag","unified":"1F1EC-1F1FC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ec-1f1fc.png","sheet_x":2,"sheet_y":4,"short_name":"flag-gw","short_names":["flag-gw"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1740,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Guyana Flag","unified":"1F1EC-1F1FE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ec-1f1fe.png","sheet_x":2,"sheet_y":5,"short_name":"flag-gy","short_names":["flag-gy"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1741,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Hong Kong SAR China Flag","unified":"1F1ED-1F1F0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ed-1f1f0.png","sheet_x":2,"sheet_y":6,"short_name":"flag-hk","short_names":["flag-hk"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1742,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Heard & McDonald Islands Flag","unified":"1F1ED-1F1F2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ed-1f1f2.png","sheet_x":2,"sheet_y":7,"short_name":"flag-hm","short_names":["flag-hm"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1743,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Honduras Flag","unified":"1F1ED-1F1F3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ed-1f1f3.png","sheet_x":2,"sheet_y":8,"short_name":"flag-hn","short_names":["flag-hn"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1744,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Croatia Flag","unified":"1F1ED-1F1F7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ed-1f1f7.png","sheet_x":2,"sheet_y":9,"short_name":"flag-hr","short_names":["flag-hr"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1745,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Haiti Flag","unified":"1F1ED-1F1F9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ed-1f1f9.png","sheet_x":2,"sheet_y":10,"short_name":"flag-ht","short_names":["flag-ht"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1746,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Hungary Flag","unified":"1F1ED-1F1FA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ed-1f1fa.png","sheet_x":2,"sheet_y":11,"short_name":"flag-hu","short_names":["flag-hu"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1747,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Canary Islands Flag","unified":"1F1EE-1F1E8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ee-1f1e8.png","sheet_x":2,"sheet_y":12,"short_name":"flag-ic","short_names":["flag-ic"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1748,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Indonesia Flag","unified":"1F1EE-1F1E9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ee-1f1e9.png","sheet_x":2,"sheet_y":13,"short_name":"flag-id","short_names":["flag-id"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1749,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Ireland Flag","unified":"1F1EE-1F1EA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ee-1f1ea.png","sheet_x":2,"sheet_y":14,"short_name":"flag-ie","short_names":["flag-ie"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1750,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Israel Flag","unified":"1F1EE-1F1F1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ee-1f1f1.png","sheet_x":2,"sheet_y":15,"short_name":"flag-il","short_names":["flag-il"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1751,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Isle of Man Flag","unified":"1F1EE-1F1F2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ee-1f1f2.png","sheet_x":2,"sheet_y":16,"short_name":"flag-im","short_names":["flag-im"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1752,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"India Flag","unified":"1F1EE-1F1F3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ee-1f1f3.png","sheet_x":2,"sheet_y":17,"short_name":"flag-in","short_names":["flag-in"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1753,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"British Indian Ocean Territory Flag","unified":"1F1EE-1F1F4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ee-1f1f4.png","sheet_x":2,"sheet_y":18,"short_name":"flag-io","short_names":["flag-io"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1754,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Iraq Flag","unified":"1F1EE-1F1F6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ee-1f1f6.png","sheet_x":2,"sheet_y":19,"short_name":"flag-iq","short_names":["flag-iq"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1755,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Iran Flag","unified":"1F1EE-1F1F7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ee-1f1f7.png","sheet_x":2,"sheet_y":20,"short_name":"flag-ir","short_names":["flag-ir"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1756,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Iceland Flag","unified":"1F1EE-1F1F8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ee-1f1f8.png","sheet_x":2,"sheet_y":21,"short_name":"flag-is","short_names":["flag-is"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1757,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Italy Flag","unified":"1F1EE-1F1F9","non_qualified":null,"docomo":null,"au":"EB0F","softbank":"E50F","google":"FE4E9","image":"1f1ee-1f1f9.png","sheet_x":2,"sheet_y":22,"short_name":"it","short_names":["it","flag-it"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1758,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Jersey Flag","unified":"1F1EF-1F1EA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ef-1f1ea.png","sheet_x":2,"sheet_y":23,"short_name":"flag-je","short_names":["flag-je"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1759,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Jamaica Flag","unified":"1F1EF-1F1F2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ef-1f1f2.png","sheet_x":2,"sheet_y":24,"short_name":"flag-jm","short_names":["flag-jm"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1760,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Jordan Flag","unified":"1F1EF-1F1F4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ef-1f1f4.png","sheet_x":2,"sheet_y":25,"short_name":"flag-jo","short_names":["flag-jo"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1761,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Japan Flag","unified":"1F1EF-1F1F5","non_qualified":null,"docomo":null,"au":"E4CC","softbank":"E50B","google":"FE4E5","image":"1f1ef-1f1f5.png","sheet_x":2,"sheet_y":26,"short_name":"jp","short_names":["jp","flag-jp"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1762,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Kenya Flag","unified":"1F1F0-1F1EA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f0-1f1ea.png","sheet_x":2,"sheet_y":27,"short_name":"flag-ke","short_names":["flag-ke"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1763,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Kyrgyzstan Flag","unified":"1F1F0-1F1EC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f0-1f1ec.png","sheet_x":2,"sheet_y":28,"short_name":"flag-kg","short_names":["flag-kg"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1764,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Cambodia Flag","unified":"1F1F0-1F1ED","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f0-1f1ed.png","sheet_x":2,"sheet_y":29,"short_name":"flag-kh","short_names":["flag-kh"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1765,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Kiribati Flag","unified":"1F1F0-1F1EE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f0-1f1ee.png","sheet_x":2,"sheet_y":30,"short_name":"flag-ki","short_names":["flag-ki"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1766,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Comoros Flag","unified":"1F1F0-1F1F2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f0-1f1f2.png","sheet_x":2,"sheet_y":31,"short_name":"flag-km","short_names":["flag-km"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1767,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"St. Kitts & Nevis Flag","unified":"1F1F0-1F1F3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f0-1f1f3.png","sheet_x":2,"sheet_y":32,"short_name":"flag-kn","short_names":["flag-kn"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1768,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"North Korea Flag","unified":"1F1F0-1F1F5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f0-1f1f5.png","sheet_x":2,"sheet_y":33,"short_name":"flag-kp","short_names":["flag-kp"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1769,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"South Korea Flag","unified":"1F1F0-1F1F7","non_qualified":null,"docomo":null,"au":"EB12","softbank":"E514","google":"FE4EE","image":"1f1f0-1f1f7.png","sheet_x":2,"sheet_y":34,"short_name":"kr","short_names":["kr","flag-kr"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1770,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Kuwait Flag","unified":"1F1F0-1F1FC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f0-1f1fc.png","sheet_x":2,"sheet_y":35,"short_name":"flag-kw","short_names":["flag-kw"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1771,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Cayman Islands Flag","unified":"1F1F0-1F1FE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f0-1f1fe.png","sheet_x":2,"sheet_y":36,"short_name":"flag-ky","short_names":["flag-ky"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1772,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Kazakhstan Flag","unified":"1F1F0-1F1FF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f0-1f1ff.png","sheet_x":2,"sheet_y":37,"short_name":"flag-kz","short_names":["flag-kz"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1773,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Laos Flag","unified":"1F1F1-1F1E6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f1-1f1e6.png","sheet_x":2,"sheet_y":38,"short_name":"flag-la","short_names":["flag-la"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1774,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Lebanon Flag","unified":"1F1F1-1F1E7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f1-1f1e7.png","sheet_x":2,"sheet_y":39,"short_name":"flag-lb","short_names":["flag-lb"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1775,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"St. Lucia Flag","unified":"1F1F1-1F1E8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f1-1f1e8.png","sheet_x":2,"sheet_y":40,"short_name":"flag-lc","short_names":["flag-lc"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1776,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Liechtenstein Flag","unified":"1F1F1-1F1EE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f1-1f1ee.png","sheet_x":2,"sheet_y":41,"short_name":"flag-li","short_names":["flag-li"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1777,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Sri Lanka Flag","unified":"1F1F1-1F1F0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f1-1f1f0.png","sheet_x":2,"sheet_y":42,"short_name":"flag-lk","short_names":["flag-lk"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1778,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Liberia Flag","unified":"1F1F1-1F1F7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f1-1f1f7.png","sheet_x":2,"sheet_y":43,"short_name":"flag-lr","short_names":["flag-lr"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1779,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Lesotho Flag","unified":"1F1F1-1F1F8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f1-1f1f8.png","sheet_x":2,"sheet_y":44,"short_name":"flag-ls","short_names":["flag-ls"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1780,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Lithuania Flag","unified":"1F1F1-1F1F9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f1-1f1f9.png","sheet_x":2,"sheet_y":45,"short_name":"flag-lt","short_names":["flag-lt"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1781,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Luxembourg Flag","unified":"1F1F1-1F1FA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f1-1f1fa.png","sheet_x":2,"sheet_y":46,"short_name":"flag-lu","short_names":["flag-lu"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1782,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Latvia Flag","unified":"1F1F1-1F1FB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f1-1f1fb.png","sheet_x":2,"sheet_y":47,"short_name":"flag-lv","short_names":["flag-lv"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1783,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Libya Flag","unified":"1F1F1-1F1FE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f1-1f1fe.png","sheet_x":2,"sheet_y":48,"short_name":"flag-ly","short_names":["flag-ly"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1784,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Morocco Flag","unified":"1F1F2-1F1E6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1e6.png","sheet_x":2,"sheet_y":49,"short_name":"flag-ma","short_names":["flag-ma"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1785,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Monaco Flag","unified":"1F1F2-1F1E8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1e8.png","sheet_x":2,"sheet_y":50,"short_name":"flag-mc","short_names":["flag-mc"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1786,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Moldova Flag","unified":"1F1F2-1F1E9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1e9.png","sheet_x":2,"sheet_y":51,"short_name":"flag-md","short_names":["flag-md"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1787,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Montenegro Flag","unified":"1F1F2-1F1EA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1ea.png","sheet_x":2,"sheet_y":52,"short_name":"flag-me","short_names":["flag-me"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1788,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"St. Martin Flag","unified":"1F1F2-1F1EB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1eb.png","sheet_x":2,"sheet_y":53,"short_name":"flag-mf","short_names":["flag-mf"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1789,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Madagascar Flag","unified":"1F1F2-1F1EC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1ec.png","sheet_x":2,"sheet_y":54,"short_name":"flag-mg","short_names":["flag-mg"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1790,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Marshall Islands Flag","unified":"1F1F2-1F1ED","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1ed.png","sheet_x":2,"sheet_y":55,"short_name":"flag-mh","short_names":["flag-mh"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1791,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"North Macedonia Flag","unified":"1F1F2-1F1F0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1f0.png","sheet_x":2,"sheet_y":56,"short_name":"flag-mk","short_names":["flag-mk"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1792,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Mali Flag","unified":"1F1F2-1F1F1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1f1.png","sheet_x":2,"sheet_y":57,"short_name":"flag-ml","short_names":["flag-ml"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1793,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Myanmar (Burma) Flag","unified":"1F1F2-1F1F2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1f2.png","sheet_x":2,"sheet_y":58,"short_name":"flag-mm","short_names":["flag-mm"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1794,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Mongolia Flag","unified":"1F1F2-1F1F3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1f3.png","sheet_x":2,"sheet_y":59,"short_name":"flag-mn","short_names":["flag-mn"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1795,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Macao SAR China Flag","unified":"1F1F2-1F1F4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1f4.png","sheet_x":2,"sheet_y":60,"short_name":"flag-mo","short_names":["flag-mo"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1796,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Northern Mariana Islands Flag","unified":"1F1F2-1F1F5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1f5.png","sheet_x":2,"sheet_y":61,"short_name":"flag-mp","short_names":["flag-mp"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1797,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Martinique Flag","unified":"1F1F2-1F1F6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1f6.png","sheet_x":3,"sheet_y":0,"short_name":"flag-mq","short_names":["flag-mq"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1798,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Mauritania Flag","unified":"1F1F2-1F1F7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1f7.png","sheet_x":3,"sheet_y":1,"short_name":"flag-mr","short_names":["flag-mr"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1799,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Montserrat Flag","unified":"1F1F2-1F1F8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1f8.png","sheet_x":3,"sheet_y":2,"short_name":"flag-ms","short_names":["flag-ms"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1800,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Malta Flag","unified":"1F1F2-1F1F9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1f9.png","sheet_x":3,"sheet_y":3,"short_name":"flag-mt","short_names":["flag-mt"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1801,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Mauritius Flag","unified":"1F1F2-1F1FA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1fa.png","sheet_x":3,"sheet_y":4,"short_name":"flag-mu","short_names":["flag-mu"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1802,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Maldives Flag","unified":"1F1F2-1F1FB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1fb.png","sheet_x":3,"sheet_y":5,"short_name":"flag-mv","short_names":["flag-mv"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1803,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Malawi Flag","unified":"1F1F2-1F1FC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1fc.png","sheet_x":3,"sheet_y":6,"short_name":"flag-mw","short_names":["flag-mw"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1804,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Mexico Flag","unified":"1F1F2-1F1FD","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1fd.png","sheet_x":3,"sheet_y":7,"short_name":"flag-mx","short_names":["flag-mx"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1805,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Malaysia Flag","unified":"1F1F2-1F1FE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1fe.png","sheet_x":3,"sheet_y":8,"short_name":"flag-my","short_names":["flag-my"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1806,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Mozambique Flag","unified":"1F1F2-1F1FF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f2-1f1ff.png","sheet_x":3,"sheet_y":9,"short_name":"flag-mz","short_names":["flag-mz"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1807,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Namibia Flag","unified":"1F1F3-1F1E6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f3-1f1e6.png","sheet_x":3,"sheet_y":10,"short_name":"flag-na","short_names":["flag-na"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1808,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"New Caledonia Flag","unified":"1F1F3-1F1E8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f3-1f1e8.png","sheet_x":3,"sheet_y":11,"short_name":"flag-nc","short_names":["flag-nc"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1809,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Niger Flag","unified":"1F1F3-1F1EA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f3-1f1ea.png","sheet_x":3,"sheet_y":12,"short_name":"flag-ne","short_names":["flag-ne"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1810,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Norfolk Island Flag","unified":"1F1F3-1F1EB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f3-1f1eb.png","sheet_x":3,"sheet_y":13,"short_name":"flag-nf","short_names":["flag-nf"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1811,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Nigeria Flag","unified":"1F1F3-1F1EC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f3-1f1ec.png","sheet_x":3,"sheet_y":14,"short_name":"flag-ng","short_names":["flag-ng"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1812,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Nicaragua Flag","unified":"1F1F3-1F1EE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f3-1f1ee.png","sheet_x":3,"sheet_y":15,"short_name":"flag-ni","short_names":["flag-ni"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1813,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Netherlands Flag","unified":"1F1F3-1F1F1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f3-1f1f1.png","sheet_x":3,"sheet_y":16,"short_name":"flag-nl","short_names":["flag-nl"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1814,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Norway Flag","unified":"1F1F3-1F1F4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f3-1f1f4.png","sheet_x":3,"sheet_y":17,"short_name":"flag-no","short_names":["flag-no"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1815,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Nepal Flag","unified":"1F1F3-1F1F5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f3-1f1f5.png","sheet_x":3,"sheet_y":18,"short_name":"flag-np","short_names":["flag-np"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1816,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Nauru Flag","unified":"1F1F3-1F1F7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f3-1f1f7.png","sheet_x":3,"sheet_y":19,"short_name":"flag-nr","short_names":["flag-nr"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1817,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Niue Flag","unified":"1F1F3-1F1FA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f3-1f1fa.png","sheet_x":3,"sheet_y":20,"short_name":"flag-nu","short_names":["flag-nu"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1818,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"New Zealand Flag","unified":"1F1F3-1F1FF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f3-1f1ff.png","sheet_x":3,"sheet_y":21,"short_name":"flag-nz","short_names":["flag-nz"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1819,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Oman Flag","unified":"1F1F4-1F1F2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f4-1f1f2.png","sheet_x":3,"sheet_y":22,"short_name":"flag-om","short_names":["flag-om"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1820,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Panama Flag","unified":"1F1F5-1F1E6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f5-1f1e6.png","sheet_x":3,"sheet_y":23,"short_name":"flag-pa","short_names":["flag-pa"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1821,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Peru Flag","unified":"1F1F5-1F1EA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f5-1f1ea.png","sheet_x":3,"sheet_y":24,"short_name":"flag-pe","short_names":["flag-pe"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1822,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"French Polynesia Flag","unified":"1F1F5-1F1EB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f5-1f1eb.png","sheet_x":3,"sheet_y":25,"short_name":"flag-pf","short_names":["flag-pf"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1823,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Papua New Guinea Flag","unified":"1F1F5-1F1EC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f5-1f1ec.png","sheet_x":3,"sheet_y":26,"short_name":"flag-pg","short_names":["flag-pg"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1824,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Philippines Flag","unified":"1F1F5-1F1ED","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f5-1f1ed.png","sheet_x":3,"sheet_y":27,"short_name":"flag-ph","short_names":["flag-ph"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1825,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Pakistan Flag","unified":"1F1F5-1F1F0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f5-1f1f0.png","sheet_x":3,"sheet_y":28,"short_name":"flag-pk","short_names":["flag-pk"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1826,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Poland Flag","unified":"1F1F5-1F1F1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f5-1f1f1.png","sheet_x":3,"sheet_y":29,"short_name":"flag-pl","short_names":["flag-pl"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1827,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"St. Pierre & Miquelon Flag","unified":"1F1F5-1F1F2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f5-1f1f2.png","sheet_x":3,"sheet_y":30,"short_name":"flag-pm","short_names":["flag-pm"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1828,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Pitcairn Islands Flag","unified":"1F1F5-1F1F3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f5-1f1f3.png","sheet_x":3,"sheet_y":31,"short_name":"flag-pn","short_names":["flag-pn"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1829,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Puerto Rico Flag","unified":"1F1F5-1F1F7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f5-1f1f7.png","sheet_x":3,"sheet_y":32,"short_name":"flag-pr","short_names":["flag-pr"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1830,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Palestinian Territories Flag","unified":"1F1F5-1F1F8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f5-1f1f8.png","sheet_x":3,"sheet_y":33,"short_name":"flag-ps","short_names":["flag-ps"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1831,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Portugal Flag","unified":"1F1F5-1F1F9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f5-1f1f9.png","sheet_x":3,"sheet_y":34,"short_name":"flag-pt","short_names":["flag-pt"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1832,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Palau Flag","unified":"1F1F5-1F1FC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f5-1f1fc.png","sheet_x":3,"sheet_y":35,"short_name":"flag-pw","short_names":["flag-pw"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1833,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Paraguay Flag","unified":"1F1F5-1F1FE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f5-1f1fe.png","sheet_x":3,"sheet_y":36,"short_name":"flag-py","short_names":["flag-py"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1834,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Qatar Flag","unified":"1F1F6-1F1E6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f6-1f1e6.png","sheet_x":3,"sheet_y":37,"short_name":"flag-qa","short_names":["flag-qa"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1835,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"R\u00e9union Flag","unified":"1F1F7-1F1EA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f7-1f1ea.png","sheet_x":3,"sheet_y":38,"short_name":"flag-re","short_names":["flag-re"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1836,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Romania Flag","unified":"1F1F7-1F1F4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f7-1f1f4.png","sheet_x":3,"sheet_y":39,"short_name":"flag-ro","short_names":["flag-ro"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1837,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Serbia Flag","unified":"1F1F7-1F1F8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f7-1f1f8.png","sheet_x":3,"sheet_y":40,"short_name":"flag-rs","short_names":["flag-rs"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1838,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Russia Flag","unified":"1F1F7-1F1FA","non_qualified":null,"docomo":null,"au":"E5D6","softbank":"E512","google":"FE4EC","image":"1f1f7-1f1fa.png","sheet_x":3,"sheet_y":41,"short_name":"ru","short_names":["ru","flag-ru"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1839,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Rwanda Flag","unified":"1F1F7-1F1FC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f7-1f1fc.png","sheet_x":3,"sheet_y":42,"short_name":"flag-rw","short_names":["flag-rw"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1840,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Saudi Arabia Flag","unified":"1F1F8-1F1E6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f8-1f1e6.png","sheet_x":3,"sheet_y":43,"short_name":"flag-sa","short_names":["flag-sa"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1841,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Solomon Islands Flag","unified":"1F1F8-1F1E7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f8-1f1e7.png","sheet_x":3,"sheet_y":44,"short_name":"flag-sb","short_names":["flag-sb"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1842,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Seychelles Flag","unified":"1F1F8-1F1E8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f8-1f1e8.png","sheet_x":3,"sheet_y":45,"short_name":"flag-sc","short_names":["flag-sc"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1843,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Sudan Flag","unified":"1F1F8-1F1E9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f8-1f1e9.png","sheet_x":3,"sheet_y":46,"short_name":"flag-sd","short_names":["flag-sd"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1844,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Sweden Flag","unified":"1F1F8-1F1EA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f8-1f1ea.png","sheet_x":3,"sheet_y":47,"short_name":"flag-se","short_names":["flag-se"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1845,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Singapore Flag","unified":"1F1F8-1F1EC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f8-1f1ec.png","sheet_x":3,"sheet_y":48,"short_name":"flag-sg","short_names":["flag-sg"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1846,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"St. Helena Flag","unified":"1F1F8-1F1ED","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f8-1f1ed.png","sheet_x":3,"sheet_y":49,"short_name":"flag-sh","short_names":["flag-sh"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1847,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Slovenia Flag","unified":"1F1F8-1F1EE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f8-1f1ee.png","sheet_x":3,"sheet_y":50,"short_name":"flag-si","short_names":["flag-si"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1848,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Svalbard & Jan Mayen Flag","unified":"1F1F8-1F1EF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f8-1f1ef.png","sheet_x":3,"sheet_y":51,"short_name":"flag-sj","short_names":["flag-sj"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1849,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Slovakia Flag","unified":"1F1F8-1F1F0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f8-1f1f0.png","sheet_x":3,"sheet_y":52,"short_name":"flag-sk","short_names":["flag-sk"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1850,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Sierra Leone Flag","unified":"1F1F8-1F1F1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f8-1f1f1.png","sheet_x":3,"sheet_y":53,"short_name":"flag-sl","short_names":["flag-sl"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1851,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"San Marino Flag","unified":"1F1F8-1F1F2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f8-1f1f2.png","sheet_x":3,"sheet_y":54,"short_name":"flag-sm","short_names":["flag-sm"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1852,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Senegal Flag","unified":"1F1F8-1F1F3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f8-1f1f3.png","sheet_x":3,"sheet_y":55,"short_name":"flag-sn","short_names":["flag-sn"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1853,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Somalia Flag","unified":"1F1F8-1F1F4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f8-1f1f4.png","sheet_x":3,"sheet_y":56,"short_name":"flag-so","short_names":["flag-so"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1854,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Suriname Flag","unified":"1F1F8-1F1F7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f8-1f1f7.png","sheet_x":3,"sheet_y":57,"short_name":"flag-sr","short_names":["flag-sr"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1855,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"South Sudan Flag","unified":"1F1F8-1F1F8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f8-1f1f8.png","sheet_x":3,"sheet_y":58,"short_name":"flag-ss","short_names":["flag-ss"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1856,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"S\u00e3o Tom\u00e9 & Pr\u00edncipe Flag","unified":"1F1F8-1F1F9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f8-1f1f9.png","sheet_x":3,"sheet_y":59,"short_name":"flag-st","short_names":["flag-st"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1857,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"El Salvador Flag","unified":"1F1F8-1F1FB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f8-1f1fb.png","sheet_x":3,"sheet_y":60,"short_name":"flag-sv","short_names":["flag-sv"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1858,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Sint Maarten Flag","unified":"1F1F8-1F1FD","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f8-1f1fd.png","sheet_x":3,"sheet_y":61,"short_name":"flag-sx","short_names":["flag-sx"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1859,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Syria Flag","unified":"1F1F8-1F1FE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f8-1f1fe.png","sheet_x":4,"sheet_y":0,"short_name":"flag-sy","short_names":["flag-sy"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1860,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Eswatini Flag","unified":"1F1F8-1F1FF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f8-1f1ff.png","sheet_x":4,"sheet_y":1,"short_name":"flag-sz","short_names":["flag-sz"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1861,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Tristan da Cunha Flag","unified":"1F1F9-1F1E6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f9-1f1e6.png","sheet_x":4,"sheet_y":2,"short_name":"flag-ta","short_names":["flag-ta"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1862,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Turks & Caicos Islands Flag","unified":"1F1F9-1F1E8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f9-1f1e8.png","sheet_x":4,"sheet_y":3,"short_name":"flag-tc","short_names":["flag-tc"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1863,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Chad Flag","unified":"1F1F9-1F1E9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f9-1f1e9.png","sheet_x":4,"sheet_y":4,"short_name":"flag-td","short_names":["flag-td"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1864,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"French Southern Territories Flag","unified":"1F1F9-1F1EB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f9-1f1eb.png","sheet_x":4,"sheet_y":5,"short_name":"flag-tf","short_names":["flag-tf"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1865,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Togo Flag","unified":"1F1F9-1F1EC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f9-1f1ec.png","sheet_x":4,"sheet_y":6,"short_name":"flag-tg","short_names":["flag-tg"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1866,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Thailand Flag","unified":"1F1F9-1F1ED","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f9-1f1ed.png","sheet_x":4,"sheet_y":7,"short_name":"flag-th","short_names":["flag-th"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1867,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Tajikistan Flag","unified":"1F1F9-1F1EF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f9-1f1ef.png","sheet_x":4,"sheet_y":8,"short_name":"flag-tj","short_names":["flag-tj"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1868,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Tokelau Flag","unified":"1F1F9-1F1F0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f9-1f1f0.png","sheet_x":4,"sheet_y":9,"short_name":"flag-tk","short_names":["flag-tk"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1869,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Timor-Leste Flag","unified":"1F1F9-1F1F1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f9-1f1f1.png","sheet_x":4,"sheet_y":10,"short_name":"flag-tl","short_names":["flag-tl"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1870,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Turkmenistan Flag","unified":"1F1F9-1F1F2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f9-1f1f2.png","sheet_x":4,"sheet_y":11,"short_name":"flag-tm","short_names":["flag-tm"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1871,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Tunisia Flag","unified":"1F1F9-1F1F3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f9-1f1f3.png","sheet_x":4,"sheet_y":12,"short_name":"flag-tn","short_names":["flag-tn"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1872,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Tonga Flag","unified":"1F1F9-1F1F4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f9-1f1f4.png","sheet_x":4,"sheet_y":13,"short_name":"flag-to","short_names":["flag-to"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1873,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"T\u00fcrkiye Flag","unified":"1F1F9-1F1F7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f9-1f1f7.png","sheet_x":4,"sheet_y":14,"short_name":"flag-tr","short_names":["flag-tr"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1874,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Trinidad & Tobago Flag","unified":"1F1F9-1F1F9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f9-1f1f9.png","sheet_x":4,"sheet_y":15,"short_name":"flag-tt","short_names":["flag-tt"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1875,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Tuvalu Flag","unified":"1F1F9-1F1FB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f9-1f1fb.png","sheet_x":4,"sheet_y":16,"short_name":"flag-tv","short_names":["flag-tv"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1876,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Taiwan Flag","unified":"1F1F9-1F1FC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f9-1f1fc.png","sheet_x":4,"sheet_y":17,"short_name":"flag-tw","short_names":["flag-tw"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1877,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Tanzania Flag","unified":"1F1F9-1F1FF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1f9-1f1ff.png","sheet_x":4,"sheet_y":18,"short_name":"flag-tz","short_names":["flag-tz"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1878,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Ukraine Flag","unified":"1F1FA-1F1E6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1fa-1f1e6.png","sheet_x":4,"sheet_y":19,"short_name":"flag-ua","short_names":["flag-ua"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1879,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Uganda Flag","unified":"1F1FA-1F1EC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1fa-1f1ec.png","sheet_x":4,"sheet_y":20,"short_name":"flag-ug","short_names":["flag-ug"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1880,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"U.S. Outlying Islands Flag","unified":"1F1FA-1F1F2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1fa-1f1f2.png","sheet_x":4,"sheet_y":21,"short_name":"flag-um","short_names":["flag-um"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1881,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"United Nations Flag","unified":"1F1FA-1F1F3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1fa-1f1f3.png","sheet_x":4,"sheet_y":22,"short_name":"flag-un","short_names":["flag-un"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1882,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"United States Flag","unified":"1F1FA-1F1F8","non_qualified":null,"docomo":null,"au":"E573","softbank":"E50C","google":"FE4E6","image":"1f1fa-1f1f8.png","sheet_x":4,"sheet_y":23,"short_name":"us","short_names":["us","flag-us"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1883,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Uruguay Flag","unified":"1F1FA-1F1FE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1fa-1f1fe.png","sheet_x":4,"sheet_y":24,"short_name":"flag-uy","short_names":["flag-uy"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1884,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Uzbekistan Flag","unified":"1F1FA-1F1FF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1fa-1f1ff.png","sheet_x":4,"sheet_y":25,"short_name":"flag-uz","short_names":["flag-uz"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1885,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Vatican City Flag","unified":"1F1FB-1F1E6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1fb-1f1e6.png","sheet_x":4,"sheet_y":26,"short_name":"flag-va","short_names":["flag-va"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1886,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"St. Vincent & Grenadines Flag","unified":"1F1FB-1F1E8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1fb-1f1e8.png","sheet_x":4,"sheet_y":27,"short_name":"flag-vc","short_names":["flag-vc"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1887,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Venezuela Flag","unified":"1F1FB-1F1EA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1fb-1f1ea.png","sheet_x":4,"sheet_y":28,"short_name":"flag-ve","short_names":["flag-ve"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1888,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"British Virgin Islands Flag","unified":"1F1FB-1F1EC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1fb-1f1ec.png","sheet_x":4,"sheet_y":29,"short_name":"flag-vg","short_names":["flag-vg"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1889,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"U.S. Virgin Islands Flag","unified":"1F1FB-1F1EE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1fb-1f1ee.png","sheet_x":4,"sheet_y":30,"short_name":"flag-vi","short_names":["flag-vi"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1890,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Vietnam Flag","unified":"1F1FB-1F1F3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1fb-1f1f3.png","sheet_x":4,"sheet_y":31,"short_name":"flag-vn","short_names":["flag-vn"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1891,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Vanuatu Flag","unified":"1F1FB-1F1FA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1fb-1f1fa.png","sheet_x":4,"sheet_y":32,"short_name":"flag-vu","short_names":["flag-vu"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1892,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Wallis & Futuna Flag","unified":"1F1FC-1F1EB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1fc-1f1eb.png","sheet_x":4,"sheet_y":33,"short_name":"flag-wf","short_names":["flag-wf"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1893,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Samoa Flag","unified":"1F1FC-1F1F8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1fc-1f1f8.png","sheet_x":4,"sheet_y":34,"short_name":"flag-ws","short_names":["flag-ws"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1894,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Kosovo Flag","unified":"1F1FD-1F1F0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1fd-1f1f0.png","sheet_x":4,"sheet_y":35,"short_name":"flag-xk","short_names":["flag-xk"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1895,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Yemen Flag","unified":"1F1FE-1F1EA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1fe-1f1ea.png","sheet_x":4,"sheet_y":36,"short_name":"flag-ye","short_names":["flag-ye"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1896,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Mayotte Flag","unified":"1F1FE-1F1F9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1fe-1f1f9.png","sheet_x":4,"sheet_y":37,"short_name":"flag-yt","short_names":["flag-yt"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1897,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"South Africa Flag","unified":"1F1FF-1F1E6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ff-1f1e6.png","sheet_x":4,"sheet_y":38,"short_name":"flag-za","short_names":["flag-za"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1898,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Zambia Flag","unified":"1F1FF-1F1F2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ff-1f1f2.png","sheet_x":4,"sheet_y":39,"short_name":"flag-zm","short_names":["flag-zm"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1899,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Zimbabwe Flag","unified":"1F1FF-1F1FC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f1ff-1f1fc.png","sheet_x":4,"sheet_y":40,"short_name":"flag-zw","short_names":["flag-zw"],"text":null,"texts":null,"category":"Flags","subcategory":"country-flag","sort_order":1900,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED KATAKANA KOKO","unified":"1F201","non_qualified":null,"docomo":null,"au":null,"softbank":"E203","google":"FEB24","image":"1f201.png","sheet_x":4,"sheet_y":41,"short_name":"koko","short_names":["koko"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1584,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED KATAKANA SA","unified":"1F202-FE0F","non_qualified":"1F202","docomo":null,"au":"EA87","softbank":"E228","google":"FEB3F","image":"1f202-fe0f.png","sheet_x":4,"sheet_y":42,"short_name":"sa","short_names":["sa"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1585,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED CJK UNIFIED IDEOGRAPH-7121","unified":"1F21A","non_qualified":null,"docomo":null,"au":null,"softbank":"E216","google":"FEB3A","image":"1f21a.png","sheet_x":4,"sheet_y":43,"short_name":"u7121","short_names":["u7121"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1591,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED CJK UNIFIED IDEOGRAPH-6307","unified":"1F22F","non_qualified":null,"docomo":null,"au":"EA8B","softbank":"E22C","google":"FEB40","image":"1f22f.png","sheet_x":4,"sheet_y":44,"short_name":"u6307","short_names":["u6307"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1588,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED CJK UNIFIED IDEOGRAPH-7981","unified":"1F232","non_qualified":null,"docomo":"E738","au":null,"softbank":null,"google":"FEB2E","image":"1f232.png","sheet_x":4,"sheet_y":45,"short_name":"u7981","short_names":["u7981"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1592,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED CJK UNIFIED IDEOGRAPH-7A7A","unified":"1F233","non_qualified":null,"docomo":"E739","au":"EA8A","softbank":"E22B","google":"FEB2F","image":"1f233.png","sheet_x":4,"sheet_y":46,"short_name":"u7a7a","short_names":["u7a7a"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1596,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED CJK UNIFIED IDEOGRAPH-5408","unified":"1F234","non_qualified":null,"docomo":"E73A","au":null,"softbank":null,"google":"FEB30","image":"1f234.png","sheet_x":4,"sheet_y":47,"short_name":"u5408","short_names":["u5408"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1595,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED CJK UNIFIED IDEOGRAPH-6E80","unified":"1F235","non_qualified":null,"docomo":"E73B","au":"EA89","softbank":"E22A","google":"FEB31","image":"1f235.png","sheet_x":4,"sheet_y":48,"short_name":"u6e80","short_names":["u6e80"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1600,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED CJK UNIFIED IDEOGRAPH-6709","unified":"1F236","non_qualified":null,"docomo":null,"au":null,"softbank":"E215","google":"FEB39","image":"1f236.png","sheet_x":4,"sheet_y":49,"short_name":"u6709","short_names":["u6709"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1587,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED CJK UNIFIED IDEOGRAPH-6708","unified":"1F237-FE0F","non_qualified":"1F237","docomo":null,"au":null,"softbank":"E217","google":"FEB3B","image":"1f237-fe0f.png","sheet_x":4,"sheet_y":50,"short_name":"u6708","short_names":["u6708"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1586,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED CJK UNIFIED IDEOGRAPH-7533","unified":"1F238","non_qualified":null,"docomo":null,"au":null,"softbank":"E218","google":"FEB3C","image":"1f238.png","sheet_x":4,"sheet_y":51,"short_name":"u7533","short_names":["u7533"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1594,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED CJK UNIFIED IDEOGRAPH-5272","unified":"1F239","non_qualified":null,"docomo":null,"au":"EA86","softbank":"E227","google":"FEB3E","image":"1f239.png","sheet_x":4,"sheet_y":52,"short_name":"u5272","short_names":["u5272"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1590,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUARED CJK UNIFIED IDEOGRAPH-55B6","unified":"1F23A","non_qualified":null,"docomo":null,"au":"EA8C","softbank":"E22D","google":"FEB41","image":"1f23a.png","sheet_x":4,"sheet_y":53,"short_name":"u55b6","short_names":["u55b6"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1599,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CIRCLED IDEOGRAPH ADVANTAGE","unified":"1F250","non_qualified":null,"docomo":null,"au":"E4F7","softbank":"E226","google":"FEB3D","image":"1f250.png","sheet_x":4,"sheet_y":54,"short_name":"ideograph_advantage","short_names":["ideograph_advantage"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1589,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CIRCLED IDEOGRAPH ACCEPT","unified":"1F251","non_qualified":null,"docomo":null,"au":"EB01","softbank":null,"google":"FEB50","image":"1f251.png","sheet_x":4,"sheet_y":55,"short_name":"accept","short_names":["accept"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1593,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CYCLONE","unified":"1F300","non_qualified":null,"docomo":"E643","au":"E469","softbank":"E443","google":"FE005","image":"1f300.png","sheet_x":4,"sheet_y":56,"short_name":"cyclone","short_names":["cyclone"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1051,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FOGGY","unified":"1F301","non_qualified":null,"docomo":"E644","au":"E598","softbank":null,"google":"FE006","image":"1f301.png","sheet_x":4,"sheet_y":57,"short_name":"foggy","short_names":["foggy"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-other","sort_order":898,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOSED UMBRELLA","unified":"1F302","non_qualified":null,"docomo":"E645","au":"EAE8","softbank":"E43C","google":"FE007","image":"1f302.png","sheet_x":4,"sheet_y":58,"short_name":"closed_umbrella","short_names":["closed_umbrella"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1053,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NIGHT WITH STARS","unified":"1F303","non_qualified":null,"docomo":"E6B3","au":"EAF1","softbank":"E44B","google":"FE008","image":"1f303.png","sheet_x":4,"sheet_y":59,"short_name":"night_with_stars","short_names":["night_with_stars"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-other","sort_order":899,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SUNRISE OVER MOUNTAINS","unified":"1F304","non_qualified":null,"docomo":"E63E","au":"EAF4","softbank":"E04D","google":"FE009","image":"1f304.png","sheet_x":4,"sheet_y":60,"short_name":"sunrise_over_mountains","short_names":["sunrise_over_mountains"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-other","sort_order":901,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SUNRISE","unified":"1F305","non_qualified":null,"docomo":"E63E","au":"EAF4","softbank":"E449","google":"FE00A","image":"1f305.png","sheet_x":4,"sheet_y":61,"short_name":"sunrise","short_names":["sunrise"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-other","sort_order":902,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CITYSCAPE AT DUSK","unified":"1F306","non_qualified":null,"docomo":null,"au":"E5DA","softbank":"E146","google":"FE00B","image":"1f306.png","sheet_x":5,"sheet_y":0,"short_name":"city_sunset","short_names":["city_sunset"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-other","sort_order":903,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SUNSET OVER BUILDINGS","unified":"1F307","non_qualified":null,"docomo":"E63E","au":"E5DA","softbank":"E44A","google":"FE00C","image":"1f307.png","sheet_x":5,"sheet_y":1,"short_name":"city_sunrise","short_names":["city_sunrise"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-other","sort_order":904,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RAINBOW","unified":"1F308","non_qualified":null,"docomo":null,"au":"EAF2","softbank":"E44C","google":"FE00D","image":"1f308.png","sheet_x":5,"sheet_y":2,"short_name":"rainbow","short_names":["rainbow"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1052,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BRIDGE AT NIGHT","unified":"1F309","non_qualified":null,"docomo":"E6B3","au":"E4BF","softbank":null,"google":"FE010","image":"1f309.png","sheet_x":5,"sheet_y":3,"short_name":"bridge_at_night","short_names":["bridge_at_night"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-other","sort_order":905,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WATER WAVE","unified":"1F30A","non_qualified":null,"docomo":"E73F","au":"EB7C","softbank":"E43E","google":"FE038","image":"1f30a.png","sheet_x":5,"sheet_y":4,"short_name":"ocean","short_names":["ocean"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1064,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"VOLCANO","unified":"1F30B","non_qualified":null,"docomo":null,"au":"EB53","softbank":null,"google":"FE03A","image":"1f30b.png","sheet_x":5,"sheet_y":5,"short_name":"volcano","short_names":["volcano"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-geographic","sort_order":856,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MILKY WAY","unified":"1F30C","non_qualified":null,"docomo":"E6B3","au":"EB5F","softbank":null,"google":"FE03B","image":"1f30c.png","sheet_x":5,"sheet_y":6,"short_name":"milky_way","short_names":["milky_way"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1038,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EARTH GLOBE EUROPE-AFRICA","unified":"1F30D","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f30d.png","sheet_x":5,"sheet_y":7,"short_name":"earth_africa","short_names":["earth_africa"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-map","sort_order":847,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EARTH GLOBE AMERICAS","unified":"1F30E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f30e.png","sheet_x":5,"sheet_y":8,"short_name":"earth_americas","short_names":["earth_americas"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-map","sort_order":848,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EARTH GLOBE ASIA-AUSTRALIA","unified":"1F30F","non_qualified":null,"docomo":null,"au":"E5B3","softbank":null,"google":"FE039","image":"1f30f.png","sheet_x":5,"sheet_y":9,"short_name":"earth_asia","short_names":["earth_asia"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-map","sort_order":849,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GLOBE WITH MERIDIANS","unified":"1F310","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f310.png","sheet_x":5,"sheet_y":10,"short_name":"globe_with_meridians","short_names":["globe_with_meridians"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-map","sort_order":850,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NEW MOON SYMBOL","unified":"1F311","non_qualified":null,"docomo":"E69C","au":"E5A8","softbank":null,"google":"FE011","image":"1f311.png","sheet_x":5,"sheet_y":11,"short_name":"new_moon","short_names":["new_moon"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1018,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WAXING CRESCENT MOON SYMBOL","unified":"1F312","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f312.png","sheet_x":5,"sheet_y":12,"short_name":"waxing_crescent_moon","short_names":["waxing_crescent_moon"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1019,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FIRST QUARTER MOON SYMBOL","unified":"1F313","non_qualified":null,"docomo":"E69E","au":"E5AA","softbank":null,"google":"FE013","image":"1f313.png","sheet_x":5,"sheet_y":13,"short_name":"first_quarter_moon","short_names":["first_quarter_moon"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1020,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WAXING GIBBOUS MOON SYMBOL","unified":"1F314","non_qualified":null,"docomo":"E69D","au":"E5A9","softbank":null,"google":"FE012","image":"1f314.png","sheet_x":5,"sheet_y":14,"short_name":"moon","short_names":["moon","waxing_gibbous_moon"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1021,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FULL MOON SYMBOL","unified":"1F315","non_qualified":null,"docomo":"E6A0","au":null,"softbank":null,"google":"FE015","image":"1f315.png","sheet_x":5,"sheet_y":15,"short_name":"full_moon","short_names":["full_moon"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1022,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WANING GIBBOUS MOON SYMBOL","unified":"1F316","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f316.png","sheet_x":5,"sheet_y":16,"short_name":"waning_gibbous_moon","short_names":["waning_gibbous_moon"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1023,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LAST QUARTER MOON SYMBOL","unified":"1F317","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f317.png","sheet_x":5,"sheet_y":17,"short_name":"last_quarter_moon","short_names":["last_quarter_moon"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1024,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WANING CRESCENT MOON SYMBOL","unified":"1F318","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f318.png","sheet_x":5,"sheet_y":18,"short_name":"waning_crescent_moon","short_names":["waning_crescent_moon"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1025,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CRESCENT MOON","unified":"1F319","non_qualified":null,"docomo":"E69F","au":"E486","softbank":"E04C","google":"FE014","image":"1f319.png","sheet_x":5,"sheet_y":19,"short_name":"crescent_moon","short_names":["crescent_moon"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1026,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NEW MOON WITH FACE","unified":"1F31A","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f31a.png","sheet_x":5,"sheet_y":20,"short_name":"new_moon_with_face","short_names":["new_moon_with_face"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1027,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FIRST QUARTER MOON WITH FACE","unified":"1F31B","non_qualified":null,"docomo":"E69E","au":"E489","softbank":null,"google":"FE016","image":"1f31b.png","sheet_x":5,"sheet_y":21,"short_name":"first_quarter_moon_with_face","short_names":["first_quarter_moon_with_face"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1028,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LAST QUARTER MOON WITH FACE","unified":"1F31C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f31c.png","sheet_x":5,"sheet_y":22,"short_name":"last_quarter_moon_with_face","short_names":["last_quarter_moon_with_face"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1029,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FULL MOON WITH FACE","unified":"1F31D","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f31d.png","sheet_x":5,"sheet_y":23,"short_name":"full_moon_with_face","short_names":["full_moon_with_face"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1032,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SUN WITH FACE","unified":"1F31E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f31e.png","sheet_x":5,"sheet_y":24,"short_name":"sun_with_face","short_names":["sun_with_face"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1033,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GLOWING STAR","unified":"1F31F","non_qualified":null,"docomo":null,"au":"E48B","softbank":"E335","google":"FEB69","image":"1f31f.png","sheet_x":5,"sheet_y":25,"short_name":"star2","short_names":["star2"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1036,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SHOOTING STAR","unified":"1F320","non_qualified":null,"docomo":null,"au":"E468","softbank":null,"google":"FEB6A","image":"1f320.png","sheet_x":5,"sheet_y":26,"short_name":"stars","short_names":["stars"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1037,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"THERMOMETER","unified":"1F321-FE0F","non_qualified":"1F321","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f321-fe0f.png","sheet_x":5,"sheet_y":27,"short_name":"thermometer","short_names":["thermometer"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1030,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SUN BEHIND SMALL CLOUD","unified":"1F324-FE0F","non_qualified":"1F324","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f324-fe0f.png","sheet_x":5,"sheet_y":28,"short_name":"mostly_sunny","short_names":["mostly_sunny","sun_small_cloud"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1042,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SUN BEHIND LARGE CLOUD","unified":"1F325-FE0F","non_qualified":"1F325","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f325-fe0f.png","sheet_x":5,"sheet_y":29,"short_name":"barely_sunny","short_names":["barely_sunny","sun_behind_cloud"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1043,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SUN BEHIND RAIN CLOUD","unified":"1F326-FE0F","non_qualified":"1F326","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f326-fe0f.png","sheet_x":5,"sheet_y":30,"short_name":"partly_sunny_rain","short_names":["partly_sunny_rain","sun_behind_rain_cloud"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1044,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOUD WITH RAIN","unified":"1F327-FE0F","non_qualified":"1F327","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f327-fe0f.png","sheet_x":5,"sheet_y":31,"short_name":"rain_cloud","short_names":["rain_cloud"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1045,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOUD WITH SNOW","unified":"1F328-FE0F","non_qualified":"1F328","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f328-fe0f.png","sheet_x":5,"sheet_y":32,"short_name":"snow_cloud","short_names":["snow_cloud"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1046,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOUD WITH LIGHTNING","unified":"1F329-FE0F","non_qualified":"1F329","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f329-fe0f.png","sheet_x":5,"sheet_y":33,"short_name":"lightning","short_names":["lightning","lightning_cloud"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1047,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TORNADO","unified":"1F32A-FE0F","non_qualified":"1F32A","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f32a-fe0f.png","sheet_x":5,"sheet_y":34,"short_name":"tornado","short_names":["tornado","tornado_cloud"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1048,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FOG","unified":"1F32B-FE0F","non_qualified":"1F32B","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f32b-fe0f.png","sheet_x":5,"sheet_y":35,"short_name":"fog","short_names":["fog"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1049,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WIND FACE","unified":"1F32C-FE0F","non_qualified":"1F32C","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f32c-fe0f.png","sheet_x":5,"sheet_y":36,"short_name":"wind_blowing_face","short_names":["wind_blowing_face"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1050,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HOT DOG","unified":"1F32D","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f32d.png","sheet_x":5,"sheet_y":37,"short_name":"hotdog","short_names":["hotdog"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":766,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TACO","unified":"1F32E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f32e.png","sheet_x":5,"sheet_y":38,"short_name":"taco","short_names":["taco"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":768,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BURRITO","unified":"1F32F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f32f.png","sheet_x":5,"sheet_y":39,"short_name":"burrito","short_names":["burrito"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":769,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CHESTNUT","unified":"1F330","non_qualified":null,"docomo":null,"au":"EB38","softbank":null,"google":"FE04C","image":"1f330.png","sheet_x":5,"sheet_y":40,"short_name":"chestnut","short_names":["chestnut"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-vegetable","sort_order":746,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SEEDLING","unified":"1F331","non_qualified":null,"docomo":"E746","au":"EB7D","softbank":null,"google":"FE03E","image":"1f331.png","sheet_x":5,"sheet_y":41,"short_name":"seedling","short_names":["seedling"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-other","sort_order":696,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EVERGREEN TREE","unified":"1F332","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f332.png","sheet_x":5,"sheet_y":42,"short_name":"evergreen_tree","short_names":["evergreen_tree"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-other","sort_order":698,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DECIDUOUS TREE","unified":"1F333","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f333.png","sheet_x":5,"sheet_y":43,"short_name":"deciduous_tree","short_names":["deciduous_tree"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-other","sort_order":699,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PALM TREE","unified":"1F334","non_qualified":null,"docomo":null,"au":"E4E2","softbank":"E307","google":"FE047","image":"1f334.png","sheet_x":5,"sheet_y":44,"short_name":"palm_tree","short_names":["palm_tree"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-other","sort_order":700,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CACTUS","unified":"1F335","non_qualified":null,"docomo":null,"au":"EA96","softbank":"E308","google":"FE048","image":"1f335.png","sheet_x":5,"sheet_y":45,"short_name":"cactus","short_names":["cactus"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-other","sort_order":701,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HOT PEPPER","unified":"1F336-FE0F","non_qualified":"1F336","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f336-fe0f.png","sheet_x":5,"sheet_y":46,"short_name":"hot_pepper","short_names":["hot_pepper"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-vegetable","sort_order":737,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TULIP","unified":"1F337","non_qualified":null,"docomo":"E743","au":"E4E4","softbank":"E304","google":"FE03D","image":"1f337.png","sheet_x":5,"sheet_y":47,"short_name":"tulip","short_names":["tulip"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-flower","sort_order":694,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CHERRY BLOSSOM","unified":"1F338","non_qualified":null,"docomo":"E748","au":"E4CA","softbank":"E030","google":"FE040","image":"1f338.png","sheet_x":5,"sheet_y":48,"short_name":"cherry_blossom","short_names":["cherry_blossom"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-flower","sort_order":685,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ROSE","unified":"1F339","non_qualified":null,"docomo":null,"au":"E5BA","softbank":"E032","google":"FE041","image":"1f339.png","sheet_x":5,"sheet_y":49,"short_name":"rose","short_names":["rose"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-flower","sort_order":689,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HIBISCUS","unified":"1F33A","non_qualified":null,"docomo":null,"au":"EA94","softbank":"E303","google":"FE045","image":"1f33a.png","sheet_x":5,"sheet_y":50,"short_name":"hibiscus","short_names":["hibiscus"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-flower","sort_order":691,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SUNFLOWER","unified":"1F33B","non_qualified":null,"docomo":null,"au":"E4E3","softbank":"E305","google":"FE046","image":"1f33b.png","sheet_x":5,"sheet_y":51,"short_name":"sunflower","short_names":["sunflower"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-flower","sort_order":692,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLOSSOM","unified":"1F33C","non_qualified":null,"docomo":null,"au":"EB49","softbank":null,"google":"FE04D","image":"1f33c.png","sheet_x":5,"sheet_y":52,"short_name":"blossom","short_names":["blossom"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-flower","sort_order":693,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EAR OF MAIZE","unified":"1F33D","non_qualified":null,"docomo":null,"au":"EB36","softbank":null,"google":"FE04A","image":"1f33d.png","sheet_x":5,"sheet_y":53,"short_name":"corn","short_names":["corn"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-vegetable","sort_order":736,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EAR OF RICE","unified":"1F33E","non_qualified":null,"docomo":null,"au":null,"softbank":"E444","google":"FE049","image":"1f33e.png","sheet_x":5,"sheet_y":54,"short_name":"ear_of_rice","short_names":["ear_of_rice"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-other","sort_order":702,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HERB","unified":"1F33F","non_qualified":null,"docomo":"E741","au":"EB82","softbank":null,"google":"FE04E","image":"1f33f.png","sheet_x":5,"sheet_y":55,"short_name":"herb","short_names":["herb"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-other","sort_order":703,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FOUR LEAF CLOVER","unified":"1F340","non_qualified":null,"docomo":"E741","au":"E513","softbank":"E110","google":"FE03C","image":"1f340.png","sheet_x":5,"sheet_y":56,"short_name":"four_leaf_clover","short_names":["four_leaf_clover"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-other","sort_order":705,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MAPLE LEAF","unified":"1F341","non_qualified":null,"docomo":"E747","au":"E4CE","softbank":"E118","google":"FE03F","image":"1f341.png","sheet_x":5,"sheet_y":57,"short_name":"maple_leaf","short_names":["maple_leaf"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-other","sort_order":706,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FALLEN LEAF","unified":"1F342","non_qualified":null,"docomo":"E747","au":"E5CD","softbank":"E119","google":"FE042","image":"1f342.png","sheet_x":5,"sheet_y":58,"short_name":"fallen_leaf","short_names":["fallen_leaf"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-other","sort_order":707,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LEAF FLUTTERING IN WIND","unified":"1F343","non_qualified":null,"docomo":null,"au":"E5CD","softbank":"E447","google":"FE043","image":"1f343.png","sheet_x":5,"sheet_y":59,"short_name":"leaves","short_names":["leaves"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-other","sort_order":708,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BROWN MUSHROOM","unified":"1F344-200D-1F7EB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f344-200d-1f7eb.png","sheet_x":5,"sheet_y":60,"short_name":"brown_mushroom","short_names":["brown_mushroom"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-vegetable","sort_order":749,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},{"name":"MUSHROOM","unified":"1F344","non_qualified":null,"docomo":null,"au":"EB37","softbank":null,"google":"FE04B","image":"1f344.png","sheet_x":5,"sheet_y":61,"short_name":"mushroom","short_names":["mushroom"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-other","sort_order":711,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TOMATO","unified":"1F345","non_qualified":null,"docomo":null,"au":"EABB","softbank":"E349","google":"FE055","image":"1f345.png","sheet_x":6,"sheet_y":0,"short_name":"tomato","short_names":["tomato"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-fruit","sort_order":729,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"AUBERGINE","unified":"1F346","non_qualified":null,"docomo":null,"au":"EABC","softbank":"E34A","google":"FE056","image":"1f346.png","sheet_x":6,"sheet_y":1,"short_name":"eggplant","short_names":["eggplant"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-vegetable","sort_order":733,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GRAPES","unified":"1F347","non_qualified":null,"docomo":null,"au":"EB34","softbank":null,"google":"FE059","image":"1f347.png","sheet_x":6,"sheet_y":2,"short_name":"grapes","short_names":["grapes"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-fruit","sort_order":712,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MELON","unified":"1F348","non_qualified":null,"docomo":null,"au":"EB32","softbank":null,"google":"FE057","image":"1f348.png","sheet_x":6,"sheet_y":3,"short_name":"melon","short_names":["melon"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-fruit","sort_order":713,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WATERMELON","unified":"1F349","non_qualified":null,"docomo":null,"au":"E4CD","softbank":"E348","google":"FE054","image":"1f349.png","sheet_x":6,"sheet_y":4,"short_name":"watermelon","short_names":["watermelon"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-fruit","sort_order":714,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TANGERINE","unified":"1F34A","non_qualified":null,"docomo":null,"au":"EABA","softbank":"E346","google":"FE052","image":"1f34a.png","sheet_x":6,"sheet_y":5,"short_name":"tangerine","short_names":["tangerine"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-fruit","sort_order":715,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LIME","unified":"1F34B-200D-1F7E9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f34b-200d-1f7e9.png","sheet_x":6,"sheet_y":6,"short_name":"lime","short_names":["lime"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-fruit","sort_order":717,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},{"name":"LEMON","unified":"1F34B","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f34b.png","sheet_x":6,"sheet_y":7,"short_name":"lemon","short_names":["lemon"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-fruit","sort_order":716,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BANANA","unified":"1F34C","non_qualified":null,"docomo":"E744","au":"EB35","softbank":null,"google":"FE050","image":"1f34c.png","sheet_x":6,"sheet_y":8,"short_name":"banana","short_names":["banana"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-fruit","sort_order":718,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PINEAPPLE","unified":"1F34D","non_qualified":null,"docomo":null,"au":"EB33","softbank":null,"google":"FE058","image":"1f34d.png","sheet_x":6,"sheet_y":9,"short_name":"pineapple","short_names":["pineapple"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-fruit","sort_order":719,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RED APPLE","unified":"1F34E","non_qualified":null,"docomo":"E745","au":"EAB9","softbank":"E345","google":"FE051","image":"1f34e.png","sheet_x":6,"sheet_y":10,"short_name":"apple","short_names":["apple"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-fruit","sort_order":721,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GREEN APPLE","unified":"1F34F","non_qualified":null,"docomo":"E745","au":"EB5A","softbank":null,"google":"FE05B","image":"1f34f.png","sheet_x":6,"sheet_y":11,"short_name":"green_apple","short_names":["green_apple"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-fruit","sort_order":722,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PEAR","unified":"1F350","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f350.png","sheet_x":6,"sheet_y":12,"short_name":"pear","short_names":["pear"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-fruit","sort_order":723,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PEACH","unified":"1F351","non_qualified":null,"docomo":null,"au":"EB39","softbank":null,"google":"FE05A","image":"1f351.png","sheet_x":6,"sheet_y":13,"short_name":"peach","short_names":["peach"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-fruit","sort_order":724,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CHERRIES","unified":"1F352","non_qualified":null,"docomo":"E742","au":"E4D2","softbank":null,"google":"FE04F","image":"1f352.png","sheet_x":6,"sheet_y":14,"short_name":"cherries","short_names":["cherries"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-fruit","sort_order":725,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"STRAWBERRY","unified":"1F353","non_qualified":null,"docomo":null,"au":"E4D4","softbank":"E347","google":"FE053","image":"1f353.png","sheet_x":6,"sheet_y":15,"short_name":"strawberry","short_names":["strawberry"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-fruit","sort_order":726,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HAMBURGER","unified":"1F354","non_qualified":null,"docomo":"E673","au":"E4D6","softbank":"E120","google":"FE960","image":"1f354.png","sheet_x":6,"sheet_y":16,"short_name":"hamburger","short_names":["hamburger"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":763,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SLICE OF PIZZA","unified":"1F355","non_qualified":null,"docomo":null,"au":"EB3B","softbank":null,"google":"FE975","image":"1f355.png","sheet_x":6,"sheet_y":17,"short_name":"pizza","short_names":["pizza"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":765,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MEAT ON BONE","unified":"1F356","non_qualified":null,"docomo":null,"au":"E4C4","softbank":null,"google":"FE972","image":"1f356.png","sheet_x":6,"sheet_y":18,"short_name":"meat_on_bone","short_names":["meat_on_bone"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":759,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"POULTRY LEG","unified":"1F357","non_qualified":null,"docomo":null,"au":"EB3C","softbank":null,"google":"FE976","image":"1f357.png","sheet_x":6,"sheet_y":19,"short_name":"poultry_leg","short_names":["poultry_leg"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":760,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RICE CRACKER","unified":"1F358","non_qualified":null,"docomo":null,"au":"EAB3","softbank":"E33D","google":"FE969","image":"1f358.png","sheet_x":6,"sheet_y":20,"short_name":"rice_cracker","short_names":["rice_cracker"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-asian","sort_order":785,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RICE BALL","unified":"1F359","non_qualified":null,"docomo":"E749","au":"E4D5","softbank":"E342","google":"FE961","image":"1f359.png","sheet_x":6,"sheet_y":21,"short_name":"rice_ball","short_names":["rice_ball"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-asian","sort_order":786,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"COOKED RICE","unified":"1F35A","non_qualified":null,"docomo":"E74C","au":"EAB4","softbank":"E33E","google":"FE96A","image":"1f35a.png","sheet_x":6,"sheet_y":22,"short_name":"rice","short_names":["rice"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-asian","sort_order":787,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CURRY AND RICE","unified":"1F35B","non_qualified":null,"docomo":null,"au":"EAB6","softbank":"E341","google":"FE96C","image":"1f35b.png","sheet_x":6,"sheet_y":23,"short_name":"curry","short_names":["curry"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-asian","sort_order":788,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"STEAMING BOWL","unified":"1F35C","non_qualified":null,"docomo":"E74C","au":"E5B4","softbank":"E340","google":"FE963","image":"1f35c.png","sheet_x":6,"sheet_y":24,"short_name":"ramen","short_names":["ramen"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-asian","sort_order":789,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SPAGHETTI","unified":"1F35D","non_qualified":null,"docomo":null,"au":"EAB5","softbank":"E33F","google":"FE96B","image":"1f35d.png","sheet_x":6,"sheet_y":25,"short_name":"spaghetti","short_names":["spaghetti"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-asian","sort_order":790,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BREAD","unified":"1F35E","non_qualified":null,"docomo":"E74D","au":"EAAF","softbank":"E339","google":"FE964","image":"1f35e.png","sheet_x":6,"sheet_y":26,"short_name":"bread","short_names":["bread"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":750,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FRENCH FRIES","unified":"1F35F","non_qualified":null,"docomo":null,"au":"EAB1","softbank":"E33B","google":"FE967","image":"1f35f.png","sheet_x":6,"sheet_y":27,"short_name":"fries","short_names":["fries"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":764,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ROASTED SWEET POTATO","unified":"1F360","non_qualified":null,"docomo":null,"au":"EB3A","softbank":null,"google":"FE974","image":"1f360.png","sheet_x":6,"sheet_y":28,"short_name":"sweet_potato","short_names":["sweet_potato"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-asian","sort_order":791,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DANGO","unified":"1F361","non_qualified":null,"docomo":null,"au":"EAB2","softbank":"E33C","google":"FE968","image":"1f361.png","sheet_x":6,"sheet_y":29,"short_name":"dango","short_names":["dango"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-asian","sort_order":797,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ODEN","unified":"1F362","non_qualified":null,"docomo":null,"au":"EAB7","softbank":"E343","google":"FE96D","image":"1f362.png","sheet_x":6,"sheet_y":30,"short_name":"oden","short_names":["oden"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-asian","sort_order":792,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SUSHI","unified":"1F363","non_qualified":null,"docomo":null,"au":"EAB8","softbank":"E344","google":"FE96E","image":"1f363.png","sheet_x":6,"sheet_y":31,"short_name":"sushi","short_names":["sushi"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-asian","sort_order":793,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FRIED SHRIMP","unified":"1F364","non_qualified":null,"docomo":null,"au":"EB70","softbank":null,"google":"FE97F","image":"1f364.png","sheet_x":6,"sheet_y":32,"short_name":"fried_shrimp","short_names":["fried_shrimp"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-asian","sort_order":794,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FISH CAKE WITH SWIRL DESIGN","unified":"1F365","non_qualified":null,"docomo":"E643","au":"E4ED","softbank":null,"google":"FE973","image":"1f365.png","sheet_x":6,"sheet_y":33,"short_name":"fish_cake","short_names":["fish_cake"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-asian","sort_order":795,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SOFT ICE CREAM","unified":"1F366","non_qualified":null,"docomo":null,"au":"EAB0","softbank":"E33A","google":"FE966","image":"1f366.png","sheet_x":6,"sheet_y":34,"short_name":"icecream","short_names":["icecream"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-sweet","sort_order":806,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SHAVED ICE","unified":"1F367","non_qualified":null,"docomo":null,"au":"EAEA","softbank":"E43F","google":"FE971","image":"1f367.png","sheet_x":6,"sheet_y":35,"short_name":"shaved_ice","short_names":["shaved_ice"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-sweet","sort_order":807,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ICE CREAM","unified":"1F368","non_qualified":null,"docomo":null,"au":"EB4A","softbank":null,"google":"FE977","image":"1f368.png","sheet_x":6,"sheet_y":36,"short_name":"ice_cream","short_names":["ice_cream"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-sweet","sort_order":808,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DOUGHNUT","unified":"1F369","non_qualified":null,"docomo":null,"au":"EB4B","softbank":null,"google":"FE978","image":"1f369.png","sheet_x":6,"sheet_y":37,"short_name":"doughnut","short_names":["doughnut"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-sweet","sort_order":809,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"COOKIE","unified":"1F36A","non_qualified":null,"docomo":null,"au":"EB4C","softbank":null,"google":"FE979","image":"1f36a.png","sheet_x":6,"sheet_y":38,"short_name":"cookie","short_names":["cookie"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-sweet","sort_order":810,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CHOCOLATE BAR","unified":"1F36B","non_qualified":null,"docomo":null,"au":"EB4D","softbank":null,"google":"FE97A","image":"1f36b.png","sheet_x":6,"sheet_y":39,"short_name":"chocolate_bar","short_names":["chocolate_bar"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-sweet","sort_order":815,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CANDY","unified":"1F36C","non_qualified":null,"docomo":null,"au":"EB4E","softbank":null,"google":"FE97B","image":"1f36c.png","sheet_x":6,"sheet_y":40,"short_name":"candy","short_names":["candy"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-sweet","sort_order":816,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LOLLIPOP","unified":"1F36D","non_qualified":null,"docomo":null,"au":"EB4F","softbank":null,"google":"FE97C","image":"1f36d.png","sheet_x":6,"sheet_y":41,"short_name":"lollipop","short_names":["lollipop"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-sweet","sort_order":817,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CUSTARD","unified":"1F36E","non_qualified":null,"docomo":null,"au":"EB56","softbank":null,"google":"FE97D","image":"1f36e.png","sheet_x":6,"sheet_y":42,"short_name":"custard","short_names":["custard"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-sweet","sort_order":818,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HONEY POT","unified":"1F36F","non_qualified":null,"docomo":null,"au":"EB59","softbank":null,"google":"FE97E","image":"1f36f.png","sheet_x":6,"sheet_y":43,"short_name":"honey_pot","short_names":["honey_pot"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-sweet","sort_order":819,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SHORTCAKE","unified":"1F370","non_qualified":null,"docomo":"E74A","au":"E4D0","softbank":"E046","google":"FE962","image":"1f370.png","sheet_x":6,"sheet_y":44,"short_name":"cake","short_names":["cake"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-sweet","sort_order":812,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BENTO BOX","unified":"1F371","non_qualified":null,"docomo":null,"au":"EABD","softbank":"E34C","google":"FE96F","image":"1f371.png","sheet_x":6,"sheet_y":45,"short_name":"bento","short_names":["bento"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-asian","sort_order":784,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"POT OF FOOD","unified":"1F372","non_qualified":null,"docomo":null,"au":"EABE","softbank":"E34D","google":"FE970","image":"1f372.png","sheet_x":6,"sheet_y":46,"short_name":"stew","short_names":["stew"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":776,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"COOKING","unified":"1F373","non_qualified":null,"docomo":null,"au":"E4D1","softbank":"E147","google":"FE965","image":"1f373.png","sheet_x":6,"sheet_y":47,"short_name":"fried_egg","short_names":["fried_egg","cooking"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":774,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FORK AND KNIFE","unified":"1F374","non_qualified":null,"docomo":"E66F","au":"E4AC","softbank":"E043","google":"FE980","image":"1f374.png","sheet_x":6,"sheet_y":48,"short_name":"fork_and_knife","short_names":["fork_and_knife"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"dishware","sort_order":842,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TEACUP WITHOUT HANDLE","unified":"1F375","non_qualified":null,"docomo":"E71E","au":"EAAE","softbank":"E338","google":"FE984","image":"1f375.png","sheet_x":6,"sheet_y":49,"short_name":"tea","short_names":["tea"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"drink","sort_order":824,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SAKE BOTTLE AND CUP","unified":"1F376","non_qualified":null,"docomo":"E74B","au":"EA97","softbank":"E30B","google":"FE985","image":"1f376.png","sheet_x":6,"sheet_y":50,"short_name":"sake","short_names":["sake"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"drink","sort_order":825,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WINE GLASS","unified":"1F377","non_qualified":null,"docomo":"E756","au":"E4C1","softbank":null,"google":"FE986","image":"1f377.png","sheet_x":6,"sheet_y":51,"short_name":"wine_glass","short_names":["wine_glass"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"drink","sort_order":827,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"COCKTAIL GLASS","unified":"1F378","non_qualified":null,"docomo":"E671","au":"E4C2","softbank":"E044","google":"FE982","image":"1f378.png","sheet_x":6,"sheet_y":52,"short_name":"cocktail","short_names":["cocktail"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"drink","sort_order":828,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TROPICAL DRINK","unified":"1F379","non_qualified":null,"docomo":"E671","au":"EB3E","softbank":null,"google":"FE988","image":"1f379.png","sheet_x":6,"sheet_y":53,"short_name":"tropical_drink","short_names":["tropical_drink"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"drink","sort_order":829,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BEER MUG","unified":"1F37A","non_qualified":null,"docomo":"E672","au":"E4C3","softbank":"E047","google":"FE983","image":"1f37a.png","sheet_x":6,"sheet_y":54,"short_name":"beer","short_names":["beer"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"drink","sort_order":830,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLINKING BEER MUGS","unified":"1F37B","non_qualified":null,"docomo":"E672","au":"EA98","softbank":"E30C","google":"FE987","image":"1f37b.png","sheet_x":6,"sheet_y":55,"short_name":"beers","short_names":["beers"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"drink","sort_order":831,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BABY BOTTLE","unified":"1F37C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f37c.png","sheet_x":6,"sheet_y":56,"short_name":"baby_bottle","short_names":["baby_bottle"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"drink","sort_order":820,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FORK AND KNIFE WITH PLATE","unified":"1F37D-FE0F","non_qualified":"1F37D","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f37d-fe0f.png","sheet_x":6,"sheet_y":57,"short_name":"knife_fork_plate","short_names":["knife_fork_plate"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"dishware","sort_order":841,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BOTTLE WITH POPPING CORK","unified":"1F37E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f37e.png","sheet_x":6,"sheet_y":58,"short_name":"champagne","short_names":["champagne"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"drink","sort_order":826,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"POPCORN","unified":"1F37F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f37f.png","sheet_x":6,"sheet_y":59,"short_name":"popcorn","short_names":["popcorn"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":780,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RIBBON","unified":"1F380","non_qualified":null,"docomo":"E684","au":"E59F","softbank":"E314","google":"FE50F","image":"1f380.png","sheet_x":6,"sheet_y":60,"short_name":"ribbon","short_names":["ribbon"],"text":null,"texts":null,"category":"Activities","subcategory":"event","sort_order":1081,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WRAPPED PRESENT","unified":"1F381","non_qualified":null,"docomo":"E685","au":"E4CF","softbank":"E112","google":"FE510","image":"1f381.png","sheet_x":6,"sheet_y":61,"short_name":"gift","short_names":["gift"],"text":null,"texts":null,"category":"Activities","subcategory":"event","sort_order":1082,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BIRTHDAY CAKE","unified":"1F382","non_qualified":null,"docomo":"E686","au":"E5A0","softbank":"E34B","google":"FE511","image":"1f382.png","sheet_x":7,"sheet_y":0,"short_name":"birthday","short_names":["birthday"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-sweet","sort_order":811,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"JACK-O-LANTERN","unified":"1F383","non_qualified":null,"docomo":null,"au":"EAEE","softbank":"E445","google":"FE51F","image":"1f383.png","sheet_x":7,"sheet_y":1,"short_name":"jack_o_lantern","short_names":["jack_o_lantern"],"text":null,"texts":null,"category":"Activities","subcategory":"event","sort_order":1065,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CHRISTMAS TREE","unified":"1F384","non_qualified":null,"docomo":"E6A4","au":"E4C9","softbank":"E033","google":"FE512","image":"1f384.png","sheet_x":7,"sheet_y":2,"short_name":"christmas_tree","short_names":["christmas_tree"],"text":null,"texts":null,"category":"Activities","subcategory":"event","sort_order":1066,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FATHER CHRISTMAS","unified":"1F385","non_qualified":null,"docomo":null,"au":"EAF0","softbank":"E448","google":"FE513","image":"1f385.png","sheet_x":7,"sheet_y":3,"short_name":"santa","short_names":["santa"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":371,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F385-1F3FB","non_qualified":null,"image":"1f385-1f3fb.png","sheet_x":7,"sheet_y":4,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F385-1F3FC","non_qualified":null,"image":"1f385-1f3fc.png","sheet_x":7,"sheet_y":5,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F385-1F3FD","non_qualified":null,"image":"1f385-1f3fd.png","sheet_x":7,"sheet_y":6,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F385-1F3FE","non_qualified":null,"image":"1f385-1f3fe.png","sheet_x":7,"sheet_y":7,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F385-1F3FF","non_qualified":null,"image":"1f385-1f3ff.png","sheet_x":7,"sheet_y":8,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"FIREWORKS","unified":"1F386","non_qualified":null,"docomo":null,"au":"E5CC","softbank":"E117","google":"FE515","image":"1f386.png","sheet_x":7,"sheet_y":9,"short_name":"fireworks","short_names":["fireworks"],"text":null,"texts":null,"category":"Activities","subcategory":"event","sort_order":1067,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FIREWORK SPARKLER","unified":"1F387","non_qualified":null,"docomo":null,"au":"EAEB","softbank":"E440","google":"FE51D","image":"1f387.png","sheet_x":7,"sheet_y":10,"short_name":"sparkler","short_names":["sparkler"],"text":null,"texts":null,"category":"Activities","subcategory":"event","sort_order":1068,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BALLOON","unified":"1F388","non_qualified":null,"docomo":null,"au":"EA9B","softbank":"E310","google":"FE516","image":"1f388.png","sheet_x":7,"sheet_y":11,"short_name":"balloon","short_names":["balloon"],"text":null,"texts":null,"category":"Activities","subcategory":"event","sort_order":1071,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PARTY POPPER","unified":"1F389","non_qualified":null,"docomo":null,"au":"EA9C","softbank":"E312","google":"FE517","image":"1f389.png","sheet_x":7,"sheet_y":12,"short_name":"tada","short_names":["tada"],"text":null,"texts":null,"category":"Activities","subcategory":"event","sort_order":1072,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CONFETTI BALL","unified":"1F38A","non_qualified":null,"docomo":null,"au":"E46F","softbank":null,"google":"FE520","image":"1f38a.png","sheet_x":7,"sheet_y":13,"short_name":"confetti_ball","short_names":["confetti_ball"],"text":null,"texts":null,"category":"Activities","subcategory":"event","sort_order":1073,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TANABATA TREE","unified":"1F38B","non_qualified":null,"docomo":null,"au":"EB3D","softbank":null,"google":"FE521","image":"1f38b.png","sheet_x":7,"sheet_y":14,"short_name":"tanabata_tree","short_names":["tanabata_tree"],"text":null,"texts":null,"category":"Activities","subcategory":"event","sort_order":1074,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CROSSED FLAGS","unified":"1F38C","non_qualified":null,"docomo":null,"au":"E5D9","softbank":"E143","google":"FE514","image":"1f38c.png","sheet_x":7,"sheet_y":15,"short_name":"crossed_flags","short_names":["crossed_flags"],"text":null,"texts":null,"category":"Flags","subcategory":"flag","sort_order":1637,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PINE DECORATION","unified":"1F38D","non_qualified":null,"docomo":null,"au":"EAE3","softbank":"E436","google":"FE518","image":"1f38d.png","sheet_x":7,"sheet_y":16,"short_name":"bamboo","short_names":["bamboo"],"text":null,"texts":null,"category":"Activities","subcategory":"event","sort_order":1075,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"JAPANESE DOLLS","unified":"1F38E","non_qualified":null,"docomo":null,"au":"EAE4","softbank":"E438","google":"FE519","image":"1f38e.png","sheet_x":7,"sheet_y":17,"short_name":"dolls","short_names":["dolls"],"text":null,"texts":null,"category":"Activities","subcategory":"event","sort_order":1076,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CARP STREAMER","unified":"1F38F","non_qualified":null,"docomo":null,"au":"EAE7","softbank":"E43B","google":"FE51C","image":"1f38f.png","sheet_x":7,"sheet_y":18,"short_name":"flags","short_names":["flags"],"text":null,"texts":null,"category":"Activities","subcategory":"event","sort_order":1077,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WIND CHIME","unified":"1F390","non_qualified":null,"docomo":null,"au":"EAED","softbank":"E442","google":"FE51E","image":"1f390.png","sheet_x":7,"sheet_y":19,"short_name":"wind_chime","short_names":["wind_chime"],"text":null,"texts":null,"category":"Activities","subcategory":"event","sort_order":1078,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MOON VIEWING CEREMONY","unified":"1F391","non_qualified":null,"docomo":null,"au":"EAEF","softbank":"E446","google":"FE017","image":"1f391.png","sheet_x":7,"sheet_y":20,"short_name":"rice_scene","short_names":["rice_scene"],"text":null,"texts":null,"category":"Activities","subcategory":"event","sort_order":1079,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SCHOOL SATCHEL","unified":"1F392","non_qualified":null,"docomo":null,"au":"EAE6","softbank":"E43A","google":"FE51B","image":"1f392.png","sheet_x":7,"sheet_y":21,"short_name":"school_satchel","short_names":["school_satchel"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1175,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GRADUATION CAP","unified":"1F393","non_qualified":null,"docomo":null,"au":"EAE5","softbank":"E439","google":"FE51A","image":"1f393.png","sheet_x":7,"sheet_y":22,"short_name":"mortar_board","short_names":["mortar_board"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1189,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MILITARY MEDAL","unified":"1F396-FE0F","non_qualified":"1F396","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f396-fe0f.png","sheet_x":7,"sheet_y":23,"short_name":"medal","short_names":["medal"],"text":null,"texts":null,"category":"Activities","subcategory":"award-medal","sort_order":1086,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"REMINDER RIBBON","unified":"1F397-FE0F","non_qualified":"1F397","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f397-fe0f.png","sheet_x":7,"sheet_y":24,"short_name":"reminder_ribbon","short_names":["reminder_ribbon"],"text":null,"texts":null,"category":"Activities","subcategory":"event","sort_order":1083,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"STUDIO MICROPHONE","unified":"1F399-FE0F","non_qualified":"1F399","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f399-fe0f.png","sheet_x":7,"sheet_y":25,"short_name":"studio_microphone","short_names":["studio_microphone"],"text":null,"texts":null,"category":"Objects","subcategory":"music","sort_order":1209,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LEVEL SLIDER","unified":"1F39A-FE0F","non_qualified":"1F39A","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f39a-fe0f.png","sheet_x":7,"sheet_y":26,"short_name":"level_slider","short_names":["level_slider"],"text":null,"texts":null,"category":"Objects","subcategory":"music","sort_order":1210,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CONTROL KNOBS","unified":"1F39B-FE0F","non_qualified":"1F39B","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f39b-fe0f.png","sheet_x":7,"sheet_y":27,"short_name":"control_knobs","short_names":["control_knobs"],"text":null,"texts":null,"category":"Objects","subcategory":"music","sort_order":1211,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FILM FRAMES","unified":"1F39E-FE0F","non_qualified":"1F39E","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f39e-fe0f.png","sheet_x":7,"sheet_y":28,"short_name":"film_frames","short_names":["film_frames"],"text":null,"texts":null,"category":"Objects","subcategory":"light & video","sort_order":1247,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ADMISSION TICKETS","unified":"1F39F-FE0F","non_qualified":"1F39F","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f39f-fe0f.png","sheet_x":7,"sheet_y":29,"short_name":"admission_tickets","short_names":["admission_tickets"],"text":null,"texts":null,"category":"Activities","subcategory":"event","sort_order":1084,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CAROUSEL HORSE","unified":"1F3A0","non_qualified":null,"docomo":"E679","au":null,"softbank":null,"google":"FE7FC","image":"1f3a0.png","sheet_x":7,"sheet_y":30,"short_name":"carousel_horse","short_names":["carousel_horse"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-other","sort_order":907,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FERRIS WHEEL","unified":"1F3A1","non_qualified":null,"docomo":null,"au":"E46D","softbank":"E124","google":"FE7FD","image":"1f3a1.png","sheet_x":7,"sheet_y":31,"short_name":"ferris_wheel","short_names":["ferris_wheel"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-other","sort_order":909,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ROLLER COASTER","unified":"1F3A2","non_qualified":null,"docomo":null,"au":"EAE2","softbank":"E433","google":"FE7FE","image":"1f3a2.png","sheet_x":7,"sheet_y":32,"short_name":"roller_coaster","short_names":["roller_coaster"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-other","sort_order":910,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FISHING POLE AND FISH","unified":"1F3A3","non_qualified":null,"docomo":"E751","au":"EB42","softbank":null,"google":"FE7FF","image":"1f3a3.png","sheet_x":7,"sheet_y":33,"short_name":"fishing_pole_and_fish","short_names":["fishing_pole_and_fish"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1113,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MICROPHONE","unified":"1F3A4","non_qualified":null,"docomo":"E676","au":"E503","softbank":"E03C","google":"FE800","image":"1f3a4.png","sheet_x":7,"sheet_y":34,"short_name":"microphone","short_names":["microphone"],"text":null,"texts":null,"category":"Objects","subcategory":"music","sort_order":1212,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MOVIE CAMERA","unified":"1F3A5","non_qualified":null,"docomo":"E677","au":"E517","softbank":"E03D","google":"FE801","image":"1f3a5.png","sheet_x":7,"sheet_y":35,"short_name":"movie_camera","short_names":["movie_camera"],"text":null,"texts":null,"category":"Objects","subcategory":"light & video","sort_order":1246,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CINEMA","unified":"1F3A6","non_qualified":null,"docomo":"E677","au":"E517","softbank":"E507","google":"FE802","image":"1f3a6.png","sheet_x":7,"sheet_y":36,"short_name":"cinema","short_names":["cinema"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1503,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HEADPHONE","unified":"1F3A7","non_qualified":null,"docomo":"E67A","au":"E508","softbank":"E30A","google":"FE803","image":"1f3a7.png","sheet_x":7,"sheet_y":37,"short_name":"headphones","short_names":["headphones"],"text":null,"texts":null,"category":"Objects","subcategory":"music","sort_order":1213,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ARTIST PALETTE","unified":"1F3A8","non_qualified":null,"docomo":"E67B","au":"E59C","softbank":"E502","google":"FE804","image":"1f3a8.png","sheet_x":7,"sheet_y":38,"short_name":"art","short_names":["art"],"text":null,"texts":null,"category":"Activities","subcategory":"arts & crafts","sort_order":1145,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TOP HAT","unified":"1F3A9","non_qualified":null,"docomo":"E67C","au":"EAF5","softbank":"E503","google":"FE805","image":"1f3a9.png","sheet_x":7,"sheet_y":39,"short_name":"tophat","short_names":["tophat"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1188,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CIRCUS TENT","unified":"1F3AA","non_qualified":null,"docomo":"E67D","au":"E59E","softbank":null,"google":"FE806","image":"1f3aa.png","sheet_x":7,"sheet_y":40,"short_name":"circus_tent","short_names":["circus_tent"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-other","sort_order":912,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TICKET","unified":"1F3AB","non_qualified":null,"docomo":"E67E","au":"E49E","softbank":"E125","google":"FE807","image":"1f3ab.png","sheet_x":7,"sheet_y":41,"short_name":"ticket","short_names":["ticket"],"text":null,"texts":null,"category":"Activities","subcategory":"event","sort_order":1085,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLAPPER BOARD","unified":"1F3AC","non_qualified":null,"docomo":"E6AC","au":"E4BE","softbank":"E324","google":"FE808","image":"1f3ac.png","sheet_x":7,"sheet_y":42,"short_name":"clapper","short_names":["clapper"],"text":null,"texts":null,"category":"Objects","subcategory":"light & video","sort_order":1249,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PERFORMING ARTS","unified":"1F3AD","non_qualified":null,"docomo":null,"au":"E59D","softbank":null,"google":"FE809","image":"1f3ad.png","sheet_x":7,"sheet_y":43,"short_name":"performing_arts","short_names":["performing_arts"],"text":null,"texts":null,"category":"Activities","subcategory":"arts & crafts","sort_order":1143,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"VIDEO GAME","unified":"1F3AE","non_qualified":null,"docomo":"E68B","au":"E4C6","softbank":null,"google":"FE80A","image":"1f3ae.png","sheet_x":7,"sheet_y":44,"short_name":"video_game","short_names":["video_game"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1126,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DIRECT HIT","unified":"1F3AF","non_qualified":null,"docomo":null,"au":"E4C5","softbank":"E130","google":"FE80C","image":"1f3af.png","sheet_x":7,"sheet_y":45,"short_name":"dart","short_names":["dart"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1119,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SLOT MACHINE","unified":"1F3B0","non_qualified":null,"docomo":null,"au":"E46E","softbank":"E133","google":"FE80D","image":"1f3b0.png","sheet_x":7,"sheet_y":46,"short_name":"slot_machine","short_names":["slot_machine"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1128,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BILLIARDS","unified":"1F3B1","non_qualified":null,"docomo":null,"au":"EADD","softbank":"E42C","google":"FE80E","image":"1f3b1.png","sheet_x":7,"sheet_y":47,"short_name":"8ball","short_names":["8ball"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1123,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GAME DIE","unified":"1F3B2","non_qualified":null,"docomo":null,"au":"E4C8","softbank":null,"google":"FE80F","image":"1f3b2.png","sheet_x":7,"sheet_y":48,"short_name":"game_die","short_names":["game_die"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1129,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BOWLING","unified":"1F3B3","non_qualified":null,"docomo":null,"au":"EB43","softbank":null,"google":"FE810","image":"1f3b3.png","sheet_x":7,"sheet_y":49,"short_name":"bowling","short_names":["bowling"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1101,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FLOWER PLAYING CARDS","unified":"1F3B4","non_qualified":null,"docomo":null,"au":"EB6E","softbank":null,"google":"FE811","image":"1f3b4.png","sheet_x":7,"sheet_y":50,"short_name":"flower_playing_cards","short_names":["flower_playing_cards"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1142,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MUSICAL NOTE","unified":"1F3B5","non_qualified":null,"docomo":"E6F6","au":"E5BE","softbank":"E03E","google":"FE813","image":"1f3b5.png","sheet_x":7,"sheet_y":51,"short_name":"musical_note","short_names":["musical_note"],"text":null,"texts":null,"category":"Objects","subcategory":"music","sort_order":1207,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MULTIPLE MUSICAL NOTES","unified":"1F3B6","non_qualified":null,"docomo":"E6FF","au":"E505","softbank":"E326","google":"FE814","image":"1f3b6.png","sheet_x":7,"sheet_y":52,"short_name":"notes","short_names":["notes"],"text":null,"texts":null,"category":"Objects","subcategory":"music","sort_order":1208,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SAXOPHONE","unified":"1F3B7","non_qualified":null,"docomo":null,"au":null,"softbank":"E040","google":"FE815","image":"1f3b7.png","sheet_x":7,"sheet_y":53,"short_name":"saxophone","short_names":["saxophone"],"text":null,"texts":null,"category":"Objects","subcategory":"musical-instrument","sort_order":1215,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GUITAR","unified":"1F3B8","non_qualified":null,"docomo":null,"au":"E506","softbank":"E041","google":"FE816","image":"1f3b8.png","sheet_x":7,"sheet_y":54,"short_name":"guitar","short_names":["guitar"],"text":null,"texts":null,"category":"Objects","subcategory":"musical-instrument","sort_order":1217,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MUSICAL KEYBOARD","unified":"1F3B9","non_qualified":null,"docomo":null,"au":"EB40","softbank":null,"google":"FE817","image":"1f3b9.png","sheet_x":7,"sheet_y":55,"short_name":"musical_keyboard","short_names":["musical_keyboard"],"text":null,"texts":null,"category":"Objects","subcategory":"musical-instrument","sort_order":1218,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TRUMPET","unified":"1F3BA","non_qualified":null,"docomo":null,"au":"EADC","softbank":"E042","google":"FE818","image":"1f3ba.png","sheet_x":7,"sheet_y":56,"short_name":"trumpet","short_names":["trumpet"],"text":null,"texts":null,"category":"Objects","subcategory":"musical-instrument","sort_order":1219,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"VIOLIN","unified":"1F3BB","non_qualified":null,"docomo":null,"au":"E507","softbank":null,"google":"FE819","image":"1f3bb.png","sheet_x":7,"sheet_y":57,"short_name":"violin","short_names":["violin"],"text":null,"texts":null,"category":"Objects","subcategory":"musical-instrument","sort_order":1220,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MUSICAL SCORE","unified":"1F3BC","non_qualified":null,"docomo":"E6FF","au":"EACC","softbank":null,"google":"FE81A","image":"1f3bc.png","sheet_x":7,"sheet_y":58,"short_name":"musical_score","short_names":["musical_score"],"text":null,"texts":null,"category":"Objects","subcategory":"music","sort_order":1206,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RUNNING SHIRT WITH SASH","unified":"1F3BD","non_qualified":null,"docomo":"E652","au":null,"softbank":null,"google":"FE7D0","image":"1f3bd.png","sheet_x":7,"sheet_y":59,"short_name":"running_shirt_with_sash","short_names":["running_shirt_with_sash"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1115,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TENNIS RACQUET AND BALL","unified":"1F3BE","non_qualified":null,"docomo":"E655","au":"E4B7","softbank":"E015","google":"FE7D3","image":"1f3be.png","sheet_x":7,"sheet_y":60,"short_name":"tennis","short_names":["tennis"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1099,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SKI AND SKI BOOT","unified":"1F3BF","non_qualified":null,"docomo":"E657","au":"EAAC","softbank":"E013","google":"FE7D5","image":"1f3bf.png","sheet_x":7,"sheet_y":61,"short_name":"ski","short_names":["ski"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1116,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BASKETBALL AND HOOP","unified":"1F3C0","non_qualified":null,"docomo":"E658","au":"E59A","softbank":"E42A","google":"FE7D6","image":"1f3c0.png","sheet_x":8,"sheet_y":0,"short_name":"basketball","short_names":["basketball"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1095,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CHEQUERED FLAG","unified":"1F3C1","non_qualified":null,"docomo":"E659","au":"E4B9","softbank":"E132","google":"FE7D7","image":"1f3c1.png","sheet_x":8,"sheet_y":1,"short_name":"checkered_flag","short_names":["checkered_flag"],"text":null,"texts":null,"category":"Flags","subcategory":"flag","sort_order":1635,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SNOWBOARDER","unified":"1F3C2","non_qualified":null,"docomo":"E712","au":"E4B8","softbank":null,"google":"FE7D8","image":"1f3c2.png","sheet_x":8,"sheet_y":2,"short_name":"snowboarder","short_names":["snowboarder"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":462,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F3C2-1F3FB","non_qualified":null,"image":"1f3c2-1f3fb.png","sheet_x":8,"sheet_y":3,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F3C2-1F3FC","non_qualified":null,"image":"1f3c2-1f3fc.png","sheet_x":8,"sheet_y":4,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F3C2-1F3FD","non_qualified":null,"image":"1f3c2-1f3fd.png","sheet_x":8,"sheet_y":5,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F3C2-1F3FE","non_qualified":null,"image":"1f3c2-1f3fe.png","sheet_x":8,"sheet_y":6,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F3C2-1F3FF","non_qualified":null,"image":"1f3c2-1f3ff.png","sheet_x":8,"sheet_y":7,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN RUNNING","unified":"1F3C3-200D-2640-FE0F","non_qualified":"1F3C3-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3c3-200d-2640-fe0f.png","sheet_x":8,"sheet_y":8,"short_name":"woman-running","short_names":["woman-running"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":443,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F3C3-1F3FB-200D-2640-FE0F","non_qualified":"1F3C3-1F3FB-200D-2640","image":"1f3c3-1f3fb-200d-2640-fe0f.png","sheet_x":8,"sheet_y":9,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F3C3-1F3FC-200D-2640-FE0F","non_qualified":"1F3C3-1F3FC-200D-2640","image":"1f3c3-1f3fc-200d-2640-fe0f.png","sheet_x":8,"sheet_y":10,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F3C3-1F3FD-200D-2640-FE0F","non_qualified":"1F3C3-1F3FD-200D-2640","image":"1f3c3-1f3fd-200d-2640-fe0f.png","sheet_x":8,"sheet_y":11,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F3C3-1F3FE-200D-2640-FE0F","non_qualified":"1F3C3-1F3FE-200D-2640","image":"1f3c3-1f3fe-200d-2640-fe0f.png","sheet_x":8,"sheet_y":12,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F3C3-1F3FF-200D-2640-FE0F","non_qualified":"1F3C3-1F3FF-200D-2640","image":"1f3c3-1f3ff-200d-2640-fe0f.png","sheet_x":8,"sheet_y":13,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN RUNNING FACING RIGHT","unified":"1F3C3-200D-2640-FE0F-200D-27A1-FE0F","non_qualified":"1F3C3-200D-2640-200D-27A1","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3c3-200d-2640-fe0f-200d-27a1-fe0f.png","sheet_x":8,"sheet_y":14,"short_name":"woman_running_facing_right","short_names":["woman_running_facing_right"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":445,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"1F3C3-1F3FB-200D-2640-FE0F-200D-27A1-FE0F","non_qualified":"1F3C3-1F3FB-200D-2640-200D-27A1","image":"1f3c3-1f3fb-200d-2640-fe0f-200d-27a1-fe0f.png","sheet_x":8,"sheet_y":15,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FC":{"unified":"1F3C3-1F3FC-200D-2640-FE0F-200D-27A1-FE0F","non_qualified":"1F3C3-1F3FC-200D-2640-200D-27A1","image":"1f3c3-1f3fc-200d-2640-fe0f-200d-27a1-fe0f.png","sheet_x":8,"sheet_y":16,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FD":{"unified":"1F3C3-1F3FD-200D-2640-FE0F-200D-27A1-FE0F","non_qualified":"1F3C3-1F3FD-200D-2640-200D-27A1","image":"1f3c3-1f3fd-200d-2640-fe0f-200d-27a1-fe0f.png","sheet_x":8,"sheet_y":17,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FE":{"unified":"1F3C3-1F3FE-200D-2640-FE0F-200D-27A1-FE0F","non_qualified":"1F3C3-1F3FE-200D-2640-200D-27A1","image":"1f3c3-1f3fe-200d-2640-fe0f-200d-27a1-fe0f.png","sheet_x":8,"sheet_y":18,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FF":{"unified":"1F3C3-1F3FF-200D-2640-FE0F-200D-27A1-FE0F","non_qualified":"1F3C3-1F3FF-200D-2640-200D-27A1","image":"1f3c3-1f3ff-200d-2640-fe0f-200d-27a1-fe0f.png","sheet_x":8,"sheet_y":19,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false}}},{"name":"MAN RUNNING","unified":"1F3C3-200D-2642-FE0F","non_qualified":"1F3C3-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3c3-200d-2642-fe0f.png","sheet_x":8,"sheet_y":20,"short_name":"man-running","short_names":["man-running"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":442,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F3C3-1F3FB-200D-2642-FE0F","non_qualified":"1F3C3-1F3FB-200D-2642","image":"1f3c3-1f3fb-200d-2642-fe0f.png","sheet_x":8,"sheet_y":21,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F3C3-1F3FC-200D-2642-FE0F","non_qualified":"1F3C3-1F3FC-200D-2642","image":"1f3c3-1f3fc-200d-2642-fe0f.png","sheet_x":8,"sheet_y":22,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F3C3-1F3FD-200D-2642-FE0F","non_qualified":"1F3C3-1F3FD-200D-2642","image":"1f3c3-1f3fd-200d-2642-fe0f.png","sheet_x":8,"sheet_y":23,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F3C3-1F3FE-200D-2642-FE0F","non_qualified":"1F3C3-1F3FE-200D-2642","image":"1f3c3-1f3fe-200d-2642-fe0f.png","sheet_x":8,"sheet_y":24,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F3C3-1F3FF-200D-2642-FE0F","non_qualified":"1F3C3-1F3FF-200D-2642","image":"1f3c3-1f3ff-200d-2642-fe0f.png","sheet_x":8,"sheet_y":25,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F3C3"},{"name":"MAN RUNNING FACING RIGHT","unified":"1F3C3-200D-2642-FE0F-200D-27A1-FE0F","non_qualified":"1F3C3-200D-2642-200D-27A1","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3c3-200d-2642-fe0f-200d-27a1-fe0f.png","sheet_x":8,"sheet_y":26,"short_name":"man_running_facing_right","short_names":["man_running_facing_right"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":446,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"1F3C3-1F3FB-200D-2642-FE0F-200D-27A1-FE0F","non_qualified":"1F3C3-1F3FB-200D-2642-200D-27A1","image":"1f3c3-1f3fb-200d-2642-fe0f-200d-27a1-fe0f.png","sheet_x":8,"sheet_y":27,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FC":{"unified":"1F3C3-1F3FC-200D-2642-FE0F-200D-27A1-FE0F","non_qualified":"1F3C3-1F3FC-200D-2642-200D-27A1","image":"1f3c3-1f3fc-200d-2642-fe0f-200d-27a1-fe0f.png","sheet_x":8,"sheet_y":28,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FD":{"unified":"1F3C3-1F3FD-200D-2642-FE0F-200D-27A1-FE0F","non_qualified":"1F3C3-1F3FD-200D-2642-200D-27A1","image":"1f3c3-1f3fd-200d-2642-fe0f-200d-27a1-fe0f.png","sheet_x":8,"sheet_y":29,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FE":{"unified":"1F3C3-1F3FE-200D-2642-FE0F-200D-27A1-FE0F","non_qualified":"1F3C3-1F3FE-200D-2642-200D-27A1","image":"1f3c3-1f3fe-200d-2642-fe0f-200d-27a1-fe0f.png","sheet_x":8,"sheet_y":30,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FF":{"unified":"1F3C3-1F3FF-200D-2642-FE0F-200D-27A1-FE0F","non_qualified":"1F3C3-1F3FF-200D-2642-200D-27A1","image":"1f3c3-1f3ff-200d-2642-fe0f-200d-27a1-fe0f.png","sheet_x":8,"sheet_y":31,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false}}},{"name":"PERSON RUNNING FACING RIGHT","unified":"1F3C3-200D-27A1-FE0F","non_qualified":"1F3C3-200D-27A1","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3c3-200d-27a1-fe0f.png","sheet_x":8,"sheet_y":32,"short_name":"person_running_facing_right","short_names":["person_running_facing_right"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":444,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"1F3C3-1F3FB-200D-27A1-FE0F","non_qualified":"1F3C3-1F3FB-200D-27A1","image":"1f3c3-1f3fb-200d-27a1-fe0f.png","sheet_x":8,"sheet_y":33,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FC":{"unified":"1F3C3-1F3FC-200D-27A1-FE0F","non_qualified":"1F3C3-1F3FC-200D-27A1","image":"1f3c3-1f3fc-200d-27a1-fe0f.png","sheet_x":8,"sheet_y":34,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FD":{"unified":"1F3C3-1F3FD-200D-27A1-FE0F","non_qualified":"1F3C3-1F3FD-200D-27A1","image":"1f3c3-1f3fd-200d-27a1-fe0f.png","sheet_x":8,"sheet_y":35,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FE":{"unified":"1F3C3-1F3FE-200D-27A1-FE0F","non_qualified":"1F3C3-1F3FE-200D-27A1","image":"1f3c3-1f3fe-200d-27a1-fe0f.png","sheet_x":8,"sheet_y":36,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FF":{"unified":"1F3C3-1F3FF-200D-27A1-FE0F","non_qualified":"1F3C3-1F3FF-200D-27A1","image":"1f3c3-1f3ff-200d-27a1-fe0f.png","sheet_x":8,"sheet_y":37,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false}}},{"name":"RUNNER","unified":"1F3C3","non_qualified":null,"docomo":"E733","au":"E46B","softbank":"E115","google":"FE7D9","image":"1f3c3.png","sheet_x":8,"sheet_y":38,"short_name":"runner","short_names":["runner","running"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":441,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F3C3-1F3FB","non_qualified":null,"image":"1f3c3-1f3fb.png","sheet_x":8,"sheet_y":39,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F3C3-1F3FC","non_qualified":null,"image":"1f3c3-1f3fc.png","sheet_x":8,"sheet_y":40,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F3C3-1F3FD","non_qualified":null,"image":"1f3c3-1f3fd.png","sheet_x":8,"sheet_y":41,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F3C3-1F3FE","non_qualified":null,"image":"1f3c3-1f3fe.png","sheet_x":8,"sheet_y":42,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F3C3-1F3FF","non_qualified":null,"image":"1f3c3-1f3ff.png","sheet_x":8,"sheet_y":43,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F3C3-200D-2642-FE0F"},{"name":"WOMAN SURFING","unified":"1F3C4-200D-2640-FE0F","non_qualified":"1F3C4-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3c4-200d-2640-fe0f.png","sheet_x":8,"sheet_y":44,"short_name":"woman-surfing","short_names":["woman-surfing"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":468,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F3C4-1F3FB-200D-2640-FE0F","non_qualified":"1F3C4-1F3FB-200D-2640","image":"1f3c4-1f3fb-200d-2640-fe0f.png","sheet_x":8,"sheet_y":45,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F3C4-1F3FC-200D-2640-FE0F","non_qualified":"1F3C4-1F3FC-200D-2640","image":"1f3c4-1f3fc-200d-2640-fe0f.png","sheet_x":8,"sheet_y":46,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F3C4-1F3FD-200D-2640-FE0F","non_qualified":"1F3C4-1F3FD-200D-2640","image":"1f3c4-1f3fd-200d-2640-fe0f.png","sheet_x":8,"sheet_y":47,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F3C4-1F3FE-200D-2640-FE0F","non_qualified":"1F3C4-1F3FE-200D-2640","image":"1f3c4-1f3fe-200d-2640-fe0f.png","sheet_x":8,"sheet_y":48,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F3C4-1F3FF-200D-2640-FE0F","non_qualified":"1F3C4-1F3FF-200D-2640","image":"1f3c4-1f3ff-200d-2640-fe0f.png","sheet_x":8,"sheet_y":49,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN SURFING","unified":"1F3C4-200D-2642-FE0F","non_qualified":"1F3C4-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3c4-200d-2642-fe0f.png","sheet_x":8,"sheet_y":50,"short_name":"man-surfing","short_names":["man-surfing"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":467,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F3C4-1F3FB-200D-2642-FE0F","non_qualified":"1F3C4-1F3FB-200D-2642","image":"1f3c4-1f3fb-200d-2642-fe0f.png","sheet_x":8,"sheet_y":51,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F3C4-1F3FC-200D-2642-FE0F","non_qualified":"1F3C4-1F3FC-200D-2642","image":"1f3c4-1f3fc-200d-2642-fe0f.png","sheet_x":8,"sheet_y":52,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F3C4-1F3FD-200D-2642-FE0F","non_qualified":"1F3C4-1F3FD-200D-2642","image":"1f3c4-1f3fd-200d-2642-fe0f.png","sheet_x":8,"sheet_y":53,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F3C4-1F3FE-200D-2642-FE0F","non_qualified":"1F3C4-1F3FE-200D-2642","image":"1f3c4-1f3fe-200d-2642-fe0f.png","sheet_x":8,"sheet_y":54,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F3C4-1F3FF-200D-2642-FE0F","non_qualified":"1F3C4-1F3FF-200D-2642","image":"1f3c4-1f3ff-200d-2642-fe0f.png","sheet_x":8,"sheet_y":55,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F3C4"},{"name":"SURFER","unified":"1F3C4","non_qualified":null,"docomo":"E712","au":"EB41","softbank":"E017","google":"FE7DA","image":"1f3c4.png","sheet_x":8,"sheet_y":56,"short_name":"surfer","short_names":["surfer"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":466,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F3C4-1F3FB","non_qualified":null,"image":"1f3c4-1f3fb.png","sheet_x":8,"sheet_y":57,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F3C4-1F3FC","non_qualified":null,"image":"1f3c4-1f3fc.png","sheet_x":8,"sheet_y":58,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F3C4-1F3FD","non_qualified":null,"image":"1f3c4-1f3fd.png","sheet_x":8,"sheet_y":59,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F3C4-1F3FE","non_qualified":null,"image":"1f3c4-1f3fe.png","sheet_x":8,"sheet_y":60,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F3C4-1F3FF","non_qualified":null,"image":"1f3c4-1f3ff.png","sheet_x":8,"sheet_y":61,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F3C4-200D-2642-FE0F"},{"name":"SPORTS MEDAL","unified":"1F3C5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3c5.png","sheet_x":9,"sheet_y":0,"short_name":"sports_medal","short_names":["sports_medal"],"text":null,"texts":null,"category":"Activities","subcategory":"award-medal","sort_order":1088,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TROPHY","unified":"1F3C6","non_qualified":null,"docomo":null,"au":"E5D3","softbank":"E131","google":"FE7DB","image":"1f3c6.png","sheet_x":9,"sheet_y":1,"short_name":"trophy","short_names":["trophy"],"text":null,"texts":null,"category":"Activities","subcategory":"award-medal","sort_order":1087,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HORSE RACING","unified":"1F3C7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3c7.png","sheet_x":9,"sheet_y":2,"short_name":"horse_racing","short_names":["horse_racing"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":460,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F3C7-1F3FB","non_qualified":null,"image":"1f3c7-1f3fb.png","sheet_x":9,"sheet_y":3,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F3C7-1F3FC","non_qualified":null,"image":"1f3c7-1f3fc.png","sheet_x":9,"sheet_y":4,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F3C7-1F3FD","non_qualified":null,"image":"1f3c7-1f3fd.png","sheet_x":9,"sheet_y":5,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F3C7-1F3FE","non_qualified":null,"image":"1f3c7-1f3fe.png","sheet_x":9,"sheet_y":6,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F3C7-1F3FF","non_qualified":null,"image":"1f3c7-1f3ff.png","sheet_x":9,"sheet_y":7,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"AMERICAN FOOTBALL","unified":"1F3C8","non_qualified":null,"docomo":null,"au":"E4BB","softbank":"E42B","google":"FE7DD","image":"1f3c8.png","sheet_x":9,"sheet_y":8,"short_name":"football","short_names":["football"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1097,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RUGBY FOOTBALL","unified":"1F3C9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3c9.png","sheet_x":9,"sheet_y":9,"short_name":"rugby_football","short_names":["rugby_football"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1098,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WOMAN SWIMMING","unified":"1F3CA-200D-2640-FE0F","non_qualified":"1F3CA-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3ca-200d-2640-fe0f.png","sheet_x":9,"sheet_y":10,"short_name":"woman-swimming","short_names":["woman-swimming"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":474,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F3CA-1F3FB-200D-2640-FE0F","non_qualified":"1F3CA-1F3FB-200D-2640","image":"1f3ca-1f3fb-200d-2640-fe0f.png","sheet_x":9,"sheet_y":11,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F3CA-1F3FC-200D-2640-FE0F","non_qualified":"1F3CA-1F3FC-200D-2640","image":"1f3ca-1f3fc-200d-2640-fe0f.png","sheet_x":9,"sheet_y":12,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F3CA-1F3FD-200D-2640-FE0F","non_qualified":"1F3CA-1F3FD-200D-2640","image":"1f3ca-1f3fd-200d-2640-fe0f.png","sheet_x":9,"sheet_y":13,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F3CA-1F3FE-200D-2640-FE0F","non_qualified":"1F3CA-1F3FE-200D-2640","image":"1f3ca-1f3fe-200d-2640-fe0f.png","sheet_x":9,"sheet_y":14,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F3CA-1F3FF-200D-2640-FE0F","non_qualified":"1F3CA-1F3FF-200D-2640","image":"1f3ca-1f3ff-200d-2640-fe0f.png","sheet_x":9,"sheet_y":15,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN SWIMMING","unified":"1F3CA-200D-2642-FE0F","non_qualified":"1F3CA-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3ca-200d-2642-fe0f.png","sheet_x":9,"sheet_y":16,"short_name":"man-swimming","short_names":["man-swimming"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":473,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F3CA-1F3FB-200D-2642-FE0F","non_qualified":"1F3CA-1F3FB-200D-2642","image":"1f3ca-1f3fb-200d-2642-fe0f.png","sheet_x":9,"sheet_y":17,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F3CA-1F3FC-200D-2642-FE0F","non_qualified":"1F3CA-1F3FC-200D-2642","image":"1f3ca-1f3fc-200d-2642-fe0f.png","sheet_x":9,"sheet_y":18,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F3CA-1F3FD-200D-2642-FE0F","non_qualified":"1F3CA-1F3FD-200D-2642","image":"1f3ca-1f3fd-200d-2642-fe0f.png","sheet_x":9,"sheet_y":19,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F3CA-1F3FE-200D-2642-FE0F","non_qualified":"1F3CA-1F3FE-200D-2642","image":"1f3ca-1f3fe-200d-2642-fe0f.png","sheet_x":9,"sheet_y":20,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F3CA-1F3FF-200D-2642-FE0F","non_qualified":"1F3CA-1F3FF-200D-2642","image":"1f3ca-1f3ff-200d-2642-fe0f.png","sheet_x":9,"sheet_y":21,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F3CA"},{"name":"SWIMMER","unified":"1F3CA","non_qualified":null,"docomo":null,"au":"EADE","softbank":"E42D","google":"FE7DE","image":"1f3ca.png","sheet_x":9,"sheet_y":22,"short_name":"swimmer","short_names":["swimmer"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":472,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F3CA-1F3FB","non_qualified":null,"image":"1f3ca-1f3fb.png","sheet_x":9,"sheet_y":23,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F3CA-1F3FC","non_qualified":null,"image":"1f3ca-1f3fc.png","sheet_x":9,"sheet_y":24,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F3CA-1F3FD","non_qualified":null,"image":"1f3ca-1f3fd.png","sheet_x":9,"sheet_y":25,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F3CA-1F3FE","non_qualified":null,"image":"1f3ca-1f3fe.png","sheet_x":9,"sheet_y":26,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F3CA-1F3FF","non_qualified":null,"image":"1f3ca-1f3ff.png","sheet_x":9,"sheet_y":27,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F3CA-200D-2642-FE0F"},{"name":"WOMAN LIFTING WEIGHTS","unified":"1F3CB-FE0F-200D-2640-FE0F","non_qualified":"1F3CB-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3cb-fe0f-200d-2640-fe0f.png","sheet_x":9,"sheet_y":28,"short_name":"woman-lifting-weights","short_names":["woman-lifting-weights"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":480,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"1F3CB-1F3FB-200D-2640-FE0F","non_qualified":"1F3CB-1F3FB-200D-2640","image":"1f3cb-1f3fb-200d-2640-fe0f.png","sheet_x":9,"sheet_y":29,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F3CB-1F3FC-200D-2640-FE0F","non_qualified":"1F3CB-1F3FC-200D-2640","image":"1f3cb-1f3fc-200d-2640-fe0f.png","sheet_x":9,"sheet_y":30,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F3CB-1F3FD-200D-2640-FE0F","non_qualified":"1F3CB-1F3FD-200D-2640","image":"1f3cb-1f3fd-200d-2640-fe0f.png","sheet_x":9,"sheet_y":31,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F3CB-1F3FE-200D-2640-FE0F","non_qualified":"1F3CB-1F3FE-200D-2640","image":"1f3cb-1f3fe-200d-2640-fe0f.png","sheet_x":9,"sheet_y":32,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F3CB-1F3FF-200D-2640-FE0F","non_qualified":"1F3CB-1F3FF-200D-2640","image":"1f3cb-1f3ff-200d-2640-fe0f.png","sheet_x":9,"sheet_y":33,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN LIFTING WEIGHTS","unified":"1F3CB-FE0F-200D-2642-FE0F","non_qualified":"1F3CB-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3cb-fe0f-200d-2642-fe0f.png","sheet_x":9,"sheet_y":34,"short_name":"man-lifting-weights","short_names":["man-lifting-weights"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":479,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"1F3CB-1F3FB-200D-2642-FE0F","non_qualified":"1F3CB-1F3FB-200D-2642","image":"1f3cb-1f3fb-200d-2642-fe0f.png","sheet_x":9,"sheet_y":35,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F3CB-1F3FC-200D-2642-FE0F","non_qualified":"1F3CB-1F3FC-200D-2642","image":"1f3cb-1f3fc-200d-2642-fe0f.png","sheet_x":9,"sheet_y":36,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F3CB-1F3FD-200D-2642-FE0F","non_qualified":"1F3CB-1F3FD-200D-2642","image":"1f3cb-1f3fd-200d-2642-fe0f.png","sheet_x":9,"sheet_y":37,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F3CB-1F3FE-200D-2642-FE0F","non_qualified":"1F3CB-1F3FE-200D-2642","image":"1f3cb-1f3fe-200d-2642-fe0f.png","sheet_x":9,"sheet_y":38,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F3CB-1F3FF-200D-2642-FE0F","non_qualified":"1F3CB-1F3FF-200D-2642","image":"1f3cb-1f3ff-200d-2642-fe0f.png","sheet_x":9,"sheet_y":39,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F3CB-FE0F"},{"name":"PERSON LIFTING WEIGHTS","unified":"1F3CB-FE0F","non_qualified":"1F3CB","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3cb-fe0f.png","sheet_x":9,"sheet_y":40,"short_name":"weight_lifter","short_names":["weight_lifter"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":478,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F3CB-1F3FB","non_qualified":null,"image":"1f3cb-1f3fb.png","sheet_x":9,"sheet_y":41,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F3CB-1F3FC","non_qualified":null,"image":"1f3cb-1f3fc.png","sheet_x":9,"sheet_y":42,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F3CB-1F3FD","non_qualified":null,"image":"1f3cb-1f3fd.png","sheet_x":9,"sheet_y":43,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F3CB-1F3FE","non_qualified":null,"image":"1f3cb-1f3fe.png","sheet_x":9,"sheet_y":44,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F3CB-1F3FF","non_qualified":null,"image":"1f3cb-1f3ff.png","sheet_x":9,"sheet_y":45,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F3CB-FE0F-200D-2642-FE0F"},{"name":"WOMAN GOLFING","unified":"1F3CC-FE0F-200D-2640-FE0F","non_qualified":"1F3CC-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3cc-fe0f-200d-2640-fe0f.png","sheet_x":9,"sheet_y":46,"short_name":"woman-golfing","short_names":["woman-golfing"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":465,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"1F3CC-1F3FB-200D-2640-FE0F","non_qualified":"1F3CC-1F3FB-200D-2640","image":"1f3cc-1f3fb-200d-2640-fe0f.png","sheet_x":9,"sheet_y":47,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F3CC-1F3FC-200D-2640-FE0F","non_qualified":"1F3CC-1F3FC-200D-2640","image":"1f3cc-1f3fc-200d-2640-fe0f.png","sheet_x":9,"sheet_y":48,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F3CC-1F3FD-200D-2640-FE0F","non_qualified":"1F3CC-1F3FD-200D-2640","image":"1f3cc-1f3fd-200d-2640-fe0f.png","sheet_x":9,"sheet_y":49,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F3CC-1F3FE-200D-2640-FE0F","non_qualified":"1F3CC-1F3FE-200D-2640","image":"1f3cc-1f3fe-200d-2640-fe0f.png","sheet_x":9,"sheet_y":50,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F3CC-1F3FF-200D-2640-FE0F","non_qualified":"1F3CC-1F3FF-200D-2640","image":"1f3cc-1f3ff-200d-2640-fe0f.png","sheet_x":9,"sheet_y":51,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN GOLFING","unified":"1F3CC-FE0F-200D-2642-FE0F","non_qualified":"1F3CC-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3cc-fe0f-200d-2642-fe0f.png","sheet_x":9,"sheet_y":52,"short_name":"man-golfing","short_names":["man-golfing"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":464,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"1F3CC-1F3FB-200D-2642-FE0F","non_qualified":"1F3CC-1F3FB-200D-2642","image":"1f3cc-1f3fb-200d-2642-fe0f.png","sheet_x":9,"sheet_y":53,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F3CC-1F3FC-200D-2642-FE0F","non_qualified":"1F3CC-1F3FC-200D-2642","image":"1f3cc-1f3fc-200d-2642-fe0f.png","sheet_x":9,"sheet_y":54,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F3CC-1F3FD-200D-2642-FE0F","non_qualified":"1F3CC-1F3FD-200D-2642","image":"1f3cc-1f3fd-200d-2642-fe0f.png","sheet_x":9,"sheet_y":55,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F3CC-1F3FE-200D-2642-FE0F","non_qualified":"1F3CC-1F3FE-200D-2642","image":"1f3cc-1f3fe-200d-2642-fe0f.png","sheet_x":9,"sheet_y":56,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F3CC-1F3FF-200D-2642-FE0F","non_qualified":"1F3CC-1F3FF-200D-2642","image":"1f3cc-1f3ff-200d-2642-fe0f.png","sheet_x":9,"sheet_y":57,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F3CC-FE0F"},{"name":"PERSON GOLFING","unified":"1F3CC-FE0F","non_qualified":"1F3CC","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3cc-fe0f.png","sheet_x":9,"sheet_y":58,"short_name":"golfer","short_names":["golfer"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":463,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F3CC-1F3FB","non_qualified":null,"image":"1f3cc-1f3fb.png","sheet_x":9,"sheet_y":59,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F3CC-1F3FC","non_qualified":null,"image":"1f3cc-1f3fc.png","sheet_x":9,"sheet_y":60,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F3CC-1F3FD","non_qualified":null,"image":"1f3cc-1f3fd.png","sheet_x":9,"sheet_y":61,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F3CC-1F3FE","non_qualified":null,"image":"1f3cc-1f3fe.png","sheet_x":10,"sheet_y":0,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F3CC-1F3FF","non_qualified":null,"image":"1f3cc-1f3ff.png","sheet_x":10,"sheet_y":1,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F3CC-FE0F-200D-2642-FE0F"},{"name":"MOTORCYCLE","unified":"1F3CD-FE0F","non_qualified":"1F3CD","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3cd-fe0f.png","sheet_x":10,"sheet_y":2,"short_name":"racing_motorcycle","short_names":["racing_motorcycle"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":943,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RACING CAR","unified":"1F3CE-FE0F","non_qualified":"1F3CE","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3ce-fe0f.png","sheet_x":10,"sheet_y":3,"short_name":"racing_car","short_names":["racing_car"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":942,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CRICKET BAT AND BALL","unified":"1F3CF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3cf.png","sheet_x":10,"sheet_y":4,"short_name":"cricket_bat_and_ball","short_names":["cricket_bat_and_ball"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1102,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"VOLLEYBALL","unified":"1F3D0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3d0.png","sheet_x":10,"sheet_y":5,"short_name":"volleyball","short_names":["volleyball"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1096,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FIELD HOCKEY STICK AND BALL","unified":"1F3D1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3d1.png","sheet_x":10,"sheet_y":6,"short_name":"field_hockey_stick_and_ball","short_names":["field_hockey_stick_and_ball"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1103,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ICE HOCKEY STICK AND PUCK","unified":"1F3D2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3d2.png","sheet_x":10,"sheet_y":7,"short_name":"ice_hockey_stick_and_puck","short_names":["ice_hockey_stick_and_puck"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1104,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TABLE TENNIS PADDLE AND BALL","unified":"1F3D3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3d3.png","sheet_x":10,"sheet_y":8,"short_name":"table_tennis_paddle_and_ball","short_names":["table_tennis_paddle_and_ball"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1106,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SNOW-CAPPED MOUNTAIN","unified":"1F3D4-FE0F","non_qualified":"1F3D4","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3d4-fe0f.png","sheet_x":10,"sheet_y":9,"short_name":"snow_capped_mountain","short_names":["snow_capped_mountain"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-geographic","sort_order":854,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CAMPING","unified":"1F3D5-FE0F","non_qualified":"1F3D5","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3d5-fe0f.png","sheet_x":10,"sheet_y":10,"short_name":"camping","short_names":["camping"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-geographic","sort_order":858,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BEACH WITH UMBRELLA","unified":"1F3D6-FE0F","non_qualified":"1F3D6","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3d6-fe0f.png","sheet_x":10,"sheet_y":11,"short_name":"beach_with_umbrella","short_names":["beach_with_umbrella"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-geographic","sort_order":859,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BUILDING CONSTRUCTION","unified":"1F3D7-FE0F","non_qualified":"1F3D7","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3d7-fe0f.png","sheet_x":10,"sheet_y":12,"short_name":"building_construction","short_names":["building_construction"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":865,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HOUSES","unified":"1F3D8-FE0F","non_qualified":"1F3D8","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3d8-fe0f.png","sheet_x":10,"sheet_y":13,"short_name":"house_buildings","short_names":["house_buildings"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":870,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CITYSCAPE","unified":"1F3D9-FE0F","non_qualified":"1F3D9","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3d9-fe0f.png","sheet_x":10,"sheet_y":14,"short_name":"cityscape","short_names":["cityscape"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-other","sort_order":900,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DERELICT HOUSE","unified":"1F3DA-FE0F","non_qualified":"1F3DA","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3da-fe0f.png","sheet_x":10,"sheet_y":15,"short_name":"derelict_house_building","short_names":["derelict_house_building"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":871,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLASSICAL BUILDING","unified":"1F3DB-FE0F","non_qualified":"1F3DB","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3db-fe0f.png","sheet_x":10,"sheet_y":16,"short_name":"classical_building","short_names":["classical_building"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":864,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DESERT","unified":"1F3DC-FE0F","non_qualified":"1F3DC","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3dc-fe0f.png","sheet_x":10,"sheet_y":17,"short_name":"desert","short_names":["desert"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-geographic","sort_order":860,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DESERT ISLAND","unified":"1F3DD-FE0F","non_qualified":"1F3DD","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3dd-fe0f.png","sheet_x":10,"sheet_y":18,"short_name":"desert_island","short_names":["desert_island"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-geographic","sort_order":861,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NATIONAL PARK","unified":"1F3DE-FE0F","non_qualified":"1F3DE","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3de-fe0f.png","sheet_x":10,"sheet_y":19,"short_name":"national_park","short_names":["national_park"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-geographic","sort_order":862,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"STADIUM","unified":"1F3DF-FE0F","non_qualified":"1F3DF","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3df-fe0f.png","sheet_x":10,"sheet_y":20,"short_name":"stadium","short_names":["stadium"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":863,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HOUSE BUILDING","unified":"1F3E0","non_qualified":null,"docomo":"E663","au":"E4AB","softbank":"E036","google":"FE4B0","image":"1f3e0.png","sheet_x":10,"sheet_y":21,"short_name":"house","short_names":["house"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":872,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HOUSE WITH GARDEN","unified":"1F3E1","non_qualified":null,"docomo":"E663","au":"EB09","softbank":null,"google":"FE4B1","image":"1f3e1.png","sheet_x":10,"sheet_y":22,"short_name":"house_with_garden","short_names":["house_with_garden"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":873,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"OFFICE BUILDING","unified":"1F3E2","non_qualified":null,"docomo":"E664","au":"E4AD","softbank":"E038","google":"FE4B2","image":"1f3e2.png","sheet_x":10,"sheet_y":23,"short_name":"office","short_names":["office"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":874,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"JAPANESE POST OFFICE","unified":"1F3E3","non_qualified":null,"docomo":"E665","au":"E5DE","softbank":"E153","google":"FE4B3","image":"1f3e3.png","sheet_x":10,"sheet_y":24,"short_name":"post_office","short_names":["post_office"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":875,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EUROPEAN POST OFFICE","unified":"1F3E4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3e4.png","sheet_x":10,"sheet_y":25,"short_name":"european_post_office","short_names":["european_post_office"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":876,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HOSPITAL","unified":"1F3E5","non_qualified":null,"docomo":"E666","au":"E5DF","softbank":"E155","google":"FE4B4","image":"1f3e5.png","sheet_x":10,"sheet_y":26,"short_name":"hospital","short_names":["hospital"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":877,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BANK","unified":"1F3E6","non_qualified":null,"docomo":"E667","au":"E4AA","softbank":"E14D","google":"FE4B5","image":"1f3e6.png","sheet_x":10,"sheet_y":27,"short_name":"bank","short_names":["bank"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":878,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"AUTOMATED TELLER MACHINE","unified":"1F3E7","non_qualified":null,"docomo":"E668","au":"E4A3","softbank":"E154","google":"FE4B6","image":"1f3e7.png","sheet_x":10,"sheet_y":28,"short_name":"atm","short_names":["atm"],"text":null,"texts":null,"category":"Symbols","subcategory":"transport-sign","sort_order":1412,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HOTEL","unified":"1F3E8","non_qualified":null,"docomo":"E669","au":"EA81","softbank":"E158","google":"FE4B7","image":"1f3e8.png","sheet_x":10,"sheet_y":29,"short_name":"hotel","short_names":["hotel"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":879,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LOVE HOTEL","unified":"1F3E9","non_qualified":null,"docomo":"E669-E6EF","au":"EAF3","softbank":"E501","google":"FE4B8","image":"1f3e9.png","sheet_x":10,"sheet_y":30,"short_name":"love_hotel","short_names":["love_hotel"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":880,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CONVENIENCE STORE","unified":"1F3EA","non_qualified":null,"docomo":"E66A","au":"E4A4","softbank":"E156","google":"FE4B9","image":"1f3ea.png","sheet_x":10,"sheet_y":31,"short_name":"convenience_store","short_names":["convenience_store"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":881,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SCHOOL","unified":"1F3EB","non_qualified":null,"docomo":"E73E","au":"EA80","softbank":"E157","google":"FE4BA","image":"1f3eb.png","sheet_x":10,"sheet_y":32,"short_name":"school","short_names":["school"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":882,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DEPARTMENT STORE","unified":"1F3EC","non_qualified":null,"docomo":null,"au":"EAF6","softbank":"E504","google":"FE4BD","image":"1f3ec.png","sheet_x":10,"sheet_y":33,"short_name":"department_store","short_names":["department_store"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":883,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACTORY","unified":"1F3ED","non_qualified":null,"docomo":null,"au":"EAF9","softbank":"E508","google":"FE4C0","image":"1f3ed.png","sheet_x":10,"sheet_y":34,"short_name":"factory","short_names":["factory"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":884,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"IZAKAYA LANTERN","unified":"1F3EE","non_qualified":null,"docomo":"E74B","au":"E4BD","softbank":null,"google":"FE4C2","image":"1f3ee.png","sheet_x":10,"sheet_y":35,"short_name":"izakaya_lantern","short_names":["izakaya_lantern","lantern"],"text":null,"texts":null,"category":"Objects","subcategory":"light & video","sort_order":1260,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"JAPANESE CASTLE","unified":"1F3EF","non_qualified":null,"docomo":null,"au":"EAF7","softbank":"E505","google":"FE4BE","image":"1f3ef.png","sheet_x":10,"sheet_y":36,"short_name":"japanese_castle","short_names":["japanese_castle"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":885,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EUROPEAN CASTLE","unified":"1F3F0","non_qualified":null,"docomo":null,"au":"EAF8","softbank":"E506","google":"FE4BF","image":"1f3f0.png","sheet_x":10,"sheet_y":37,"short_name":"european_castle","short_names":["european_castle"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":886,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RAINBOW FLAG","unified":"1F3F3-FE0F-200D-1F308","non_qualified":"1F3F3-200D-1F308","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3f3-fe0f-200d-1f308.png","sheet_x":10,"sheet_y":38,"short_name":"rainbow-flag","short_names":["rainbow-flag"],"text":null,"texts":null,"category":"Flags","subcategory":"flag","sort_order":1640,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TRANSGENDER FLAG","unified":"1F3F3-FE0F-200D-26A7-FE0F","non_qualified":"1F3F3-200D-26A7","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3f3-fe0f-200d-26a7-fe0f.png","sheet_x":10,"sheet_y":39,"short_name":"transgender_flag","short_names":["transgender_flag"],"text":null,"texts":null,"category":"Flags","subcategory":"flag","sort_order":1641,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"WHITE FLAG","unified":"1F3F3-FE0F","non_qualified":"1F3F3","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3f3-fe0f.png","sheet_x":10,"sheet_y":40,"short_name":"waving_white_flag","short_names":["waving_white_flag"],"text":null,"texts":null,"category":"Flags","subcategory":"flag","sort_order":1639,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PIRATE FLAG","unified":"1F3F4-200D-2620-FE0F","non_qualified":"1F3F4-200D-2620","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3f4-200d-2620-fe0f.png","sheet_x":10,"sheet_y":41,"short_name":"pirate_flag","short_names":["pirate_flag"],"text":null,"texts":null,"category":"Flags","subcategory":"flag","sort_order":1642,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"England Flag","unified":"1F3F4-E0067-E0062-E0065-E006E-E0067-E007F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3f4-e0067-e0062-e0065-e006e-e0067-e007f.png","sheet_x":10,"sheet_y":42,"short_name":"flag-england","short_names":["flag-england"],"text":null,"texts":null,"category":"Flags","subcategory":"subdivision-flag","sort_order":1901,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Scotland Flag","unified":"1F3F4-E0067-E0062-E0073-E0063-E0074-E007F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3f4-e0067-e0062-e0073-e0063-e0074-e007f.png","sheet_x":10,"sheet_y":43,"short_name":"flag-scotland","short_names":["flag-scotland"],"text":null,"texts":null,"category":"Flags","subcategory":"subdivision-flag","sort_order":1902,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"Wales Flag","unified":"1F3F4-E0067-E0062-E0077-E006C-E0073-E007F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3f4-e0067-e0062-e0077-e006c-e0073-e007f.png","sheet_x":10,"sheet_y":44,"short_name":"flag-wales","short_names":["flag-wales"],"text":null,"texts":null,"category":"Flags","subcategory":"subdivision-flag","sort_order":1903,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WAVING BLACK FLAG","unified":"1F3F4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3f4.png","sheet_x":10,"sheet_y":45,"short_name":"waving_black_flag","short_names":["waving_black_flag"],"text":null,"texts":null,"category":"Flags","subcategory":"flag","sort_order":1638,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ROSETTE","unified":"1F3F5-FE0F","non_qualified":"1F3F5","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3f5-fe0f.png","sheet_x":10,"sheet_y":46,"short_name":"rosette","short_names":["rosette"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-flower","sort_order":688,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LABEL","unified":"1F3F7-FE0F","non_qualified":"1F3F7","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3f7-fe0f.png","sheet_x":10,"sheet_y":47,"short_name":"label","short_names":["label"],"text":null,"texts":null,"category":"Objects","subcategory":"book-paper","sort_order":1278,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BADMINTON RACQUET AND SHUTTLECOCK","unified":"1F3F8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3f8.png","sheet_x":10,"sheet_y":48,"short_name":"badminton_racquet_and_shuttlecock","short_names":["badminton_racquet_and_shuttlecock"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1107,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BOW AND ARROW","unified":"1F3F9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3f9.png","sheet_x":10,"sheet_y":49,"short_name":"bow_and_arrow","short_names":["bow_and_arrow"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1347,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"AMPHORA","unified":"1F3FA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3fa.png","sheet_x":10,"sheet_y":50,"short_name":"amphora","short_names":["amphora"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"dishware","sort_order":846,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EMOJI MODIFIER FITZPATRICK TYPE-1-2","unified":"1F3FB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3fb.png","sheet_x":10,"sheet_y":51,"short_name":"skin-tone-2","short_names":["skin-tone-2"],"text":null,"texts":null,"category":"Component","subcategory":"skin-tone","sort_order":554,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EMOJI MODIFIER FITZPATRICK TYPE-3","unified":"1F3FC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3fc.png","sheet_x":10,"sheet_y":52,"short_name":"skin-tone-3","short_names":["skin-tone-3"],"text":null,"texts":null,"category":"Component","subcategory":"skin-tone","sort_order":555,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EMOJI MODIFIER FITZPATRICK TYPE-4","unified":"1F3FD","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3fd.png","sheet_x":10,"sheet_y":53,"short_name":"skin-tone-4","short_names":["skin-tone-4"],"text":null,"texts":null,"category":"Component","subcategory":"skin-tone","sort_order":556,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EMOJI MODIFIER FITZPATRICK TYPE-5","unified":"1F3FE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3fe.png","sheet_x":10,"sheet_y":54,"short_name":"skin-tone-5","short_names":["skin-tone-5"],"text":null,"texts":null,"category":"Component","subcategory":"skin-tone","sort_order":557,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EMOJI MODIFIER FITZPATRICK TYPE-6","unified":"1F3FF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f3ff.png","sheet_x":10,"sheet_y":55,"short_name":"skin-tone-6","short_names":["skin-tone-6"],"text":null,"texts":null,"category":"Component","subcategory":"skin-tone","sort_order":558,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RAT","unified":"1F400","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f400.png","sheet_x":10,"sheet_y":56,"short_name":"rat","short_names":["rat"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":607,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MOUSE","unified":"1F401","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f401.png","sheet_x":10,"sheet_y":57,"short_name":"mouse2","short_names":["mouse2"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":606,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"OX","unified":"1F402","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f402.png","sheet_x":10,"sheet_y":58,"short_name":"ox","short_names":["ox"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":587,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WATER BUFFALO","unified":"1F403","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f403.png","sheet_x":10,"sheet_y":59,"short_name":"water_buffalo","short_names":["water_buffalo"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":588,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"COW","unified":"1F404","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f404.png","sheet_x":10,"sheet_y":60,"short_name":"cow2","short_names":["cow2"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":589,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TIGER","unified":"1F405","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f405.png","sheet_x":10,"sheet_y":61,"short_name":"tiger2","short_names":["tiger2"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":576,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LEOPARD","unified":"1F406","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f406.png","sheet_x":11,"sheet_y":0,"short_name":"leopard","short_names":["leopard"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":577,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RABBIT","unified":"1F407","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f407.png","sheet_x":11,"sheet_y":1,"short_name":"rabbit2","short_names":["rabbit2"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":610,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK CAT","unified":"1F408-200D-2B1B","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f408-200d-2b1b.png","sheet_x":11,"sheet_y":2,"short_name":"black_cat","short_names":["black_cat"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":573,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CAT","unified":"1F408","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f408.png","sheet_x":11,"sheet_y":3,"short_name":"cat2","short_names":["cat2"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":572,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DRAGON","unified":"1F409","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f409.png","sheet_x":11,"sheet_y":4,"short_name":"dragon","short_names":["dragon"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-reptile","sort_order":653,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CROCODILE","unified":"1F40A","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f40a.png","sheet_x":11,"sheet_y":5,"short_name":"crocodile","short_names":["crocodile"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-reptile","sort_order":648,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WHALE","unified":"1F40B","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f40b.png","sheet_x":11,"sheet_y":6,"short_name":"whale2","short_names":["whale2"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-marine","sort_order":657,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SNAIL","unified":"1F40C","non_qualified":null,"docomo":"E74E","au":"EB7E","softbank":null,"google":"FE1B9","image":"1f40c.png","sheet_x":11,"sheet_y":7,"short_name":"snail","short_names":["snail"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bug","sort_order":668,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SNAKE","unified":"1F40D","non_qualified":null,"docomo":null,"au":"EB22","softbank":"E52D","google":"FE1D3","image":"1f40d.png","sheet_x":11,"sheet_y":8,"short_name":"snake","short_names":["snake"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-reptile","sort_order":651,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HORSE","unified":"1F40E","non_qualified":null,"docomo":"E754","au":"E4D8","softbank":"E134","google":"FE7DC","image":"1f40e.png","sheet_x":11,"sheet_y":9,"short_name":"racehorse","short_names":["racehorse"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":581,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RAM","unified":"1F40F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f40f.png","sheet_x":11,"sheet_y":10,"short_name":"ram","short_names":["ram"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":594,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GOAT","unified":"1F410","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f410.png","sheet_x":11,"sheet_y":11,"short_name":"goat","short_names":["goat"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":596,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SHEEP","unified":"1F411","non_qualified":null,"docomo":null,"au":"E48F","softbank":"E529","google":"FE1CF","image":"1f411.png","sheet_x":11,"sheet_y":12,"short_name":"sheep","short_names":["sheep"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":595,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MONKEY","unified":"1F412","non_qualified":null,"docomo":null,"au":"E4D9","softbank":"E528","google":"FE1CE","image":"1f412.png","sheet_x":11,"sheet_y":13,"short_name":"monkey","short_names":["monkey"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":560,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ROOSTER","unified":"1F413","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f413.png","sheet_x":11,"sheet_y":14,"short_name":"rooster","short_names":["rooster"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bird","sort_order":627,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CHICKEN","unified":"1F414","non_qualified":null,"docomo":null,"au":"EB23","softbank":"E52E","google":"FE1D4","image":"1f414.png","sheet_x":11,"sheet_y":15,"short_name":"chicken","short_names":["chicken"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bird","sort_order":626,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SERVICE DOG","unified":"1F415-200D-1F9BA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f415-200d-1f9ba.png","sheet_x":11,"sheet_y":16,"short_name":"service_dog","short_names":["service_dog"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":566,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DOG","unified":"1F415","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f415.png","sheet_x":11,"sheet_y":17,"short_name":"dog2","short_names":["dog2"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":564,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PIG","unified":"1F416","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f416.png","sheet_x":11,"sheet_y":18,"short_name":"pig2","short_names":["pig2"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":591,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BOAR","unified":"1F417","non_qualified":null,"docomo":null,"au":"EB24","softbank":"E52F","google":"FE1D5","image":"1f417.png","sheet_x":11,"sheet_y":19,"short_name":"boar","short_names":["boar"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":592,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ELEPHANT","unified":"1F418","non_qualified":null,"docomo":null,"au":"EB1F","softbank":"E526","google":"FE1CC","image":"1f418.png","sheet_x":11,"sheet_y":20,"short_name":"elephant","short_names":["elephant"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":601,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"OCTOPUS","unified":"1F419","non_qualified":null,"docomo":null,"au":"E5C7","softbank":"E10A","google":"FE1C5","image":"1f419.png","sheet_x":11,"sheet_y":21,"short_name":"octopus","short_names":["octopus"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-marine","sort_order":664,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SPIRAL SHELL","unified":"1F41A","non_qualified":null,"docomo":null,"au":"EAEC","softbank":"E441","google":"FE1C6","image":"1f41a.png","sheet_x":11,"sheet_y":22,"short_name":"shell","short_names":["shell"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-marine","sort_order":665,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BUG","unified":"1F41B","non_qualified":null,"docomo":null,"au":"EB1E","softbank":"E525","google":"FE1CB","image":"1f41b.png","sheet_x":11,"sheet_y":23,"short_name":"bug","short_names":["bug"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bug","sort_order":670,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ANT","unified":"1F41C","non_qualified":null,"docomo":null,"au":"E4DD","softbank":null,"google":"FE1DA","image":"1f41c.png","sheet_x":11,"sheet_y":24,"short_name":"ant","short_names":["ant"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bug","sort_order":671,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HONEYBEE","unified":"1F41D","non_qualified":null,"docomo":null,"au":"EB57","softbank":null,"google":"FE1E1","image":"1f41d.png","sheet_x":11,"sheet_y":25,"short_name":"bee","short_names":["bee","honeybee"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bug","sort_order":672,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LADY BEETLE","unified":"1F41E","non_qualified":null,"docomo":null,"au":"EB58","softbank":null,"google":"FE1E2","image":"1f41e.png","sheet_x":11,"sheet_y":26,"short_name":"ladybug","short_names":["ladybug","lady_beetle"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bug","sort_order":674,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FISH","unified":"1F41F","non_qualified":null,"docomo":"E751","au":"E49A","softbank":"E019","google":"FE1BD","image":"1f41f.png","sheet_x":11,"sheet_y":27,"short_name":"fish","short_names":["fish"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-marine","sort_order":660,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TROPICAL FISH","unified":"1F420","non_qualified":null,"docomo":"E751","au":"EB1D","softbank":"E522","google":"FE1C9","image":"1f420.png","sheet_x":11,"sheet_y":28,"short_name":"tropical_fish","short_names":["tropical_fish"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-marine","sort_order":661,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLOWFISH","unified":"1F421","non_qualified":null,"docomo":"E751","au":"E4D3","softbank":null,"google":"FE1D9","image":"1f421.png","sheet_x":11,"sheet_y":29,"short_name":"blowfish","short_names":["blowfish"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-marine","sort_order":662,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TURTLE","unified":"1F422","non_qualified":null,"docomo":null,"au":"E5D4","softbank":null,"google":"FE1DC","image":"1f422.png","sheet_x":11,"sheet_y":30,"short_name":"turtle","short_names":["turtle"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-reptile","sort_order":649,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HATCHING CHICK","unified":"1F423","non_qualified":null,"docomo":"E74F","au":"E5DB","softbank":null,"google":"FE1DD","image":"1f423.png","sheet_x":11,"sheet_y":31,"short_name":"hatching_chick","short_names":["hatching_chick"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bird","sort_order":628,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BABY CHICK","unified":"1F424","non_qualified":null,"docomo":"E74F","au":"E4E0","softbank":"E523","google":"FE1BA","image":"1f424.png","sheet_x":11,"sheet_y":32,"short_name":"baby_chick","short_names":["baby_chick"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bird","sort_order":629,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FRONT-FACING BABY CHICK","unified":"1F425","non_qualified":null,"docomo":"E74F","au":"EB76","softbank":null,"google":"FE1BB","image":"1f425.png","sheet_x":11,"sheet_y":33,"short_name":"hatched_chick","short_names":["hatched_chick"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bird","sort_order":630,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PHOENIX","unified":"1F426-200D-1F525","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f426-200d-1f525.png","sheet_x":11,"sheet_y":34,"short_name":"phoenix","short_names":["phoenix"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bird","sort_order":646,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},{"name":"BLACK BIRD","unified":"1F426-200D-2B1B","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f426-200d-2b1b.png","sheet_x":11,"sheet_y":35,"short_name":"black_bird","short_names":["black_bird"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bird","sort_order":644,"added_in":"15.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BIRD","unified":"1F426","non_qualified":null,"docomo":"E74F","au":"E4E0","softbank":"E521","google":"FE1C8","image":"1f426.png","sheet_x":11,"sheet_y":36,"short_name":"bird","short_names":["bird"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bird","sort_order":631,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PENGUIN","unified":"1F427","non_qualified":null,"docomo":"E750","au":"E4DC","softbank":"E055","google":"FE1BC","image":"1f427.png","sheet_x":11,"sheet_y":37,"short_name":"penguin","short_names":["penguin"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bird","sort_order":632,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"KOALA","unified":"1F428","non_qualified":null,"docomo":null,"au":"EB20","softbank":"E527","google":"FE1CD","image":"1f428.png","sheet_x":11,"sheet_y":38,"short_name":"koala","short_names":["koala"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":617,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"POODLE","unified":"1F429","non_qualified":null,"docomo":"E6A1","au":"E4DF","softbank":null,"google":"FE1D8","image":"1f429.png","sheet_x":11,"sheet_y":39,"short_name":"poodle","short_names":["poodle"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":567,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DROMEDARY CAMEL","unified":"1F42A","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f42a.png","sheet_x":11,"sheet_y":40,"short_name":"dromedary_camel","short_names":["dromedary_camel"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":597,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BACTRIAN CAMEL","unified":"1F42B","non_qualified":null,"docomo":null,"au":"EB25","softbank":"E530","google":"FE1D6","image":"1f42b.png","sheet_x":11,"sheet_y":41,"short_name":"camel","short_names":["camel"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":598,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DOLPHIN","unified":"1F42C","non_qualified":null,"docomo":null,"au":"EB1B","softbank":"E520","google":"FE1C7","image":"1f42c.png","sheet_x":11,"sheet_y":42,"short_name":"dolphin","short_names":["dolphin","flipper"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-marine","sort_order":658,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MOUSE FACE","unified":"1F42D","non_qualified":null,"docomo":null,"au":"E5C2","softbank":"E053","google":"FE1C2","image":"1f42d.png","sheet_x":11,"sheet_y":43,"short_name":"mouse","short_names":["mouse"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":605,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"COW FACE","unified":"1F42E","non_qualified":null,"docomo":null,"au":"EB21","softbank":"E52B","google":"FE1D1","image":"1f42e.png","sheet_x":11,"sheet_y":44,"short_name":"cow","short_names":["cow"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":586,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TIGER FACE","unified":"1F42F","non_qualified":null,"docomo":null,"au":"E5C0","softbank":"E050","google":"FE1C0","image":"1f42f.png","sheet_x":11,"sheet_y":45,"short_name":"tiger","short_names":["tiger"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":575,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RABBIT FACE","unified":"1F430","non_qualified":null,"docomo":null,"au":"E4D7","softbank":"E52C","google":"FE1D2","image":"1f430.png","sheet_x":11,"sheet_y":46,"short_name":"rabbit","short_names":["rabbit"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":609,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CAT FACE","unified":"1F431","non_qualified":null,"docomo":"E6A2","au":"E4DB","softbank":"E04F","google":"FE1B8","image":"1f431.png","sheet_x":11,"sheet_y":47,"short_name":"cat","short_names":["cat"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":571,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DRAGON FACE","unified":"1F432","non_qualified":null,"docomo":null,"au":"EB3F","softbank":null,"google":"FE1DE","image":"1f432.png","sheet_x":11,"sheet_y":48,"short_name":"dragon_face","short_names":["dragon_face"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-reptile","sort_order":652,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SPOUTING WHALE","unified":"1F433","non_qualified":null,"docomo":null,"au":"E470","softbank":"E054","google":"FE1C3","image":"1f433.png","sheet_x":11,"sheet_y":49,"short_name":"whale","short_names":["whale"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-marine","sort_order":656,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HORSE FACE","unified":"1F434","non_qualified":null,"docomo":"E754","au":"E4D8","softbank":"E01A","google":"FE1BE","image":"1f434.png","sheet_x":11,"sheet_y":50,"short_name":"horse","short_names":["horse"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":578,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MONKEY FACE","unified":"1F435","non_qualified":null,"docomo":null,"au":"E4D9","softbank":"E109","google":"FE1C4","image":"1f435.png","sheet_x":11,"sheet_y":51,"short_name":"monkey_face","short_names":["monkey_face"],"text":null,"texts":[":o)"],"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":559,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DOG FACE","unified":"1F436","non_qualified":null,"docomo":"E6A1","au":"E4E1","softbank":"E052","google":"FE1B7","image":"1f436.png","sheet_x":11,"sheet_y":52,"short_name":"dog","short_names":["dog"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":563,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PIG FACE","unified":"1F437","non_qualified":null,"docomo":"E755","au":"E4DE","softbank":"E10B","google":"FE1BF","image":"1f437.png","sheet_x":11,"sheet_y":53,"short_name":"pig","short_names":["pig"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":590,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FROG FACE","unified":"1F438","non_qualified":null,"docomo":null,"au":"E4DA","softbank":"E531","google":"FE1D7","image":"1f438.png","sheet_x":11,"sheet_y":54,"short_name":"frog","short_names":["frog"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-amphibian","sort_order":647,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HAMSTER FACE","unified":"1F439","non_qualified":null,"docomo":null,"au":null,"softbank":"E524","google":"FE1CA","image":"1f439.png","sheet_x":11,"sheet_y":55,"short_name":"hamster","short_names":["hamster"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":608,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WOLF FACE","unified":"1F43A","non_qualified":null,"docomo":"E6A1","au":"E4E1","softbank":"E52A","google":"FE1D0","image":"1f43a.png","sheet_x":11,"sheet_y":56,"short_name":"wolf","short_names":["wolf"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":568,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"POLAR BEAR","unified":"1F43B-200D-2744-FE0F","non_qualified":"1F43B-200D-2744","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f43b-200d-2744-fe0f.png","sheet_x":11,"sheet_y":57,"short_name":"polar_bear","short_names":["polar_bear"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":616,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BEAR FACE","unified":"1F43B","non_qualified":null,"docomo":null,"au":"E5C1","softbank":"E051","google":"FE1C1","image":"1f43b.png","sheet_x":11,"sheet_y":58,"short_name":"bear","short_names":["bear"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":615,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PANDA FACE","unified":"1F43C","non_qualified":null,"docomo":null,"au":"EB46","softbank":null,"google":"FE1DF","image":"1f43c.png","sheet_x":11,"sheet_y":59,"short_name":"panda_face","short_names":["panda_face"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":618,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PIG NOSE","unified":"1F43D","non_qualified":null,"docomo":"E755","au":"EB48","softbank":null,"google":"FE1E0","image":"1f43d.png","sheet_x":11,"sheet_y":60,"short_name":"pig_nose","short_names":["pig_nose"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":593,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PAW PRINTS","unified":"1F43E","non_qualified":null,"docomo":"E698","au":"E4EE","softbank":null,"google":"FE1DB","image":"1f43e.png","sheet_x":11,"sheet_y":61,"short_name":"feet","short_names":["feet","paw_prints"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":624,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CHIPMUNK","unified":"1F43F-FE0F","non_qualified":"1F43F","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f43f-fe0f.png","sheet_x":12,"sheet_y":0,"short_name":"chipmunk","short_names":["chipmunk"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":611,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EYES","unified":"1F440","non_qualified":null,"docomo":"E691","au":"E5A4","softbank":"E419","google":"FE190","image":"1f440.png","sheet_x":12,"sheet_y":1,"short_name":"eyes","short_names":["eyes"],"text":null,"texts":null,"category":"People & Body","subcategory":"body-parts","sort_order":225,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EYE IN SPEECH BUBBLE","unified":"1F441-FE0F-200D-1F5E8-FE0F","non_qualified":"1F441-200D-1F5E8","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f441-fe0f-200d-1f5e8-fe0f.png","sheet_x":12,"sheet_y":2,"short_name":"eye-in-speech-bubble","short_names":["eye-in-speech-bubble"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":164,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false},{"name":"EYE","unified":"1F441-FE0F","non_qualified":"1F441","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f441-fe0f.png","sheet_x":12,"sheet_y":3,"short_name":"eye","short_names":["eye"],"text":null,"texts":null,"category":"People & Body","subcategory":"body-parts","sort_order":226,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EAR","unified":"1F442","non_qualified":null,"docomo":"E692","au":"E5A5","softbank":"E41B","google":"FE191","image":"1f442.png","sheet_x":12,"sheet_y":4,"short_name":"ear","short_names":["ear"],"text":null,"texts":null,"category":"People & Body","subcategory":"body-parts","sort_order":217,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F442-1F3FB","non_qualified":null,"image":"1f442-1f3fb.png","sheet_x":12,"sheet_y":5,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F442-1F3FC","non_qualified":null,"image":"1f442-1f3fc.png","sheet_x":12,"sheet_y":6,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F442-1F3FD","non_qualified":null,"image":"1f442-1f3fd.png","sheet_x":12,"sheet_y":7,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F442-1F3FE","non_qualified":null,"image":"1f442-1f3fe.png","sheet_x":12,"sheet_y":8,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F442-1F3FF","non_qualified":null,"image":"1f442-1f3ff.png","sheet_x":12,"sheet_y":9,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"NOSE","unified":"1F443","non_qualified":null,"docomo":null,"au":"EAD0","softbank":"E41A","google":"FE192","image":"1f443.png","sheet_x":12,"sheet_y":10,"short_name":"nose","short_names":["nose"],"text":null,"texts":null,"category":"People & Body","subcategory":"body-parts","sort_order":219,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F443-1F3FB","non_qualified":null,"image":"1f443-1f3fb.png","sheet_x":12,"sheet_y":11,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F443-1F3FC","non_qualified":null,"image":"1f443-1f3fc.png","sheet_x":12,"sheet_y":12,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F443-1F3FD","non_qualified":null,"image":"1f443-1f3fd.png","sheet_x":12,"sheet_y":13,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F443-1F3FE","non_qualified":null,"image":"1f443-1f3fe.png","sheet_x":12,"sheet_y":14,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F443-1F3FF","non_qualified":null,"image":"1f443-1f3ff.png","sheet_x":12,"sheet_y":15,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MOUTH","unified":"1F444","non_qualified":null,"docomo":"E6F9","au":"EAD1","softbank":"E41C","google":"FE193","image":"1f444.png","sheet_x":12,"sheet_y":16,"short_name":"lips","short_names":["lips"],"text":null,"texts":null,"category":"People & Body","subcategory":"body-parts","sort_order":228,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TONGUE","unified":"1F445","non_qualified":null,"docomo":"E728","au":"EB47","softbank":null,"google":"FE194","image":"1f445.png","sheet_x":12,"sheet_y":17,"short_name":"tongue","short_names":["tongue"],"text":null,"texts":null,"category":"People & Body","subcategory":"body-parts","sort_order":227,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WHITE UP POINTING BACKHAND INDEX","unified":"1F446","non_qualified":null,"docomo":null,"au":"EA8D","softbank":"E22E","google":"FEB99","image":"1f446.png","sheet_x":12,"sheet_y":18,"short_name":"point_up_2","short_names":["point_up_2"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-single-finger","sort_order":191,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F446-1F3FB","non_qualified":null,"image":"1f446-1f3fb.png","sheet_x":12,"sheet_y":19,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F446-1F3FC","non_qualified":null,"image":"1f446-1f3fc.png","sheet_x":12,"sheet_y":20,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F446-1F3FD","non_qualified":null,"image":"1f446-1f3fd.png","sheet_x":12,"sheet_y":21,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F446-1F3FE","non_qualified":null,"image":"1f446-1f3fe.png","sheet_x":12,"sheet_y":22,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F446-1F3FF","non_qualified":null,"image":"1f446-1f3ff.png","sheet_x":12,"sheet_y":23,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WHITE DOWN POINTING BACKHAND INDEX","unified":"1F447","non_qualified":null,"docomo":null,"au":"EA8E","softbank":"E22F","google":"FEB9A","image":"1f447.png","sheet_x":12,"sheet_y":24,"short_name":"point_down","short_names":["point_down"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-single-finger","sort_order":193,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F447-1F3FB","non_qualified":null,"image":"1f447-1f3fb.png","sheet_x":12,"sheet_y":25,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F447-1F3FC","non_qualified":null,"image":"1f447-1f3fc.png","sheet_x":12,"sheet_y":26,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F447-1F3FD","non_qualified":null,"image":"1f447-1f3fd.png","sheet_x":12,"sheet_y":27,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F447-1F3FE","non_qualified":null,"image":"1f447-1f3fe.png","sheet_x":12,"sheet_y":28,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F447-1F3FF","non_qualified":null,"image":"1f447-1f3ff.png","sheet_x":12,"sheet_y":29,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WHITE LEFT POINTING BACKHAND INDEX","unified":"1F448","non_qualified":null,"docomo":null,"au":"E4FF","softbank":"E230","google":"FEB9B","image":"1f448.png","sheet_x":12,"sheet_y":30,"short_name":"point_left","short_names":["point_left"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-single-finger","sort_order":189,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F448-1F3FB","non_qualified":null,"image":"1f448-1f3fb.png","sheet_x":12,"sheet_y":31,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F448-1F3FC","non_qualified":null,"image":"1f448-1f3fc.png","sheet_x":12,"sheet_y":32,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F448-1F3FD","non_qualified":null,"image":"1f448-1f3fd.png","sheet_x":12,"sheet_y":33,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F448-1F3FE","non_qualified":null,"image":"1f448-1f3fe.png","sheet_x":12,"sheet_y":34,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F448-1F3FF","non_qualified":null,"image":"1f448-1f3ff.png","sheet_x":12,"sheet_y":35,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WHITE RIGHT POINTING BACKHAND INDEX","unified":"1F449","non_qualified":null,"docomo":null,"au":"E500","softbank":"E231","google":"FEB9C","image":"1f449.png","sheet_x":12,"sheet_y":36,"short_name":"point_right","short_names":["point_right"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-single-finger","sort_order":190,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F449-1F3FB","non_qualified":null,"image":"1f449-1f3fb.png","sheet_x":12,"sheet_y":37,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F449-1F3FC","non_qualified":null,"image":"1f449-1f3fc.png","sheet_x":12,"sheet_y":38,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F449-1F3FD","non_qualified":null,"image":"1f449-1f3fd.png","sheet_x":12,"sheet_y":39,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F449-1F3FE","non_qualified":null,"image":"1f449-1f3fe.png","sheet_x":12,"sheet_y":40,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F449-1F3FF","non_qualified":null,"image":"1f449-1f3ff.png","sheet_x":12,"sheet_y":41,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"FISTED HAND SIGN","unified":"1F44A","non_qualified":null,"docomo":"E6FD","au":"E4F3","softbank":"E00D","google":"FEB96","image":"1f44a.png","sheet_x":12,"sheet_y":42,"short_name":"facepunch","short_names":["facepunch","punch"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-closed","sort_order":199,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F44A-1F3FB","non_qualified":null,"image":"1f44a-1f3fb.png","sheet_x":12,"sheet_y":43,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F44A-1F3FC","non_qualified":null,"image":"1f44a-1f3fc.png","sheet_x":12,"sheet_y":44,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F44A-1F3FD","non_qualified":null,"image":"1f44a-1f3fd.png","sheet_x":12,"sheet_y":45,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F44A-1F3FE","non_qualified":null,"image":"1f44a-1f3fe.png","sheet_x":12,"sheet_y":46,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F44A-1F3FF","non_qualified":null,"image":"1f44a-1f3ff.png","sheet_x":12,"sheet_y":47,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WAVING HAND SIGN","unified":"1F44B","non_qualified":null,"docomo":"E695","au":"EAD6","softbank":"E41E","google":"FEB9D","image":"1f44b.png","sheet_x":12,"sheet_y":48,"short_name":"wave","short_names":["wave"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-open","sort_order":169,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F44B-1F3FB","non_qualified":null,"image":"1f44b-1f3fb.png","sheet_x":12,"sheet_y":49,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F44B-1F3FC","non_qualified":null,"image":"1f44b-1f3fc.png","sheet_x":12,"sheet_y":50,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F44B-1F3FD","non_qualified":null,"image":"1f44b-1f3fd.png","sheet_x":12,"sheet_y":51,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F44B-1F3FE","non_qualified":null,"image":"1f44b-1f3fe.png","sheet_x":12,"sheet_y":52,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F44B-1F3FF","non_qualified":null,"image":"1f44b-1f3ff.png","sheet_x":12,"sheet_y":53,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"OK HAND SIGN","unified":"1F44C","non_qualified":null,"docomo":"E70B","au":"EAD4","softbank":"E420","google":"FEB9F","image":"1f44c.png","sheet_x":12,"sheet_y":54,"short_name":"ok_hand","short_names":["ok_hand"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-partial","sort_order":180,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F44C-1F3FB","non_qualified":null,"image":"1f44c-1f3fb.png","sheet_x":12,"sheet_y":55,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F44C-1F3FC","non_qualified":null,"image":"1f44c-1f3fc.png","sheet_x":12,"sheet_y":56,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F44C-1F3FD","non_qualified":null,"image":"1f44c-1f3fd.png","sheet_x":12,"sheet_y":57,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F44C-1F3FE","non_qualified":null,"image":"1f44c-1f3fe.png","sheet_x":12,"sheet_y":58,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F44C-1F3FF","non_qualified":null,"image":"1f44c-1f3ff.png","sheet_x":12,"sheet_y":59,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"THUMBS UP SIGN","unified":"1F44D","non_qualified":null,"docomo":"E727","au":"E4F9","softbank":"E00E","google":"FEB97","image":"1f44d.png","sheet_x":12,"sheet_y":60,"short_name":"+1","short_names":["+1","thumbsup"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-closed","sort_order":196,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F44D-1F3FB","non_qualified":null,"image":"1f44d-1f3fb.png","sheet_x":12,"sheet_y":61,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F44D-1F3FC","non_qualified":null,"image":"1f44d-1f3fc.png","sheet_x":13,"sheet_y":0,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F44D-1F3FD","non_qualified":null,"image":"1f44d-1f3fd.png","sheet_x":13,"sheet_y":1,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F44D-1F3FE","non_qualified":null,"image":"1f44d-1f3fe.png","sheet_x":13,"sheet_y":2,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F44D-1F3FF","non_qualified":null,"image":"1f44d-1f3ff.png","sheet_x":13,"sheet_y":3,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"THUMBS DOWN SIGN","unified":"1F44E","non_qualified":null,"docomo":"E700","au":"EAD5","softbank":"E421","google":"FEBA0","image":"1f44e.png","sheet_x":13,"sheet_y":4,"short_name":"-1","short_names":["-1","thumbsdown"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-closed","sort_order":197,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F44E-1F3FB","non_qualified":null,"image":"1f44e-1f3fb.png","sheet_x":13,"sheet_y":5,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F44E-1F3FC","non_qualified":null,"image":"1f44e-1f3fc.png","sheet_x":13,"sheet_y":6,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F44E-1F3FD","non_qualified":null,"image":"1f44e-1f3fd.png","sheet_x":13,"sheet_y":7,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F44E-1F3FE","non_qualified":null,"image":"1f44e-1f3fe.png","sheet_x":13,"sheet_y":8,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F44E-1F3FF","non_qualified":null,"image":"1f44e-1f3ff.png","sheet_x":13,"sheet_y":9,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"CLAPPING HANDS SIGN","unified":"1F44F","non_qualified":null,"docomo":null,"au":"EAD3","softbank":"E41F","google":"FEB9E","image":"1f44f.png","sheet_x":13,"sheet_y":10,"short_name":"clap","short_names":["clap"],"text":null,"texts":null,"category":"People & Body","subcategory":"hands","sort_order":202,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F44F-1F3FB","non_qualified":null,"image":"1f44f-1f3fb.png","sheet_x":13,"sheet_y":11,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F44F-1F3FC","non_qualified":null,"image":"1f44f-1f3fc.png","sheet_x":13,"sheet_y":12,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F44F-1F3FD","non_qualified":null,"image":"1f44f-1f3fd.png","sheet_x":13,"sheet_y":13,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F44F-1F3FE","non_qualified":null,"image":"1f44f-1f3fe.png","sheet_x":13,"sheet_y":14,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F44F-1F3FF","non_qualified":null,"image":"1f44f-1f3ff.png","sheet_x":13,"sheet_y":15,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"OPEN HANDS SIGN","unified":"1F450","non_qualified":null,"docomo":"E695","au":"EAD6","softbank":"E422","google":"FEBA1","image":"1f450.png","sheet_x":13,"sheet_y":16,"short_name":"open_hands","short_names":["open_hands"],"text":null,"texts":null,"category":"People & Body","subcategory":"hands","sort_order":205,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F450-1F3FB","non_qualified":null,"image":"1f450-1f3fb.png","sheet_x":13,"sheet_y":17,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F450-1F3FC","non_qualified":null,"image":"1f450-1f3fc.png","sheet_x":13,"sheet_y":18,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F450-1F3FD","non_qualified":null,"image":"1f450-1f3fd.png","sheet_x":13,"sheet_y":19,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F450-1F3FE","non_qualified":null,"image":"1f450-1f3fe.png","sheet_x":13,"sheet_y":20,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F450-1F3FF","non_qualified":null,"image":"1f450-1f3ff.png","sheet_x":13,"sheet_y":21,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"CROWN","unified":"1F451","non_qualified":null,"docomo":"E71A","au":"E5C9","softbank":"E10E","google":"FE4D1","image":"1f451.png","sheet_x":13,"sheet_y":22,"short_name":"crown","short_names":["crown"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1186,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WOMANS HAT","unified":"1F452","non_qualified":null,"docomo":null,"au":"EA9E","softbank":"E318","google":"FE4D4","image":"1f452.png","sheet_x":13,"sheet_y":23,"short_name":"womans_hat","short_names":["womans_hat"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1187,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EYEGLASSES","unified":"1F453","non_qualified":null,"docomo":"E69A","au":"E4FE","softbank":null,"google":"FE4CE","image":"1f453.png","sheet_x":13,"sheet_y":24,"short_name":"eyeglasses","short_names":["eyeglasses"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1150,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NECKTIE","unified":"1F454","non_qualified":null,"docomo":null,"au":"EA93","softbank":"E302","google":"FE4D3","image":"1f454.png","sheet_x":13,"sheet_y":25,"short_name":"necktie","short_names":["necktie"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1155,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"T-SHIRT","unified":"1F455","non_qualified":null,"docomo":"E70E","au":"E5B6","softbank":"E006","google":"FE4CF","image":"1f455.png","sheet_x":13,"sheet_y":26,"short_name":"shirt","short_names":["shirt","tshirt"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1156,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"JEANS","unified":"1F456","non_qualified":null,"docomo":"E711","au":"EB77","softbank":null,"google":"FE4D0","image":"1f456.png","sheet_x":13,"sheet_y":27,"short_name":"jeans","short_names":["jeans"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1157,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DRESS","unified":"1F457","non_qualified":null,"docomo":null,"au":"EB6B","softbank":"E319","google":"FE4D5","image":"1f457.png","sheet_x":13,"sheet_y":28,"short_name":"dress","short_names":["dress"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1162,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"KIMONO","unified":"1F458","non_qualified":null,"docomo":null,"au":"EAA3","softbank":"E321","google":"FE4D9","image":"1f458.png","sheet_x":13,"sheet_y":29,"short_name":"kimono","short_names":["kimono"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1163,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BIKINI","unified":"1F459","non_qualified":null,"docomo":null,"au":"EAA4","softbank":"E322","google":"FE4DA","image":"1f459.png","sheet_x":13,"sheet_y":30,"short_name":"bikini","short_names":["bikini"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1168,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WOMANS CLOTHES","unified":"1F45A","non_qualified":null,"docomo":"E70E","au":"E50D","softbank":null,"google":"FE4DB","image":"1f45a.png","sheet_x":13,"sheet_y":31,"short_name":"womans_clothes","short_names":["womans_clothes"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1169,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PURSE","unified":"1F45B","non_qualified":null,"docomo":"E70F","au":"E504","softbank":null,"google":"FE4DC","image":"1f45b.png","sheet_x":13,"sheet_y":32,"short_name":"purse","short_names":["purse"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1171,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HANDBAG","unified":"1F45C","non_qualified":null,"docomo":"E682","au":"E49C","softbank":"E323","google":"FE4F0","image":"1f45c.png","sheet_x":13,"sheet_y":33,"short_name":"handbag","short_names":["handbag"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1172,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"POUCH","unified":"1F45D","non_qualified":null,"docomo":"E6AD","au":null,"softbank":null,"google":"FE4F1","image":"1f45d.png","sheet_x":13,"sheet_y":34,"short_name":"pouch","short_names":["pouch"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1173,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MANS SHOE","unified":"1F45E","non_qualified":null,"docomo":"E699","au":"E5B7","softbank":null,"google":"FE4CC","image":"1f45e.png","sheet_x":13,"sheet_y":35,"short_name":"mans_shoe","short_names":["mans_shoe","shoe"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1177,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ATHLETIC SHOE","unified":"1F45F","non_qualified":null,"docomo":"E699","au":"EB2B","softbank":"E007","google":"FE4CD","image":"1f45f.png","sheet_x":13,"sheet_y":36,"short_name":"athletic_shoe","short_names":["athletic_shoe"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1178,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HIGH-HEELED SHOE","unified":"1F460","non_qualified":null,"docomo":"E674","au":"E51A","softbank":"E13E","google":"FE4D6","image":"1f460.png","sheet_x":13,"sheet_y":37,"short_name":"high_heel","short_names":["high_heel"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1181,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WOMANS SANDAL","unified":"1F461","non_qualified":null,"docomo":"E674","au":"E51A","softbank":"E31A","google":"FE4D7","image":"1f461.png","sheet_x":13,"sheet_y":38,"short_name":"sandal","short_names":["sandal"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1182,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WOMANS BOOTS","unified":"1F462","non_qualified":null,"docomo":null,"au":"EA9F","softbank":"E31B","google":"FE4D8","image":"1f462.png","sheet_x":13,"sheet_y":39,"short_name":"boot","short_names":["boot"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1184,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FOOTPRINTS","unified":"1F463","non_qualified":null,"docomo":"E698","au":"EB2A","softbank":"E536","google":"FE553","image":"1f463.png","sheet_x":13,"sheet_y":40,"short_name":"footprints","short_names":["footprints"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-symbol","sort_order":553,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BUST IN SILHOUETTE","unified":"1F464","non_qualified":null,"docomo":"E6B1","au":null,"softbank":null,"google":"FE19A","image":"1f464.png","sheet_x":13,"sheet_y":41,"short_name":"bust_in_silhouette","short_names":["bust_in_silhouette"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-symbol","sort_order":545,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BUSTS IN SILHOUETTE","unified":"1F465","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f465.png","sheet_x":13,"sheet_y":42,"short_name":"busts_in_silhouette","short_names":["busts_in_silhouette"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-symbol","sort_order":546,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BOY","unified":"1F466","non_qualified":null,"docomo":"E6F0","au":"E4FC","softbank":"E001","google":"FE19B","image":"1f466.png","sheet_x":13,"sheet_y":43,"short_name":"boy","short_names":["boy"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":232,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F466-1F3FB","non_qualified":null,"image":"1f466-1f3fb.png","sheet_x":13,"sheet_y":44,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F466-1F3FC","non_qualified":null,"image":"1f466-1f3fc.png","sheet_x":13,"sheet_y":45,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F466-1F3FD","non_qualified":null,"image":"1f466-1f3fd.png","sheet_x":13,"sheet_y":46,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F466-1F3FE","non_qualified":null,"image":"1f466-1f3fe.png","sheet_x":13,"sheet_y":47,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F466-1F3FF","non_qualified":null,"image":"1f466-1f3ff.png","sheet_x":13,"sheet_y":48,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"GIRL","unified":"1F467","non_qualified":null,"docomo":"E6F0","au":"E4FA","softbank":"E002","google":"FE19C","image":"1f467.png","sheet_x":13,"sheet_y":49,"short_name":"girl","short_names":["girl"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":233,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F467-1F3FB","non_qualified":null,"image":"1f467-1f3fb.png","sheet_x":13,"sheet_y":50,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F467-1F3FC","non_qualified":null,"image":"1f467-1f3fc.png","sheet_x":13,"sheet_y":51,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F467-1F3FD","non_qualified":null,"image":"1f467-1f3fd.png","sheet_x":13,"sheet_y":52,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F467-1F3FE","non_qualified":null,"image":"1f467-1f3fe.png","sheet_x":13,"sheet_y":53,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F467-1F3FF","non_qualified":null,"image":"1f467-1f3ff.png","sheet_x":13,"sheet_y":54,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN FARMER","unified":"1F468-200D-1F33E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f33e.png","sheet_x":13,"sheet_y":55,"short_name":"male-farmer","short_names":["male-farmer"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":301,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F33E","non_qualified":null,"image":"1f468-1f3fb-200d-1f33e.png","sheet_x":13,"sheet_y":56,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-1F33E","non_qualified":null,"image":"1f468-1f3fc-200d-1f33e.png","sheet_x":13,"sheet_y":57,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-1F33E","non_qualified":null,"image":"1f468-1f3fd-200d-1f33e.png","sheet_x":13,"sheet_y":58,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-1F33E","non_qualified":null,"image":"1f468-1f3fe-200d-1f33e.png","sheet_x":13,"sheet_y":59,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-1F33E","non_qualified":null,"image":"1f468-1f3ff-200d-1f33e.png","sheet_x":13,"sheet_y":60,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN COOK","unified":"1F468-200D-1F373","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f373.png","sheet_x":13,"sheet_y":61,"short_name":"male-cook","short_names":["male-cook"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":304,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F373","non_qualified":null,"image":"1f468-1f3fb-200d-1f373.png","sheet_x":14,"sheet_y":0,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-1F373","non_qualified":null,"image":"1f468-1f3fc-200d-1f373.png","sheet_x":14,"sheet_y":1,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-1F373","non_qualified":null,"image":"1f468-1f3fd-200d-1f373.png","sheet_x":14,"sheet_y":2,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-1F373","non_qualified":null,"image":"1f468-1f3fe-200d-1f373.png","sheet_x":14,"sheet_y":3,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-1F373","non_qualified":null,"image":"1f468-1f3ff-200d-1f373.png","sheet_x":14,"sheet_y":4,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN FEEDING BABY","unified":"1F468-200D-1F37C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f37c.png","sheet_x":14,"sheet_y":5,"short_name":"man_feeding_baby","short_names":["man_feeding_baby"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":368,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F37C","non_qualified":null,"image":"1f468-1f3fb-200d-1f37c.png","sheet_x":14,"sheet_y":6,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-1F37C","non_qualified":null,"image":"1f468-1f3fc-200d-1f37c.png","sheet_x":14,"sheet_y":7,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-1F37C","non_qualified":null,"image":"1f468-1f3fd-200d-1f37c.png","sheet_x":14,"sheet_y":8,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-1F37C","non_qualified":null,"image":"1f468-1f3fe-200d-1f37c.png","sheet_x":14,"sheet_y":9,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-1F37C","non_qualified":null,"image":"1f468-1f3ff-200d-1f37c.png","sheet_x":14,"sheet_y":10,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN STUDENT","unified":"1F468-200D-1F393","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f393.png","sheet_x":14,"sheet_y":11,"short_name":"male-student","short_names":["male-student"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":292,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F393","non_qualified":null,"image":"1f468-1f3fb-200d-1f393.png","sheet_x":14,"sheet_y":12,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-1F393","non_qualified":null,"image":"1f468-1f3fc-200d-1f393.png","sheet_x":14,"sheet_y":13,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-1F393","non_qualified":null,"image":"1f468-1f3fd-200d-1f393.png","sheet_x":14,"sheet_y":14,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-1F393","non_qualified":null,"image":"1f468-1f3fe-200d-1f393.png","sheet_x":14,"sheet_y":15,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-1F393","non_qualified":null,"image":"1f468-1f3ff-200d-1f393.png","sheet_x":14,"sheet_y":16,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN SINGER","unified":"1F468-200D-1F3A4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f3a4.png","sheet_x":14,"sheet_y":17,"short_name":"male-singer","short_names":["male-singer"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":322,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F3A4","non_qualified":null,"image":"1f468-1f3fb-200d-1f3a4.png","sheet_x":14,"sheet_y":18,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-1F3A4","non_qualified":null,"image":"1f468-1f3fc-200d-1f3a4.png","sheet_x":14,"sheet_y":19,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-1F3A4","non_qualified":null,"image":"1f468-1f3fd-200d-1f3a4.png","sheet_x":14,"sheet_y":20,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-1F3A4","non_qualified":null,"image":"1f468-1f3fe-200d-1f3a4.png","sheet_x":14,"sheet_y":21,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-1F3A4","non_qualified":null,"image":"1f468-1f3ff-200d-1f3a4.png","sheet_x":14,"sheet_y":22,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN ARTIST","unified":"1F468-200D-1F3A8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f3a8.png","sheet_x":14,"sheet_y":23,"short_name":"male-artist","short_names":["male-artist"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":325,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F3A8","non_qualified":null,"image":"1f468-1f3fb-200d-1f3a8.png","sheet_x":14,"sheet_y":24,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-1F3A8","non_qualified":null,"image":"1f468-1f3fc-200d-1f3a8.png","sheet_x":14,"sheet_y":25,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-1F3A8","non_qualified":null,"image":"1f468-1f3fd-200d-1f3a8.png","sheet_x":14,"sheet_y":26,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-1F3A8","non_qualified":null,"image":"1f468-1f3fe-200d-1f3a8.png","sheet_x":14,"sheet_y":27,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-1F3A8","non_qualified":null,"image":"1f468-1f3ff-200d-1f3a8.png","sheet_x":14,"sheet_y":28,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN TEACHER","unified":"1F468-200D-1F3EB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f3eb.png","sheet_x":14,"sheet_y":29,"short_name":"male-teacher","short_names":["male-teacher"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":295,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F3EB","non_qualified":null,"image":"1f468-1f3fb-200d-1f3eb.png","sheet_x":14,"sheet_y":30,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-1F3EB","non_qualified":null,"image":"1f468-1f3fc-200d-1f3eb.png","sheet_x":14,"sheet_y":31,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-1F3EB","non_qualified":null,"image":"1f468-1f3fd-200d-1f3eb.png","sheet_x":14,"sheet_y":32,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-1F3EB","non_qualified":null,"image":"1f468-1f3fe-200d-1f3eb.png","sheet_x":14,"sheet_y":33,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-1F3EB","non_qualified":null,"image":"1f468-1f3ff-200d-1f3eb.png","sheet_x":14,"sheet_y":34,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN FACTORY WORKER","unified":"1F468-200D-1F3ED","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f3ed.png","sheet_x":14,"sheet_y":35,"short_name":"male-factory-worker","short_names":["male-factory-worker"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":310,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F3ED","non_qualified":null,"image":"1f468-1f3fb-200d-1f3ed.png","sheet_x":14,"sheet_y":36,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-1F3ED","non_qualified":null,"image":"1f468-1f3fc-200d-1f3ed.png","sheet_x":14,"sheet_y":37,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-1F3ED","non_qualified":null,"image":"1f468-1f3fd-200d-1f3ed.png","sheet_x":14,"sheet_y":38,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-1F3ED","non_qualified":null,"image":"1f468-1f3fe-200d-1f3ed.png","sheet_x":14,"sheet_y":39,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-1F3ED","non_qualified":null,"image":"1f468-1f3ff-200d-1f3ed.png","sheet_x":14,"sheet_y":40,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"FAMILY: MAN, BOY, BOY","unified":"1F468-200D-1F466-200D-1F466","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f466-200d-1f466.png","sheet_x":14,"sheet_y":41,"short_name":"man-boy-boy","short_names":["man-boy-boy"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":535,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAMILY: MAN, BOY","unified":"1F468-200D-1F466","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f466.png","sheet_x":14,"sheet_y":42,"short_name":"man-boy","short_names":["man-boy"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":534,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAMILY: MAN, GIRL, BOY","unified":"1F468-200D-1F467-200D-1F466","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f467-200d-1f466.png","sheet_x":14,"sheet_y":43,"short_name":"man-girl-boy","short_names":["man-girl-boy"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":537,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAMILY: MAN, GIRL, GIRL","unified":"1F468-200D-1F467-200D-1F467","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f467-200d-1f467.png","sheet_x":14,"sheet_y":44,"short_name":"man-girl-girl","short_names":["man-girl-girl"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":538,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAMILY: MAN, GIRL","unified":"1F468-200D-1F467","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f467.png","sheet_x":14,"sheet_y":45,"short_name":"man-girl","short_names":["man-girl"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":536,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAMILY: MAN, MAN, BOY","unified":"1F468-200D-1F468-200D-1F466","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f468-200d-1f466.png","sheet_x":14,"sheet_y":46,"short_name":"man-man-boy","short_names":["man-man-boy"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":524,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAMILY: MAN, MAN, BOY, BOY","unified":"1F468-200D-1F468-200D-1F466-200D-1F466","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f468-200d-1f466-200d-1f466.png","sheet_x":14,"sheet_y":47,"short_name":"man-man-boy-boy","short_names":["man-man-boy-boy"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":527,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAMILY: MAN, MAN, GIRL","unified":"1F468-200D-1F468-200D-1F467","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f468-200d-1f467.png","sheet_x":14,"sheet_y":48,"short_name":"man-man-girl","short_names":["man-man-girl"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":525,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAMILY: MAN, MAN, GIRL, BOY","unified":"1F468-200D-1F468-200D-1F467-200D-1F466","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f468-200d-1f467-200d-1f466.png","sheet_x":14,"sheet_y":49,"short_name":"man-man-girl-boy","short_names":["man-man-girl-boy"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":526,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAMILY: MAN, MAN, GIRL, GIRL","unified":"1F468-200D-1F468-200D-1F467-200D-1F467","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f468-200d-1f467-200d-1f467.png","sheet_x":14,"sheet_y":50,"short_name":"man-man-girl-girl","short_names":["man-man-girl-girl"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":528,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAMILY: MAN, WOMAN, BOY","unified":"1F468-200D-1F469-200D-1F466","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f469-200d-1f466.png","sheet_x":14,"sheet_y":51,"short_name":"man-woman-boy","short_names":["man-woman-boy"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":519,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F46A"},{"name":"FAMILY: MAN, WOMAN, BOY, BOY","unified":"1F468-200D-1F469-200D-1F466-200D-1F466","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f469-200d-1f466-200d-1f466.png","sheet_x":14,"sheet_y":52,"short_name":"man-woman-boy-boy","short_names":["man-woman-boy-boy"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":522,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAMILY: MAN, WOMAN, GIRL","unified":"1F468-200D-1F469-200D-1F467","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f469-200d-1f467.png","sheet_x":14,"sheet_y":53,"short_name":"man-woman-girl","short_names":["man-woman-girl"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":520,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAMILY: MAN, WOMAN, GIRL, BOY","unified":"1F468-200D-1F469-200D-1F467-200D-1F466","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f469-200d-1f467-200d-1f466.png","sheet_x":14,"sheet_y":54,"short_name":"man-woman-girl-boy","short_names":["man-woman-girl-boy"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":521,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAMILY: MAN, WOMAN, GIRL, GIRL","unified":"1F468-200D-1F469-200D-1F467-200D-1F467","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f469-200d-1f467-200d-1f467.png","sheet_x":14,"sheet_y":55,"short_name":"man-woman-girl-girl","short_names":["man-woman-girl-girl"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":523,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MAN TECHNOLOGIST","unified":"1F468-200D-1F4BB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f4bb.png","sheet_x":14,"sheet_y":56,"short_name":"male-technologist","short_names":["male-technologist"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":319,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F4BB","non_qualified":null,"image":"1f468-1f3fb-200d-1f4bb.png","sheet_x":14,"sheet_y":57,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-1F4BB","non_qualified":null,"image":"1f468-1f3fc-200d-1f4bb.png","sheet_x":14,"sheet_y":58,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-1F4BB","non_qualified":null,"image":"1f468-1f3fd-200d-1f4bb.png","sheet_x":14,"sheet_y":59,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-1F4BB","non_qualified":null,"image":"1f468-1f3fe-200d-1f4bb.png","sheet_x":14,"sheet_y":60,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-1F4BB","non_qualified":null,"image":"1f468-1f3ff-200d-1f4bb.png","sheet_x":14,"sheet_y":61,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN OFFICE WORKER","unified":"1F468-200D-1F4BC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f4bc.png","sheet_x":15,"sheet_y":0,"short_name":"male-office-worker","short_names":["male-office-worker"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":313,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F4BC","non_qualified":null,"image":"1f468-1f3fb-200d-1f4bc.png","sheet_x":15,"sheet_y":1,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-1F4BC","non_qualified":null,"image":"1f468-1f3fc-200d-1f4bc.png","sheet_x":15,"sheet_y":2,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-1F4BC","non_qualified":null,"image":"1f468-1f3fd-200d-1f4bc.png","sheet_x":15,"sheet_y":3,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-1F4BC","non_qualified":null,"image":"1f468-1f3fe-200d-1f4bc.png","sheet_x":15,"sheet_y":4,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-1F4BC","non_qualified":null,"image":"1f468-1f3ff-200d-1f4bc.png","sheet_x":15,"sheet_y":5,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN MECHANIC","unified":"1F468-200D-1F527","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f527.png","sheet_x":15,"sheet_y":6,"short_name":"male-mechanic","short_names":["male-mechanic"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":307,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F527","non_qualified":null,"image":"1f468-1f3fb-200d-1f527.png","sheet_x":15,"sheet_y":7,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-1F527","non_qualified":null,"image":"1f468-1f3fc-200d-1f527.png","sheet_x":15,"sheet_y":8,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-1F527","non_qualified":null,"image":"1f468-1f3fd-200d-1f527.png","sheet_x":15,"sheet_y":9,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-1F527","non_qualified":null,"image":"1f468-1f3fe-200d-1f527.png","sheet_x":15,"sheet_y":10,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-1F527","non_qualified":null,"image":"1f468-1f3ff-200d-1f527.png","sheet_x":15,"sheet_y":11,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN SCIENTIST","unified":"1F468-200D-1F52C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f52c.png","sheet_x":15,"sheet_y":12,"short_name":"male-scientist","short_names":["male-scientist"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":316,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F52C","non_qualified":null,"image":"1f468-1f3fb-200d-1f52c.png","sheet_x":15,"sheet_y":13,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-1F52C","non_qualified":null,"image":"1f468-1f3fc-200d-1f52c.png","sheet_x":15,"sheet_y":14,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-1F52C","non_qualified":null,"image":"1f468-1f3fd-200d-1f52c.png","sheet_x":15,"sheet_y":15,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-1F52C","non_qualified":null,"image":"1f468-1f3fe-200d-1f52c.png","sheet_x":15,"sheet_y":16,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-1F52C","non_qualified":null,"image":"1f468-1f3ff-200d-1f52c.png","sheet_x":15,"sheet_y":17,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN ASTRONAUT","unified":"1F468-200D-1F680","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f680.png","sheet_x":15,"sheet_y":18,"short_name":"male-astronaut","short_names":["male-astronaut"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":331,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F680","non_qualified":null,"image":"1f468-1f3fb-200d-1f680.png","sheet_x":15,"sheet_y":19,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-1F680","non_qualified":null,"image":"1f468-1f3fc-200d-1f680.png","sheet_x":15,"sheet_y":20,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-1F680","non_qualified":null,"image":"1f468-1f3fd-200d-1f680.png","sheet_x":15,"sheet_y":21,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-1F680","non_qualified":null,"image":"1f468-1f3fe-200d-1f680.png","sheet_x":15,"sheet_y":22,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-1F680","non_qualified":null,"image":"1f468-1f3ff-200d-1f680.png","sheet_x":15,"sheet_y":23,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN FIREFIGHTER","unified":"1F468-200D-1F692","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f692.png","sheet_x":15,"sheet_y":24,"short_name":"male-firefighter","short_names":["male-firefighter"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":334,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F692","non_qualified":null,"image":"1f468-1f3fb-200d-1f692.png","sheet_x":15,"sheet_y":25,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-1F692","non_qualified":null,"image":"1f468-1f3fc-200d-1f692.png","sheet_x":15,"sheet_y":26,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-1F692","non_qualified":null,"image":"1f468-1f3fd-200d-1f692.png","sheet_x":15,"sheet_y":27,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-1F692","non_qualified":null,"image":"1f468-1f3fe-200d-1f692.png","sheet_x":15,"sheet_y":28,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-1F692","non_qualified":null,"image":"1f468-1f3ff-200d-1f692.png","sheet_x":15,"sheet_y":29,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN WITH WHITE CANE FACING RIGHT","unified":"1F468-200D-1F9AF-200D-27A1-FE0F","non_qualified":"1F468-200D-1F9AF-200D-27A1","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f9af-200d-27a1-fe0f.png","sheet_x":15,"sheet_y":30,"short_name":"man_with_white_cane_facing_right","short_names":["man_with_white_cane_facing_right"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":426,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F9AF-200D-27A1-FE0F","non_qualified":"1F468-1F3FB-200D-1F9AF-200D-27A1","image":"1f468-1f3fb-200d-1f9af-200d-27a1-fe0f.png","sheet_x":15,"sheet_y":31,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FC":{"unified":"1F468-1F3FC-200D-1F9AF-200D-27A1-FE0F","non_qualified":"1F468-1F3FC-200D-1F9AF-200D-27A1","image":"1f468-1f3fc-200d-1f9af-200d-27a1-fe0f.png","sheet_x":15,"sheet_y":32,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FD":{"unified":"1F468-1F3FD-200D-1F9AF-200D-27A1-FE0F","non_qualified":"1F468-1F3FD-200D-1F9AF-200D-27A1","image":"1f468-1f3fd-200d-1f9af-200d-27a1-fe0f.png","sheet_x":15,"sheet_y":33,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FE":{"unified":"1F468-1F3FE-200D-1F9AF-200D-27A1-FE0F","non_qualified":"1F468-1F3FE-200D-1F9AF-200D-27A1","image":"1f468-1f3fe-200d-1f9af-200d-27a1-fe0f.png","sheet_x":15,"sheet_y":34,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FF":{"unified":"1F468-1F3FF-200D-1F9AF-200D-27A1-FE0F","non_qualified":"1F468-1F3FF-200D-1F9AF-200D-27A1","image":"1f468-1f3ff-200d-1f9af-200d-27a1-fe0f.png","sheet_x":15,"sheet_y":35,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false}}},{"name":"MAN WITH WHITE CANE","unified":"1F468-200D-1F9AF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f9af.png","sheet_x":15,"sheet_y":36,"short_name":"man_with_probing_cane","short_names":["man_with_probing_cane"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":425,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F9AF","non_qualified":null,"image":"1f468-1f3fb-200d-1f9af.png","sheet_x":15,"sheet_y":37,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-1F9AF","non_qualified":null,"image":"1f468-1f3fc-200d-1f9af.png","sheet_x":15,"sheet_y":38,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-1F9AF","non_qualified":null,"image":"1f468-1f3fd-200d-1f9af.png","sheet_x":15,"sheet_y":39,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-1F9AF","non_qualified":null,"image":"1f468-1f3fe-200d-1f9af.png","sheet_x":15,"sheet_y":40,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-1F9AF","non_qualified":null,"image":"1f468-1f3ff-200d-1f9af.png","sheet_x":15,"sheet_y":41,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN: RED HAIR","unified":"1F468-200D-1F9B0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f9b0.png","sheet_x":15,"sheet_y":42,"short_name":"red_haired_man","short_names":["red_haired_man"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":240,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F9B0","non_qualified":null,"image":"1f468-1f3fb-200d-1f9b0.png","sheet_x":15,"sheet_y":43,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-1F9B0","non_qualified":null,"image":"1f468-1f3fc-200d-1f9b0.png","sheet_x":15,"sheet_y":44,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-1F9B0","non_qualified":null,"image":"1f468-1f3fd-200d-1f9b0.png","sheet_x":15,"sheet_y":45,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-1F9B0","non_qualified":null,"image":"1f468-1f3fe-200d-1f9b0.png","sheet_x":15,"sheet_y":46,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-1F9B0","non_qualified":null,"image":"1f468-1f3ff-200d-1f9b0.png","sheet_x":15,"sheet_y":47,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN: CURLY HAIR","unified":"1F468-200D-1F9B1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f9b1.png","sheet_x":15,"sheet_y":48,"short_name":"curly_haired_man","short_names":["curly_haired_man"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":241,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F9B1","non_qualified":null,"image":"1f468-1f3fb-200d-1f9b1.png","sheet_x":15,"sheet_y":49,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-1F9B1","non_qualified":null,"image":"1f468-1f3fc-200d-1f9b1.png","sheet_x":15,"sheet_y":50,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-1F9B1","non_qualified":null,"image":"1f468-1f3fd-200d-1f9b1.png","sheet_x":15,"sheet_y":51,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-1F9B1","non_qualified":null,"image":"1f468-1f3fe-200d-1f9b1.png","sheet_x":15,"sheet_y":52,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-1F9B1","non_qualified":null,"image":"1f468-1f3ff-200d-1f9b1.png","sheet_x":15,"sheet_y":53,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN: BALD","unified":"1F468-200D-1F9B2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f9b2.png","sheet_x":15,"sheet_y":54,"short_name":"bald_man","short_names":["bald_man"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":243,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F9B2","non_qualified":null,"image":"1f468-1f3fb-200d-1f9b2.png","sheet_x":15,"sheet_y":55,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-1F9B2","non_qualified":null,"image":"1f468-1f3fc-200d-1f9b2.png","sheet_x":15,"sheet_y":56,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-1F9B2","non_qualified":null,"image":"1f468-1f3fd-200d-1f9b2.png","sheet_x":15,"sheet_y":57,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-1F9B2","non_qualified":null,"image":"1f468-1f3fe-200d-1f9b2.png","sheet_x":15,"sheet_y":58,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-1F9B2","non_qualified":null,"image":"1f468-1f3ff-200d-1f9b2.png","sheet_x":15,"sheet_y":59,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN: WHITE HAIR","unified":"1F468-200D-1F9B3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f9b3.png","sheet_x":15,"sheet_y":60,"short_name":"white_haired_man","short_names":["white_haired_man"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":242,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F9B3","non_qualified":null,"image":"1f468-1f3fb-200d-1f9b3.png","sheet_x":15,"sheet_y":61,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-1F9B3","non_qualified":null,"image":"1f468-1f3fc-200d-1f9b3.png","sheet_x":16,"sheet_y":0,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-1F9B3","non_qualified":null,"image":"1f468-1f3fd-200d-1f9b3.png","sheet_x":16,"sheet_y":1,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-1F9B3","non_qualified":null,"image":"1f468-1f3fe-200d-1f9b3.png","sheet_x":16,"sheet_y":2,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-1F9B3","non_qualified":null,"image":"1f468-1f3ff-200d-1f9b3.png","sheet_x":16,"sheet_y":3,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN IN MOTORIZED WHEELCHAIR FACING RIGHT","unified":"1F468-200D-1F9BC-200D-27A1-FE0F","non_qualified":"1F468-200D-1F9BC-200D-27A1","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f9bc-200d-27a1-fe0f.png","sheet_x":16,"sheet_y":4,"short_name":"man_in_motorized_wheelchair_facing_right","short_names":["man_in_motorized_wheelchair_facing_right"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":432,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F9BC-200D-27A1-FE0F","non_qualified":"1F468-1F3FB-200D-1F9BC-200D-27A1","image":"1f468-1f3fb-200d-1f9bc-200d-27a1-fe0f.png","sheet_x":16,"sheet_y":5,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FC":{"unified":"1F468-1F3FC-200D-1F9BC-200D-27A1-FE0F","non_qualified":"1F468-1F3FC-200D-1F9BC-200D-27A1","image":"1f468-1f3fc-200d-1f9bc-200d-27a1-fe0f.png","sheet_x":16,"sheet_y":6,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FD":{"unified":"1F468-1F3FD-200D-1F9BC-200D-27A1-FE0F","non_qualified":"1F468-1F3FD-200D-1F9BC-200D-27A1","image":"1f468-1f3fd-200d-1f9bc-200d-27a1-fe0f.png","sheet_x":16,"sheet_y":7,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FE":{"unified":"1F468-1F3FE-200D-1F9BC-200D-27A1-FE0F","non_qualified":"1F468-1F3FE-200D-1F9BC-200D-27A1","image":"1f468-1f3fe-200d-1f9bc-200d-27a1-fe0f.png","sheet_x":16,"sheet_y":8,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FF":{"unified":"1F468-1F3FF-200D-1F9BC-200D-27A1-FE0F","non_qualified":"1F468-1F3FF-200D-1F9BC-200D-27A1","image":"1f468-1f3ff-200d-1f9bc-200d-27a1-fe0f.png","sheet_x":16,"sheet_y":9,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false}}},{"name":"MAN IN MOTORIZED WHEELCHAIR","unified":"1F468-200D-1F9BC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f9bc.png","sheet_x":16,"sheet_y":10,"short_name":"man_in_motorized_wheelchair","short_names":["man_in_motorized_wheelchair"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":431,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F9BC","non_qualified":null,"image":"1f468-1f3fb-200d-1f9bc.png","sheet_x":16,"sheet_y":11,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-1F9BC","non_qualified":null,"image":"1f468-1f3fc-200d-1f9bc.png","sheet_x":16,"sheet_y":12,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-1F9BC","non_qualified":null,"image":"1f468-1f3fd-200d-1f9bc.png","sheet_x":16,"sheet_y":13,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-1F9BC","non_qualified":null,"image":"1f468-1f3fe-200d-1f9bc.png","sheet_x":16,"sheet_y":14,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-1F9BC","non_qualified":null,"image":"1f468-1f3ff-200d-1f9bc.png","sheet_x":16,"sheet_y":15,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN IN MANUAL WHEELCHAIR FACING RIGHT","unified":"1F468-200D-1F9BD-200D-27A1-FE0F","non_qualified":"1F468-200D-1F9BD-200D-27A1","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f9bd-200d-27a1-fe0f.png","sheet_x":16,"sheet_y":16,"short_name":"man_in_manual_wheelchair_facing_right","short_names":["man_in_manual_wheelchair_facing_right"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":438,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F9BD-200D-27A1-FE0F","non_qualified":"1F468-1F3FB-200D-1F9BD-200D-27A1","image":"1f468-1f3fb-200d-1f9bd-200d-27a1-fe0f.png","sheet_x":16,"sheet_y":17,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FC":{"unified":"1F468-1F3FC-200D-1F9BD-200D-27A1-FE0F","non_qualified":"1F468-1F3FC-200D-1F9BD-200D-27A1","image":"1f468-1f3fc-200d-1f9bd-200d-27a1-fe0f.png","sheet_x":16,"sheet_y":18,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FD":{"unified":"1F468-1F3FD-200D-1F9BD-200D-27A1-FE0F","non_qualified":"1F468-1F3FD-200D-1F9BD-200D-27A1","image":"1f468-1f3fd-200d-1f9bd-200d-27a1-fe0f.png","sheet_x":16,"sheet_y":19,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FE":{"unified":"1F468-1F3FE-200D-1F9BD-200D-27A1-FE0F","non_qualified":"1F468-1F3FE-200D-1F9BD-200D-27A1","image":"1f468-1f3fe-200d-1f9bd-200d-27a1-fe0f.png","sheet_x":16,"sheet_y":20,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FF":{"unified":"1F468-1F3FF-200D-1F9BD-200D-27A1-FE0F","non_qualified":"1F468-1F3FF-200D-1F9BD-200D-27A1","image":"1f468-1f3ff-200d-1f9bd-200d-27a1-fe0f.png","sheet_x":16,"sheet_y":21,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false}}},{"name":"MAN IN MANUAL WHEELCHAIR","unified":"1F468-200D-1F9BD","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-1f9bd.png","sheet_x":16,"sheet_y":22,"short_name":"man_in_manual_wheelchair","short_names":["man_in_manual_wheelchair"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":437,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-1F9BD","non_qualified":null,"image":"1f468-1f3fb-200d-1f9bd.png","sheet_x":16,"sheet_y":23,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-1F9BD","non_qualified":null,"image":"1f468-1f3fc-200d-1f9bd.png","sheet_x":16,"sheet_y":24,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-1F9BD","non_qualified":null,"image":"1f468-1f3fd-200d-1f9bd.png","sheet_x":16,"sheet_y":25,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-1F9BD","non_qualified":null,"image":"1f468-1f3fe-200d-1f9bd.png","sheet_x":16,"sheet_y":26,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-1F9BD","non_qualified":null,"image":"1f468-1f3ff-200d-1f9bd.png","sheet_x":16,"sheet_y":27,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN HEALTH WORKER","unified":"1F468-200D-2695-FE0F","non_qualified":"1F468-200D-2695","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-2695-fe0f.png","sheet_x":16,"sheet_y":28,"short_name":"male-doctor","short_names":["male-doctor"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":289,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-2695-FE0F","non_qualified":"1F468-1F3FB-200D-2695","image":"1f468-1f3fb-200d-2695-fe0f.png","sheet_x":16,"sheet_y":29,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-2695-FE0F","non_qualified":"1F468-1F3FC-200D-2695","image":"1f468-1f3fc-200d-2695-fe0f.png","sheet_x":16,"sheet_y":30,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-2695-FE0F","non_qualified":"1F468-1F3FD-200D-2695","image":"1f468-1f3fd-200d-2695-fe0f.png","sheet_x":16,"sheet_y":31,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-2695-FE0F","non_qualified":"1F468-1F3FE-200D-2695","image":"1f468-1f3fe-200d-2695-fe0f.png","sheet_x":16,"sheet_y":32,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-2695-FE0F","non_qualified":"1F468-1F3FF-200D-2695","image":"1f468-1f3ff-200d-2695-fe0f.png","sheet_x":16,"sheet_y":33,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN JUDGE","unified":"1F468-200D-2696-FE0F","non_qualified":"1F468-200D-2696","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-2696-fe0f.png","sheet_x":16,"sheet_y":34,"short_name":"male-judge","short_names":["male-judge"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":298,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-2696-FE0F","non_qualified":"1F468-1F3FB-200D-2696","image":"1f468-1f3fb-200d-2696-fe0f.png","sheet_x":16,"sheet_y":35,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-2696-FE0F","non_qualified":"1F468-1F3FC-200D-2696","image":"1f468-1f3fc-200d-2696-fe0f.png","sheet_x":16,"sheet_y":36,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-2696-FE0F","non_qualified":"1F468-1F3FD-200D-2696","image":"1f468-1f3fd-200d-2696-fe0f.png","sheet_x":16,"sheet_y":37,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-2696-FE0F","non_qualified":"1F468-1F3FE-200D-2696","image":"1f468-1f3fe-200d-2696-fe0f.png","sheet_x":16,"sheet_y":38,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-2696-FE0F","non_qualified":"1F468-1F3FF-200D-2696","image":"1f468-1f3ff-200d-2696-fe0f.png","sheet_x":16,"sheet_y":39,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN PILOT","unified":"1F468-200D-2708-FE0F","non_qualified":"1F468-200D-2708","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-2708-fe0f.png","sheet_x":16,"sheet_y":40,"short_name":"male-pilot","short_names":["male-pilot"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":328,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB-200D-2708-FE0F","non_qualified":"1F468-1F3FB-200D-2708","image":"1f468-1f3fb-200d-2708-fe0f.png","sheet_x":16,"sheet_y":41,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC-200D-2708-FE0F","non_qualified":"1F468-1F3FC-200D-2708","image":"1f468-1f3fc-200d-2708-fe0f.png","sheet_x":16,"sheet_y":42,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD-200D-2708-FE0F","non_qualified":"1F468-1F3FD-200D-2708","image":"1f468-1f3fd-200d-2708-fe0f.png","sheet_x":16,"sheet_y":43,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE-200D-2708-FE0F","non_qualified":"1F468-1F3FE-200D-2708","image":"1f468-1f3fe-200d-2708-fe0f.png","sheet_x":16,"sheet_y":44,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF-200D-2708-FE0F","non_qualified":"1F468-1F3FF-200D-2708","image":"1f468-1f3ff-200d-2708-fe0f.png","sheet_x":16,"sheet_y":45,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"COUPLE WITH HEART: MAN, MAN","unified":"1F468-200D-2764-FE0F-200D-1F468","non_qualified":"1F468-200D-2764-200D-1F468","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-2764-fe0f-200d-1f468.png","sheet_x":16,"sheet_y":46,"short_name":"man-heart-man","short_names":["man-heart-man"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":517,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB-1F3FB":{"unified":"1F468-1F3FB-200D-2764-FE0F-200D-1F468-1F3FB","non_qualified":"1F468-1F3FB-200D-2764-200D-1F468-1F3FB","image":"1f468-1f3fb-200d-2764-fe0f-200d-1f468-1f3fb.png","sheet_x":16,"sheet_y":47,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FC":{"unified":"1F468-1F3FB-200D-2764-FE0F-200D-1F468-1F3FC","non_qualified":"1F468-1F3FB-200D-2764-200D-1F468-1F3FC","image":"1f468-1f3fb-200d-2764-fe0f-200d-1f468-1f3fc.png","sheet_x":16,"sheet_y":48,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FD":{"unified":"1F468-1F3FB-200D-2764-FE0F-200D-1F468-1F3FD","non_qualified":"1F468-1F3FB-200D-2764-200D-1F468-1F3FD","image":"1f468-1f3fb-200d-2764-fe0f-200d-1f468-1f3fd.png","sheet_x":16,"sheet_y":49,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FE":{"unified":"1F468-1F3FB-200D-2764-FE0F-200D-1F468-1F3FE","non_qualified":"1F468-1F3FB-200D-2764-200D-1F468-1F3FE","image":"1f468-1f3fb-200d-2764-fe0f-200d-1f468-1f3fe.png","sheet_x":16,"sheet_y":50,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FF":{"unified":"1F468-1F3FB-200D-2764-FE0F-200D-1F468-1F3FF","non_qualified":"1F468-1F3FB-200D-2764-200D-1F468-1F3FF","image":"1f468-1f3fb-200d-2764-fe0f-200d-1f468-1f3ff.png","sheet_x":16,"sheet_y":51,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FB":{"unified":"1F468-1F3FC-200D-2764-FE0F-200D-1F468-1F3FB","non_qualified":"1F468-1F3FC-200D-2764-200D-1F468-1F3FB","image":"1f468-1f3fc-200d-2764-fe0f-200d-1f468-1f3fb.png","sheet_x":16,"sheet_y":52,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FC":{"unified":"1F468-1F3FC-200D-2764-FE0F-200D-1F468-1F3FC","non_qualified":"1F468-1F3FC-200D-2764-200D-1F468-1F3FC","image":"1f468-1f3fc-200d-2764-fe0f-200d-1f468-1f3fc.png","sheet_x":16,"sheet_y":53,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FD":{"unified":"1F468-1F3FC-200D-2764-FE0F-200D-1F468-1F3FD","non_qualified":"1F468-1F3FC-200D-2764-200D-1F468-1F3FD","image":"1f468-1f3fc-200d-2764-fe0f-200d-1f468-1f3fd.png","sheet_x":16,"sheet_y":54,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FE":{"unified":"1F468-1F3FC-200D-2764-FE0F-200D-1F468-1F3FE","non_qualified":"1F468-1F3FC-200D-2764-200D-1F468-1F3FE","image":"1f468-1f3fc-200d-2764-fe0f-200d-1f468-1f3fe.png","sheet_x":16,"sheet_y":55,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FF":{"unified":"1F468-1F3FC-200D-2764-FE0F-200D-1F468-1F3FF","non_qualified":"1F468-1F3FC-200D-2764-200D-1F468-1F3FF","image":"1f468-1f3fc-200d-2764-fe0f-200d-1f468-1f3ff.png","sheet_x":16,"sheet_y":56,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FB":{"unified":"1F468-1F3FD-200D-2764-FE0F-200D-1F468-1F3FB","non_qualified":"1F468-1F3FD-200D-2764-200D-1F468-1F3FB","image":"1f468-1f3fd-200d-2764-fe0f-200d-1f468-1f3fb.png","sheet_x":16,"sheet_y":57,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FC":{"unified":"1F468-1F3FD-200D-2764-FE0F-200D-1F468-1F3FC","non_qualified":"1F468-1F3FD-200D-2764-200D-1F468-1F3FC","image":"1f468-1f3fd-200d-2764-fe0f-200d-1f468-1f3fc.png","sheet_x":16,"sheet_y":58,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FD":{"unified":"1F468-1F3FD-200D-2764-FE0F-200D-1F468-1F3FD","non_qualified":"1F468-1F3FD-200D-2764-200D-1F468-1F3FD","image":"1f468-1f3fd-200d-2764-fe0f-200d-1f468-1f3fd.png","sheet_x":16,"sheet_y":59,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FE":{"unified":"1F468-1F3FD-200D-2764-FE0F-200D-1F468-1F3FE","non_qualified":"1F468-1F3FD-200D-2764-200D-1F468-1F3FE","image":"1f468-1f3fd-200d-2764-fe0f-200d-1f468-1f3fe.png","sheet_x":16,"sheet_y":60,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FF":{"unified":"1F468-1F3FD-200D-2764-FE0F-200D-1F468-1F3FF","non_qualified":"1F468-1F3FD-200D-2764-200D-1F468-1F3FF","image":"1f468-1f3fd-200d-2764-fe0f-200d-1f468-1f3ff.png","sheet_x":16,"sheet_y":61,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FB":{"unified":"1F468-1F3FE-200D-2764-FE0F-200D-1F468-1F3FB","non_qualified":"1F468-1F3FE-200D-2764-200D-1F468-1F3FB","image":"1f468-1f3fe-200d-2764-fe0f-200d-1f468-1f3fb.png","sheet_x":17,"sheet_y":0,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FC":{"unified":"1F468-1F3FE-200D-2764-FE0F-200D-1F468-1F3FC","non_qualified":"1F468-1F3FE-200D-2764-200D-1F468-1F3FC","image":"1f468-1f3fe-200d-2764-fe0f-200d-1f468-1f3fc.png","sheet_x":17,"sheet_y":1,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FD":{"unified":"1F468-1F3FE-200D-2764-FE0F-200D-1F468-1F3FD","non_qualified":"1F468-1F3FE-200D-2764-200D-1F468-1F3FD","image":"1f468-1f3fe-200d-2764-fe0f-200d-1f468-1f3fd.png","sheet_x":17,"sheet_y":2,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FE":{"unified":"1F468-1F3FE-200D-2764-FE0F-200D-1F468-1F3FE","non_qualified":"1F468-1F3FE-200D-2764-200D-1F468-1F3FE","image":"1f468-1f3fe-200d-2764-fe0f-200d-1f468-1f3fe.png","sheet_x":17,"sheet_y":3,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FF":{"unified":"1F468-1F3FE-200D-2764-FE0F-200D-1F468-1F3FF","non_qualified":"1F468-1F3FE-200D-2764-200D-1F468-1F3FF","image":"1f468-1f3fe-200d-2764-fe0f-200d-1f468-1f3ff.png","sheet_x":17,"sheet_y":4,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FB":{"unified":"1F468-1F3FF-200D-2764-FE0F-200D-1F468-1F3FB","non_qualified":"1F468-1F3FF-200D-2764-200D-1F468-1F3FB","image":"1f468-1f3ff-200d-2764-fe0f-200d-1f468-1f3fb.png","sheet_x":17,"sheet_y":5,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FC":{"unified":"1F468-1F3FF-200D-2764-FE0F-200D-1F468-1F3FC","non_qualified":"1F468-1F3FF-200D-2764-200D-1F468-1F3FC","image":"1f468-1f3ff-200d-2764-fe0f-200d-1f468-1f3fc.png","sheet_x":17,"sheet_y":6,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FD":{"unified":"1F468-1F3FF-200D-2764-FE0F-200D-1F468-1F3FD","non_qualified":"1F468-1F3FF-200D-2764-200D-1F468-1F3FD","image":"1f468-1f3ff-200d-2764-fe0f-200d-1f468-1f3fd.png","sheet_x":17,"sheet_y":7,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FE":{"unified":"1F468-1F3FF-200D-2764-FE0F-200D-1F468-1F3FE","non_qualified":"1F468-1F3FF-200D-2764-200D-1F468-1F3FE","image":"1f468-1f3ff-200d-2764-fe0f-200d-1f468-1f3fe.png","sheet_x":17,"sheet_y":8,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FF":{"unified":"1F468-1F3FF-200D-2764-FE0F-200D-1F468-1F3FF","non_qualified":"1F468-1F3FF-200D-2764-200D-1F468-1F3FF","image":"1f468-1f3ff-200d-2764-fe0f-200d-1f468-1f3ff.png","sheet_x":17,"sheet_y":9,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"KISS: MAN, MAN","unified":"1F468-200D-2764-FE0F-200D-1F48B-200D-1F468","non_qualified":"1F468-200D-2764-200D-1F48B-200D-1F468","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f468-200d-2764-fe0f-200d-1f48b-200d-1f468.png","sheet_x":17,"sheet_y":10,"short_name":"man-kiss-man","short_names":["man-kiss-man"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":513,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB-1F3FB":{"unified":"1F468-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FB","non_qualified":"1F468-1F3FB-200D-2764-200D-1F48B-200D-1F468-1F3FB","image":"1f468-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb.png","sheet_x":17,"sheet_y":11,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FC":{"unified":"1F468-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FC","non_qualified":"1F468-1F3FB-200D-2764-200D-1F48B-200D-1F468-1F3FC","image":"1f468-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc.png","sheet_x":17,"sheet_y":12,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FD":{"unified":"1F468-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FD","non_qualified":"1F468-1F3FB-200D-2764-200D-1F48B-200D-1F468-1F3FD","image":"1f468-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd.png","sheet_x":17,"sheet_y":13,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FE":{"unified":"1F468-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FE","non_qualified":"1F468-1F3FB-200D-2764-200D-1F48B-200D-1F468-1F3FE","image":"1f468-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe.png","sheet_x":17,"sheet_y":14,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FF":{"unified":"1F468-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FF","non_qualified":"1F468-1F3FB-200D-2764-200D-1F48B-200D-1F468-1F3FF","image":"1f468-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff.png","sheet_x":17,"sheet_y":15,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FB":{"unified":"1F468-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FB","non_qualified":"1F468-1F3FC-200D-2764-200D-1F48B-200D-1F468-1F3FB","image":"1f468-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb.png","sheet_x":17,"sheet_y":16,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FC":{"unified":"1F468-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FC","non_qualified":"1F468-1F3FC-200D-2764-200D-1F48B-200D-1F468-1F3FC","image":"1f468-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc.png","sheet_x":17,"sheet_y":17,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FD":{"unified":"1F468-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FD","non_qualified":"1F468-1F3FC-200D-2764-200D-1F48B-200D-1F468-1F3FD","image":"1f468-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd.png","sheet_x":17,"sheet_y":18,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FE":{"unified":"1F468-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FE","non_qualified":"1F468-1F3FC-200D-2764-200D-1F48B-200D-1F468-1F3FE","image":"1f468-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe.png","sheet_x":17,"sheet_y":19,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FF":{"unified":"1F468-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FF","non_qualified":"1F468-1F3FC-200D-2764-200D-1F48B-200D-1F468-1F3FF","image":"1f468-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff.png","sheet_x":17,"sheet_y":20,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FB":{"unified":"1F468-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FB","non_qualified":"1F468-1F3FD-200D-2764-200D-1F48B-200D-1F468-1F3FB","image":"1f468-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb.png","sheet_x":17,"sheet_y":21,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FC":{"unified":"1F468-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FC","non_qualified":"1F468-1F3FD-200D-2764-200D-1F48B-200D-1F468-1F3FC","image":"1f468-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc.png","sheet_x":17,"sheet_y":22,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FD":{"unified":"1F468-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FD","non_qualified":"1F468-1F3FD-200D-2764-200D-1F48B-200D-1F468-1F3FD","image":"1f468-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd.png","sheet_x":17,"sheet_y":23,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FE":{"unified":"1F468-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FE","non_qualified":"1F468-1F3FD-200D-2764-200D-1F48B-200D-1F468-1F3FE","image":"1f468-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe.png","sheet_x":17,"sheet_y":24,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FF":{"unified":"1F468-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FF","non_qualified":"1F468-1F3FD-200D-2764-200D-1F48B-200D-1F468-1F3FF","image":"1f468-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff.png","sheet_x":17,"sheet_y":25,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FB":{"unified":"1F468-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FB","non_qualified":"1F468-1F3FE-200D-2764-200D-1F48B-200D-1F468-1F3FB","image":"1f468-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb.png","sheet_x":17,"sheet_y":26,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FC":{"unified":"1F468-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FC","non_qualified":"1F468-1F3FE-200D-2764-200D-1F48B-200D-1F468-1F3FC","image":"1f468-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc.png","sheet_x":17,"sheet_y":27,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FD":{"unified":"1F468-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FD","non_qualified":"1F468-1F3FE-200D-2764-200D-1F48B-200D-1F468-1F3FD","image":"1f468-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd.png","sheet_x":17,"sheet_y":28,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FE":{"unified":"1F468-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FE","non_qualified":"1F468-1F3FE-200D-2764-200D-1F48B-200D-1F468-1F3FE","image":"1f468-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe.png","sheet_x":17,"sheet_y":29,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FF":{"unified":"1F468-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FF","non_qualified":"1F468-1F3FE-200D-2764-200D-1F48B-200D-1F468-1F3FF","image":"1f468-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff.png","sheet_x":17,"sheet_y":30,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FB":{"unified":"1F468-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FB","non_qualified":"1F468-1F3FF-200D-2764-200D-1F48B-200D-1F468-1F3FB","image":"1f468-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb.png","sheet_x":17,"sheet_y":31,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FC":{"unified":"1F468-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FC","non_qualified":"1F468-1F3FF-200D-2764-200D-1F48B-200D-1F468-1F3FC","image":"1f468-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc.png","sheet_x":17,"sheet_y":32,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FD":{"unified":"1F468-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FD","non_qualified":"1F468-1F3FF-200D-2764-200D-1F48B-200D-1F468-1F3FD","image":"1f468-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd.png","sheet_x":17,"sheet_y":33,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FE":{"unified":"1F468-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FE","non_qualified":"1F468-1F3FF-200D-2764-200D-1F48B-200D-1F468-1F3FE","image":"1f468-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe.png","sheet_x":17,"sheet_y":34,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FF":{"unified":"1F468-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FF","non_qualified":"1F468-1F3FF-200D-2764-200D-1F48B-200D-1F468-1F3FF","image":"1f468-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff.png","sheet_x":17,"sheet_y":35,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN","unified":"1F468","non_qualified":null,"docomo":"E6F0","au":"E4FC","softbank":"E004","google":"FE19D","image":"1f468.png","sheet_x":17,"sheet_y":36,"short_name":"man","short_names":["man"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":236,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F468-1F3FB","non_qualified":null,"image":"1f468-1f3fb.png","sheet_x":17,"sheet_y":37,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F468-1F3FC","non_qualified":null,"image":"1f468-1f3fc.png","sheet_x":17,"sheet_y":38,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F468-1F3FD","non_qualified":null,"image":"1f468-1f3fd.png","sheet_x":17,"sheet_y":39,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F468-1F3FE","non_qualified":null,"image":"1f468-1f3fe.png","sheet_x":17,"sheet_y":40,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F468-1F3FF","non_qualified":null,"image":"1f468-1f3ff.png","sheet_x":17,"sheet_y":41,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN FARMER","unified":"1F469-200D-1F33E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f33e.png","sheet_x":17,"sheet_y":42,"short_name":"female-farmer","short_names":["female-farmer"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":302,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F33E","non_qualified":null,"image":"1f469-1f3fb-200d-1f33e.png","sheet_x":17,"sheet_y":43,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-1F33E","non_qualified":null,"image":"1f469-1f3fc-200d-1f33e.png","sheet_x":17,"sheet_y":44,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-1F33E","non_qualified":null,"image":"1f469-1f3fd-200d-1f33e.png","sheet_x":17,"sheet_y":45,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-1F33E","non_qualified":null,"image":"1f469-1f3fe-200d-1f33e.png","sheet_x":17,"sheet_y":46,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-1F33E","non_qualified":null,"image":"1f469-1f3ff-200d-1f33e.png","sheet_x":17,"sheet_y":47,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN COOK","unified":"1F469-200D-1F373","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f373.png","sheet_x":17,"sheet_y":48,"short_name":"female-cook","short_names":["female-cook"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":305,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F373","non_qualified":null,"image":"1f469-1f3fb-200d-1f373.png","sheet_x":17,"sheet_y":49,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-1F373","non_qualified":null,"image":"1f469-1f3fc-200d-1f373.png","sheet_x":17,"sheet_y":50,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-1F373","non_qualified":null,"image":"1f469-1f3fd-200d-1f373.png","sheet_x":17,"sheet_y":51,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-1F373","non_qualified":null,"image":"1f469-1f3fe-200d-1f373.png","sheet_x":17,"sheet_y":52,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-1F373","non_qualified":null,"image":"1f469-1f3ff-200d-1f373.png","sheet_x":17,"sheet_y":53,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN FEEDING BABY","unified":"1F469-200D-1F37C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f37c.png","sheet_x":17,"sheet_y":54,"short_name":"woman_feeding_baby","short_names":["woman_feeding_baby"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":367,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F37C","non_qualified":null,"image":"1f469-1f3fb-200d-1f37c.png","sheet_x":17,"sheet_y":55,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-1F37C","non_qualified":null,"image":"1f469-1f3fc-200d-1f37c.png","sheet_x":17,"sheet_y":56,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-1F37C","non_qualified":null,"image":"1f469-1f3fd-200d-1f37c.png","sheet_x":17,"sheet_y":57,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-1F37C","non_qualified":null,"image":"1f469-1f3fe-200d-1f37c.png","sheet_x":17,"sheet_y":58,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-1F37C","non_qualified":null,"image":"1f469-1f3ff-200d-1f37c.png","sheet_x":17,"sheet_y":59,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN STUDENT","unified":"1F469-200D-1F393","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f393.png","sheet_x":17,"sheet_y":60,"short_name":"female-student","short_names":["female-student"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":293,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F393","non_qualified":null,"image":"1f469-1f3fb-200d-1f393.png","sheet_x":17,"sheet_y":61,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-1F393","non_qualified":null,"image":"1f469-1f3fc-200d-1f393.png","sheet_x":18,"sheet_y":0,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-1F393","non_qualified":null,"image":"1f469-1f3fd-200d-1f393.png","sheet_x":18,"sheet_y":1,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-1F393","non_qualified":null,"image":"1f469-1f3fe-200d-1f393.png","sheet_x":18,"sheet_y":2,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-1F393","non_qualified":null,"image":"1f469-1f3ff-200d-1f393.png","sheet_x":18,"sheet_y":3,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN SINGER","unified":"1F469-200D-1F3A4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f3a4.png","sheet_x":18,"sheet_y":4,"short_name":"female-singer","short_names":["female-singer"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":323,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F3A4","non_qualified":null,"image":"1f469-1f3fb-200d-1f3a4.png","sheet_x":18,"sheet_y":5,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-1F3A4","non_qualified":null,"image":"1f469-1f3fc-200d-1f3a4.png","sheet_x":18,"sheet_y":6,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-1F3A4","non_qualified":null,"image":"1f469-1f3fd-200d-1f3a4.png","sheet_x":18,"sheet_y":7,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-1F3A4","non_qualified":null,"image":"1f469-1f3fe-200d-1f3a4.png","sheet_x":18,"sheet_y":8,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-1F3A4","non_qualified":null,"image":"1f469-1f3ff-200d-1f3a4.png","sheet_x":18,"sheet_y":9,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN ARTIST","unified":"1F469-200D-1F3A8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f3a8.png","sheet_x":18,"sheet_y":10,"short_name":"female-artist","short_names":["female-artist"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":326,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F3A8","non_qualified":null,"image":"1f469-1f3fb-200d-1f3a8.png","sheet_x":18,"sheet_y":11,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-1F3A8","non_qualified":null,"image":"1f469-1f3fc-200d-1f3a8.png","sheet_x":18,"sheet_y":12,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-1F3A8","non_qualified":null,"image":"1f469-1f3fd-200d-1f3a8.png","sheet_x":18,"sheet_y":13,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-1F3A8","non_qualified":null,"image":"1f469-1f3fe-200d-1f3a8.png","sheet_x":18,"sheet_y":14,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-1F3A8","non_qualified":null,"image":"1f469-1f3ff-200d-1f3a8.png","sheet_x":18,"sheet_y":15,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN TEACHER","unified":"1F469-200D-1F3EB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f3eb.png","sheet_x":18,"sheet_y":16,"short_name":"female-teacher","short_names":["female-teacher"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":296,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F3EB","non_qualified":null,"image":"1f469-1f3fb-200d-1f3eb.png","sheet_x":18,"sheet_y":17,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-1F3EB","non_qualified":null,"image":"1f469-1f3fc-200d-1f3eb.png","sheet_x":18,"sheet_y":18,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-1F3EB","non_qualified":null,"image":"1f469-1f3fd-200d-1f3eb.png","sheet_x":18,"sheet_y":19,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-1F3EB","non_qualified":null,"image":"1f469-1f3fe-200d-1f3eb.png","sheet_x":18,"sheet_y":20,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-1F3EB","non_qualified":null,"image":"1f469-1f3ff-200d-1f3eb.png","sheet_x":18,"sheet_y":21,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN FACTORY WORKER","unified":"1F469-200D-1F3ED","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f3ed.png","sheet_x":18,"sheet_y":22,"short_name":"female-factory-worker","short_names":["female-factory-worker"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":311,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F3ED","non_qualified":null,"image":"1f469-1f3fb-200d-1f3ed.png","sheet_x":18,"sheet_y":23,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-1F3ED","non_qualified":null,"image":"1f469-1f3fc-200d-1f3ed.png","sheet_x":18,"sheet_y":24,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-1F3ED","non_qualified":null,"image":"1f469-1f3fd-200d-1f3ed.png","sheet_x":18,"sheet_y":25,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-1F3ED","non_qualified":null,"image":"1f469-1f3fe-200d-1f3ed.png","sheet_x":18,"sheet_y":26,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-1F3ED","non_qualified":null,"image":"1f469-1f3ff-200d-1f3ed.png","sheet_x":18,"sheet_y":27,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"FAMILY: WOMAN, BOY, BOY","unified":"1F469-200D-1F466-200D-1F466","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f466-200d-1f466.png","sheet_x":18,"sheet_y":28,"short_name":"woman-boy-boy","short_names":["woman-boy-boy"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":540,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAMILY: WOMAN, BOY","unified":"1F469-200D-1F466","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f466.png","sheet_x":18,"sheet_y":29,"short_name":"woman-boy","short_names":["woman-boy"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":539,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAMILY: WOMAN, GIRL, BOY","unified":"1F469-200D-1F467-200D-1F466","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f467-200d-1f466.png","sheet_x":18,"sheet_y":30,"short_name":"woman-girl-boy","short_names":["woman-girl-boy"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":542,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAMILY: WOMAN, GIRL, GIRL","unified":"1F469-200D-1F467-200D-1F467","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f467-200d-1f467.png","sheet_x":18,"sheet_y":31,"short_name":"woman-girl-girl","short_names":["woman-girl-girl"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":543,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAMILY: WOMAN, GIRL","unified":"1F469-200D-1F467","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f467.png","sheet_x":18,"sheet_y":32,"short_name":"woman-girl","short_names":["woman-girl"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":541,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAMILY: WOMAN, WOMAN, BOY","unified":"1F469-200D-1F469-200D-1F466","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f469-200d-1f466.png","sheet_x":18,"sheet_y":33,"short_name":"woman-woman-boy","short_names":["woman-woman-boy"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":529,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAMILY: WOMAN, WOMAN, BOY, BOY","unified":"1F469-200D-1F469-200D-1F466-200D-1F466","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f469-200d-1f466-200d-1f466.png","sheet_x":18,"sheet_y":34,"short_name":"woman-woman-boy-boy","short_names":["woman-woman-boy-boy"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":532,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAMILY: WOMAN, WOMAN, GIRL","unified":"1F469-200D-1F469-200D-1F467","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f469-200d-1f467.png","sheet_x":18,"sheet_y":35,"short_name":"woman-woman-girl","short_names":["woman-woman-girl"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":530,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAMILY: WOMAN, WOMAN, GIRL, BOY","unified":"1F469-200D-1F469-200D-1F467-200D-1F466","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f469-200d-1f467-200d-1f466.png","sheet_x":18,"sheet_y":36,"short_name":"woman-woman-girl-boy","short_names":["woman-woman-girl-boy"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":531,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAMILY: WOMAN, WOMAN, GIRL, GIRL","unified":"1F469-200D-1F469-200D-1F467-200D-1F467","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f469-200d-1f467-200d-1f467.png","sheet_x":18,"sheet_y":37,"short_name":"woman-woman-girl-girl","short_names":["woman-woman-girl-girl"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":533,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WOMAN TECHNOLOGIST","unified":"1F469-200D-1F4BB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f4bb.png","sheet_x":18,"sheet_y":38,"short_name":"female-technologist","short_names":["female-technologist"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":320,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F4BB","non_qualified":null,"image":"1f469-1f3fb-200d-1f4bb.png","sheet_x":18,"sheet_y":39,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-1F4BB","non_qualified":null,"image":"1f469-1f3fc-200d-1f4bb.png","sheet_x":18,"sheet_y":40,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-1F4BB","non_qualified":null,"image":"1f469-1f3fd-200d-1f4bb.png","sheet_x":18,"sheet_y":41,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-1F4BB","non_qualified":null,"image":"1f469-1f3fe-200d-1f4bb.png","sheet_x":18,"sheet_y":42,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-1F4BB","non_qualified":null,"image":"1f469-1f3ff-200d-1f4bb.png","sheet_x":18,"sheet_y":43,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN OFFICE WORKER","unified":"1F469-200D-1F4BC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f4bc.png","sheet_x":18,"sheet_y":44,"short_name":"female-office-worker","short_names":["female-office-worker"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":314,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F4BC","non_qualified":null,"image":"1f469-1f3fb-200d-1f4bc.png","sheet_x":18,"sheet_y":45,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-1F4BC","non_qualified":null,"image":"1f469-1f3fc-200d-1f4bc.png","sheet_x":18,"sheet_y":46,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-1F4BC","non_qualified":null,"image":"1f469-1f3fd-200d-1f4bc.png","sheet_x":18,"sheet_y":47,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-1F4BC","non_qualified":null,"image":"1f469-1f3fe-200d-1f4bc.png","sheet_x":18,"sheet_y":48,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-1F4BC","non_qualified":null,"image":"1f469-1f3ff-200d-1f4bc.png","sheet_x":18,"sheet_y":49,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN MECHANIC","unified":"1F469-200D-1F527","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f527.png","sheet_x":18,"sheet_y":50,"short_name":"female-mechanic","short_names":["female-mechanic"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":308,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F527","non_qualified":null,"image":"1f469-1f3fb-200d-1f527.png","sheet_x":18,"sheet_y":51,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-1F527","non_qualified":null,"image":"1f469-1f3fc-200d-1f527.png","sheet_x":18,"sheet_y":52,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-1F527","non_qualified":null,"image":"1f469-1f3fd-200d-1f527.png","sheet_x":18,"sheet_y":53,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-1F527","non_qualified":null,"image":"1f469-1f3fe-200d-1f527.png","sheet_x":18,"sheet_y":54,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-1F527","non_qualified":null,"image":"1f469-1f3ff-200d-1f527.png","sheet_x":18,"sheet_y":55,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN SCIENTIST","unified":"1F469-200D-1F52C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f52c.png","sheet_x":18,"sheet_y":56,"short_name":"female-scientist","short_names":["female-scientist"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":317,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F52C","non_qualified":null,"image":"1f469-1f3fb-200d-1f52c.png","sheet_x":18,"sheet_y":57,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-1F52C","non_qualified":null,"image":"1f469-1f3fc-200d-1f52c.png","sheet_x":18,"sheet_y":58,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-1F52C","non_qualified":null,"image":"1f469-1f3fd-200d-1f52c.png","sheet_x":18,"sheet_y":59,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-1F52C","non_qualified":null,"image":"1f469-1f3fe-200d-1f52c.png","sheet_x":18,"sheet_y":60,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-1F52C","non_qualified":null,"image":"1f469-1f3ff-200d-1f52c.png","sheet_x":18,"sheet_y":61,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN ASTRONAUT","unified":"1F469-200D-1F680","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f680.png","sheet_x":19,"sheet_y":0,"short_name":"female-astronaut","short_names":["female-astronaut"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":332,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F680","non_qualified":null,"image":"1f469-1f3fb-200d-1f680.png","sheet_x":19,"sheet_y":1,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-1F680","non_qualified":null,"image":"1f469-1f3fc-200d-1f680.png","sheet_x":19,"sheet_y":2,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-1F680","non_qualified":null,"image":"1f469-1f3fd-200d-1f680.png","sheet_x":19,"sheet_y":3,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-1F680","non_qualified":null,"image":"1f469-1f3fe-200d-1f680.png","sheet_x":19,"sheet_y":4,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-1F680","non_qualified":null,"image":"1f469-1f3ff-200d-1f680.png","sheet_x":19,"sheet_y":5,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN FIREFIGHTER","unified":"1F469-200D-1F692","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f692.png","sheet_x":19,"sheet_y":6,"short_name":"female-firefighter","short_names":["female-firefighter"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":335,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F692","non_qualified":null,"image":"1f469-1f3fb-200d-1f692.png","sheet_x":19,"sheet_y":7,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-1F692","non_qualified":null,"image":"1f469-1f3fc-200d-1f692.png","sheet_x":19,"sheet_y":8,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-1F692","non_qualified":null,"image":"1f469-1f3fd-200d-1f692.png","sheet_x":19,"sheet_y":9,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-1F692","non_qualified":null,"image":"1f469-1f3fe-200d-1f692.png","sheet_x":19,"sheet_y":10,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-1F692","non_qualified":null,"image":"1f469-1f3ff-200d-1f692.png","sheet_x":19,"sheet_y":11,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN WITH WHITE CANE FACING RIGHT","unified":"1F469-200D-1F9AF-200D-27A1-FE0F","non_qualified":"1F469-200D-1F9AF-200D-27A1","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f9af-200d-27a1-fe0f.png","sheet_x":19,"sheet_y":12,"short_name":"woman_with_white_cane_facing_right","short_names":["woman_with_white_cane_facing_right"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":428,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F9AF-200D-27A1-FE0F","non_qualified":"1F469-1F3FB-200D-1F9AF-200D-27A1","image":"1f469-1f3fb-200d-1f9af-200d-27a1-fe0f.png","sheet_x":19,"sheet_y":13,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FC":{"unified":"1F469-1F3FC-200D-1F9AF-200D-27A1-FE0F","non_qualified":"1F469-1F3FC-200D-1F9AF-200D-27A1","image":"1f469-1f3fc-200d-1f9af-200d-27a1-fe0f.png","sheet_x":19,"sheet_y":14,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FD":{"unified":"1F469-1F3FD-200D-1F9AF-200D-27A1-FE0F","non_qualified":"1F469-1F3FD-200D-1F9AF-200D-27A1","image":"1f469-1f3fd-200d-1f9af-200d-27a1-fe0f.png","sheet_x":19,"sheet_y":15,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FE":{"unified":"1F469-1F3FE-200D-1F9AF-200D-27A1-FE0F","non_qualified":"1F469-1F3FE-200D-1F9AF-200D-27A1","image":"1f469-1f3fe-200d-1f9af-200d-27a1-fe0f.png","sheet_x":19,"sheet_y":16,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FF":{"unified":"1F469-1F3FF-200D-1F9AF-200D-27A1-FE0F","non_qualified":"1F469-1F3FF-200D-1F9AF-200D-27A1","image":"1f469-1f3ff-200d-1f9af-200d-27a1-fe0f.png","sheet_x":19,"sheet_y":17,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false}}},{"name":"WOMAN WITH WHITE CANE","unified":"1F469-200D-1F9AF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f9af.png","sheet_x":19,"sheet_y":18,"short_name":"woman_with_probing_cane","short_names":["woman_with_probing_cane"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":427,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F9AF","non_qualified":null,"image":"1f469-1f3fb-200d-1f9af.png","sheet_x":19,"sheet_y":19,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-1F9AF","non_qualified":null,"image":"1f469-1f3fc-200d-1f9af.png","sheet_x":19,"sheet_y":20,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-1F9AF","non_qualified":null,"image":"1f469-1f3fd-200d-1f9af.png","sheet_x":19,"sheet_y":21,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-1F9AF","non_qualified":null,"image":"1f469-1f3fe-200d-1f9af.png","sheet_x":19,"sheet_y":22,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-1F9AF","non_qualified":null,"image":"1f469-1f3ff-200d-1f9af.png","sheet_x":19,"sheet_y":23,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN: RED HAIR","unified":"1F469-200D-1F9B0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f9b0.png","sheet_x":19,"sheet_y":24,"short_name":"red_haired_woman","short_names":["red_haired_woman"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":245,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F9B0","non_qualified":null,"image":"1f469-1f3fb-200d-1f9b0.png","sheet_x":19,"sheet_y":25,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-1F9B0","non_qualified":null,"image":"1f469-1f3fc-200d-1f9b0.png","sheet_x":19,"sheet_y":26,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-1F9B0","non_qualified":null,"image":"1f469-1f3fd-200d-1f9b0.png","sheet_x":19,"sheet_y":27,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-1F9B0","non_qualified":null,"image":"1f469-1f3fe-200d-1f9b0.png","sheet_x":19,"sheet_y":28,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-1F9B0","non_qualified":null,"image":"1f469-1f3ff-200d-1f9b0.png","sheet_x":19,"sheet_y":29,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN: CURLY HAIR","unified":"1F469-200D-1F9B1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f9b1.png","sheet_x":19,"sheet_y":30,"short_name":"curly_haired_woman","short_names":["curly_haired_woman"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":247,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F9B1","non_qualified":null,"image":"1f469-1f3fb-200d-1f9b1.png","sheet_x":19,"sheet_y":31,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-1F9B1","non_qualified":null,"image":"1f469-1f3fc-200d-1f9b1.png","sheet_x":19,"sheet_y":32,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-1F9B1","non_qualified":null,"image":"1f469-1f3fd-200d-1f9b1.png","sheet_x":19,"sheet_y":33,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-1F9B1","non_qualified":null,"image":"1f469-1f3fe-200d-1f9b1.png","sheet_x":19,"sheet_y":34,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-1F9B1","non_qualified":null,"image":"1f469-1f3ff-200d-1f9b1.png","sheet_x":19,"sheet_y":35,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN: BALD","unified":"1F469-200D-1F9B2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f9b2.png","sheet_x":19,"sheet_y":36,"short_name":"bald_woman","short_names":["bald_woman"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":251,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F9B2","non_qualified":null,"image":"1f469-1f3fb-200d-1f9b2.png","sheet_x":19,"sheet_y":37,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-1F9B2","non_qualified":null,"image":"1f469-1f3fc-200d-1f9b2.png","sheet_x":19,"sheet_y":38,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-1F9B2","non_qualified":null,"image":"1f469-1f3fd-200d-1f9b2.png","sheet_x":19,"sheet_y":39,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-1F9B2","non_qualified":null,"image":"1f469-1f3fe-200d-1f9b2.png","sheet_x":19,"sheet_y":40,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-1F9B2","non_qualified":null,"image":"1f469-1f3ff-200d-1f9b2.png","sheet_x":19,"sheet_y":41,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN: WHITE HAIR","unified":"1F469-200D-1F9B3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f9b3.png","sheet_x":19,"sheet_y":42,"short_name":"white_haired_woman","short_names":["white_haired_woman"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":249,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F9B3","non_qualified":null,"image":"1f469-1f3fb-200d-1f9b3.png","sheet_x":19,"sheet_y":43,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-1F9B3","non_qualified":null,"image":"1f469-1f3fc-200d-1f9b3.png","sheet_x":19,"sheet_y":44,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-1F9B3","non_qualified":null,"image":"1f469-1f3fd-200d-1f9b3.png","sheet_x":19,"sheet_y":45,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-1F9B3","non_qualified":null,"image":"1f469-1f3fe-200d-1f9b3.png","sheet_x":19,"sheet_y":46,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-1F9B3","non_qualified":null,"image":"1f469-1f3ff-200d-1f9b3.png","sheet_x":19,"sheet_y":47,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN IN MOTORIZED WHEELCHAIR FACING RIGHT","unified":"1F469-200D-1F9BC-200D-27A1-FE0F","non_qualified":"1F469-200D-1F9BC-200D-27A1","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f9bc-200d-27a1-fe0f.png","sheet_x":19,"sheet_y":48,"short_name":"woman_in_motorized_wheelchair_facing_right","short_names":["woman_in_motorized_wheelchair_facing_right"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":434,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F9BC-200D-27A1-FE0F","non_qualified":"1F469-1F3FB-200D-1F9BC-200D-27A1","image":"1f469-1f3fb-200d-1f9bc-200d-27a1-fe0f.png","sheet_x":19,"sheet_y":49,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FC":{"unified":"1F469-1F3FC-200D-1F9BC-200D-27A1-FE0F","non_qualified":"1F469-1F3FC-200D-1F9BC-200D-27A1","image":"1f469-1f3fc-200d-1f9bc-200d-27a1-fe0f.png","sheet_x":19,"sheet_y":50,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FD":{"unified":"1F469-1F3FD-200D-1F9BC-200D-27A1-FE0F","non_qualified":"1F469-1F3FD-200D-1F9BC-200D-27A1","image":"1f469-1f3fd-200d-1f9bc-200d-27a1-fe0f.png","sheet_x":19,"sheet_y":51,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FE":{"unified":"1F469-1F3FE-200D-1F9BC-200D-27A1-FE0F","non_qualified":"1F469-1F3FE-200D-1F9BC-200D-27A1","image":"1f469-1f3fe-200d-1f9bc-200d-27a1-fe0f.png","sheet_x":19,"sheet_y":52,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FF":{"unified":"1F469-1F3FF-200D-1F9BC-200D-27A1-FE0F","non_qualified":"1F469-1F3FF-200D-1F9BC-200D-27A1","image":"1f469-1f3ff-200d-1f9bc-200d-27a1-fe0f.png","sheet_x":19,"sheet_y":53,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false}}},{"name":"WOMAN IN MOTORIZED WHEELCHAIR","unified":"1F469-200D-1F9BC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f9bc.png","sheet_x":19,"sheet_y":54,"short_name":"woman_in_motorized_wheelchair","short_names":["woman_in_motorized_wheelchair"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":433,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F9BC","non_qualified":null,"image":"1f469-1f3fb-200d-1f9bc.png","sheet_x":19,"sheet_y":55,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-1F9BC","non_qualified":null,"image":"1f469-1f3fc-200d-1f9bc.png","sheet_x":19,"sheet_y":56,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-1F9BC","non_qualified":null,"image":"1f469-1f3fd-200d-1f9bc.png","sheet_x":19,"sheet_y":57,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-1F9BC","non_qualified":null,"image":"1f469-1f3fe-200d-1f9bc.png","sheet_x":19,"sheet_y":58,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-1F9BC","non_qualified":null,"image":"1f469-1f3ff-200d-1f9bc.png","sheet_x":19,"sheet_y":59,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN IN MANUAL WHEELCHAIR FACING RIGHT","unified":"1F469-200D-1F9BD-200D-27A1-FE0F","non_qualified":"1F469-200D-1F9BD-200D-27A1","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f9bd-200d-27a1-fe0f.png","sheet_x":19,"sheet_y":60,"short_name":"woman_in_manual_wheelchair_facing_right","short_names":["woman_in_manual_wheelchair_facing_right"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":440,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F9BD-200D-27A1-FE0F","non_qualified":"1F469-1F3FB-200D-1F9BD-200D-27A1","image":"1f469-1f3fb-200d-1f9bd-200d-27a1-fe0f.png","sheet_x":19,"sheet_y":61,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FC":{"unified":"1F469-1F3FC-200D-1F9BD-200D-27A1-FE0F","non_qualified":"1F469-1F3FC-200D-1F9BD-200D-27A1","image":"1f469-1f3fc-200d-1f9bd-200d-27a1-fe0f.png","sheet_x":20,"sheet_y":0,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FD":{"unified":"1F469-1F3FD-200D-1F9BD-200D-27A1-FE0F","non_qualified":"1F469-1F3FD-200D-1F9BD-200D-27A1","image":"1f469-1f3fd-200d-1f9bd-200d-27a1-fe0f.png","sheet_x":20,"sheet_y":1,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FE":{"unified":"1F469-1F3FE-200D-1F9BD-200D-27A1-FE0F","non_qualified":"1F469-1F3FE-200D-1F9BD-200D-27A1","image":"1f469-1f3fe-200d-1f9bd-200d-27a1-fe0f.png","sheet_x":20,"sheet_y":2,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FF":{"unified":"1F469-1F3FF-200D-1F9BD-200D-27A1-FE0F","non_qualified":"1F469-1F3FF-200D-1F9BD-200D-27A1","image":"1f469-1f3ff-200d-1f9bd-200d-27a1-fe0f.png","sheet_x":20,"sheet_y":3,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false}}},{"name":"WOMAN IN MANUAL WHEELCHAIR","unified":"1F469-200D-1F9BD","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-1f9bd.png","sheet_x":20,"sheet_y":4,"short_name":"woman_in_manual_wheelchair","short_names":["woman_in_manual_wheelchair"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":439,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-1F9BD","non_qualified":null,"image":"1f469-1f3fb-200d-1f9bd.png","sheet_x":20,"sheet_y":5,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-1F9BD","non_qualified":null,"image":"1f469-1f3fc-200d-1f9bd.png","sheet_x":20,"sheet_y":6,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-1F9BD","non_qualified":null,"image":"1f469-1f3fd-200d-1f9bd.png","sheet_x":20,"sheet_y":7,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-1F9BD","non_qualified":null,"image":"1f469-1f3fe-200d-1f9bd.png","sheet_x":20,"sheet_y":8,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-1F9BD","non_qualified":null,"image":"1f469-1f3ff-200d-1f9bd.png","sheet_x":20,"sheet_y":9,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN HEALTH WORKER","unified":"1F469-200D-2695-FE0F","non_qualified":"1F469-200D-2695","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-2695-fe0f.png","sheet_x":20,"sheet_y":10,"short_name":"female-doctor","short_names":["female-doctor"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":290,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-2695-FE0F","non_qualified":"1F469-1F3FB-200D-2695","image":"1f469-1f3fb-200d-2695-fe0f.png","sheet_x":20,"sheet_y":11,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-2695-FE0F","non_qualified":"1F469-1F3FC-200D-2695","image":"1f469-1f3fc-200d-2695-fe0f.png","sheet_x":20,"sheet_y":12,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-2695-FE0F","non_qualified":"1F469-1F3FD-200D-2695","image":"1f469-1f3fd-200d-2695-fe0f.png","sheet_x":20,"sheet_y":13,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-2695-FE0F","non_qualified":"1F469-1F3FE-200D-2695","image":"1f469-1f3fe-200d-2695-fe0f.png","sheet_x":20,"sheet_y":14,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-2695-FE0F","non_qualified":"1F469-1F3FF-200D-2695","image":"1f469-1f3ff-200d-2695-fe0f.png","sheet_x":20,"sheet_y":15,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN JUDGE","unified":"1F469-200D-2696-FE0F","non_qualified":"1F469-200D-2696","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-2696-fe0f.png","sheet_x":20,"sheet_y":16,"short_name":"female-judge","short_names":["female-judge"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":299,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-2696-FE0F","non_qualified":"1F469-1F3FB-200D-2696","image":"1f469-1f3fb-200d-2696-fe0f.png","sheet_x":20,"sheet_y":17,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-2696-FE0F","non_qualified":"1F469-1F3FC-200D-2696","image":"1f469-1f3fc-200d-2696-fe0f.png","sheet_x":20,"sheet_y":18,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-2696-FE0F","non_qualified":"1F469-1F3FD-200D-2696","image":"1f469-1f3fd-200d-2696-fe0f.png","sheet_x":20,"sheet_y":19,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-2696-FE0F","non_qualified":"1F469-1F3FE-200D-2696","image":"1f469-1f3fe-200d-2696-fe0f.png","sheet_x":20,"sheet_y":20,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-2696-FE0F","non_qualified":"1F469-1F3FF-200D-2696","image":"1f469-1f3ff-200d-2696-fe0f.png","sheet_x":20,"sheet_y":21,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN PILOT","unified":"1F469-200D-2708-FE0F","non_qualified":"1F469-200D-2708","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-2708-fe0f.png","sheet_x":20,"sheet_y":22,"short_name":"female-pilot","short_names":["female-pilot"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":329,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB-200D-2708-FE0F","non_qualified":"1F469-1F3FB-200D-2708","image":"1f469-1f3fb-200d-2708-fe0f.png","sheet_x":20,"sheet_y":23,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC-200D-2708-FE0F","non_qualified":"1F469-1F3FC-200D-2708","image":"1f469-1f3fc-200d-2708-fe0f.png","sheet_x":20,"sheet_y":24,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD-200D-2708-FE0F","non_qualified":"1F469-1F3FD-200D-2708","image":"1f469-1f3fd-200d-2708-fe0f.png","sheet_x":20,"sheet_y":25,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE-200D-2708-FE0F","non_qualified":"1F469-1F3FE-200D-2708","image":"1f469-1f3fe-200d-2708-fe0f.png","sheet_x":20,"sheet_y":26,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF-200D-2708-FE0F","non_qualified":"1F469-1F3FF-200D-2708","image":"1f469-1f3ff-200d-2708-fe0f.png","sheet_x":20,"sheet_y":27,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"COUPLE WITH HEART: WOMAN, MAN","unified":"1F469-200D-2764-FE0F-200D-1F468","non_qualified":"1F469-200D-2764-200D-1F468","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-2764-fe0f-200d-1f468.png","sheet_x":20,"sheet_y":28,"short_name":"woman-heart-man","short_names":["woman-heart-man"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":516,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB-1F3FB":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F468-1F3FB","non_qualified":"1F469-1F3FB-200D-2764-200D-1F468-1F3FB","image":"1f469-1f3fb-200d-2764-fe0f-200d-1f468-1f3fb.png","sheet_x":20,"sheet_y":29,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FC":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F468-1F3FC","non_qualified":"1F469-1F3FB-200D-2764-200D-1F468-1F3FC","image":"1f469-1f3fb-200d-2764-fe0f-200d-1f468-1f3fc.png","sheet_x":20,"sheet_y":30,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FD":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F468-1F3FD","non_qualified":"1F469-1F3FB-200D-2764-200D-1F468-1F3FD","image":"1f469-1f3fb-200d-2764-fe0f-200d-1f468-1f3fd.png","sheet_x":20,"sheet_y":31,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FE":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F468-1F3FE","non_qualified":"1F469-1F3FB-200D-2764-200D-1F468-1F3FE","image":"1f469-1f3fb-200d-2764-fe0f-200d-1f468-1f3fe.png","sheet_x":20,"sheet_y":32,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FF":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F468-1F3FF","non_qualified":"1F469-1F3FB-200D-2764-200D-1F468-1F3FF","image":"1f469-1f3fb-200d-2764-fe0f-200d-1f468-1f3ff.png","sheet_x":20,"sheet_y":33,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FB":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F468-1F3FB","non_qualified":"1F469-1F3FC-200D-2764-200D-1F468-1F3FB","image":"1f469-1f3fc-200d-2764-fe0f-200d-1f468-1f3fb.png","sheet_x":20,"sheet_y":34,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FC":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F468-1F3FC","non_qualified":"1F469-1F3FC-200D-2764-200D-1F468-1F3FC","image":"1f469-1f3fc-200d-2764-fe0f-200d-1f468-1f3fc.png","sheet_x":20,"sheet_y":35,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FD":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F468-1F3FD","non_qualified":"1F469-1F3FC-200D-2764-200D-1F468-1F3FD","image":"1f469-1f3fc-200d-2764-fe0f-200d-1f468-1f3fd.png","sheet_x":20,"sheet_y":36,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FE":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F468-1F3FE","non_qualified":"1F469-1F3FC-200D-2764-200D-1F468-1F3FE","image":"1f469-1f3fc-200d-2764-fe0f-200d-1f468-1f3fe.png","sheet_x":20,"sheet_y":37,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FF":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F468-1F3FF","non_qualified":"1F469-1F3FC-200D-2764-200D-1F468-1F3FF","image":"1f469-1f3fc-200d-2764-fe0f-200d-1f468-1f3ff.png","sheet_x":20,"sheet_y":38,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FB":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F468-1F3FB","non_qualified":"1F469-1F3FD-200D-2764-200D-1F468-1F3FB","image":"1f469-1f3fd-200d-2764-fe0f-200d-1f468-1f3fb.png","sheet_x":20,"sheet_y":39,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FC":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F468-1F3FC","non_qualified":"1F469-1F3FD-200D-2764-200D-1F468-1F3FC","image":"1f469-1f3fd-200d-2764-fe0f-200d-1f468-1f3fc.png","sheet_x":20,"sheet_y":40,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FD":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F468-1F3FD","non_qualified":"1F469-1F3FD-200D-2764-200D-1F468-1F3FD","image":"1f469-1f3fd-200d-2764-fe0f-200d-1f468-1f3fd.png","sheet_x":20,"sheet_y":41,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FE":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F468-1F3FE","non_qualified":"1F469-1F3FD-200D-2764-200D-1F468-1F3FE","image":"1f469-1f3fd-200d-2764-fe0f-200d-1f468-1f3fe.png","sheet_x":20,"sheet_y":42,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FF":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F468-1F3FF","non_qualified":"1F469-1F3FD-200D-2764-200D-1F468-1F3FF","image":"1f469-1f3fd-200d-2764-fe0f-200d-1f468-1f3ff.png","sheet_x":20,"sheet_y":43,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FB":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F468-1F3FB","non_qualified":"1F469-1F3FE-200D-2764-200D-1F468-1F3FB","image":"1f469-1f3fe-200d-2764-fe0f-200d-1f468-1f3fb.png","sheet_x":20,"sheet_y":44,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FC":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F468-1F3FC","non_qualified":"1F469-1F3FE-200D-2764-200D-1F468-1F3FC","image":"1f469-1f3fe-200d-2764-fe0f-200d-1f468-1f3fc.png","sheet_x":20,"sheet_y":45,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FD":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F468-1F3FD","non_qualified":"1F469-1F3FE-200D-2764-200D-1F468-1F3FD","image":"1f469-1f3fe-200d-2764-fe0f-200d-1f468-1f3fd.png","sheet_x":20,"sheet_y":46,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FE":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F468-1F3FE","non_qualified":"1F469-1F3FE-200D-2764-200D-1F468-1F3FE","image":"1f469-1f3fe-200d-2764-fe0f-200d-1f468-1f3fe.png","sheet_x":20,"sheet_y":47,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FF":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F468-1F3FF","non_qualified":"1F469-1F3FE-200D-2764-200D-1F468-1F3FF","image":"1f469-1f3fe-200d-2764-fe0f-200d-1f468-1f3ff.png","sheet_x":20,"sheet_y":48,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FB":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F468-1F3FB","non_qualified":"1F469-1F3FF-200D-2764-200D-1F468-1F3FB","image":"1f469-1f3ff-200d-2764-fe0f-200d-1f468-1f3fb.png","sheet_x":20,"sheet_y":49,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FC":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F468-1F3FC","non_qualified":"1F469-1F3FF-200D-2764-200D-1F468-1F3FC","image":"1f469-1f3ff-200d-2764-fe0f-200d-1f468-1f3fc.png","sheet_x":20,"sheet_y":50,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FD":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F468-1F3FD","non_qualified":"1F469-1F3FF-200D-2764-200D-1F468-1F3FD","image":"1f469-1f3ff-200d-2764-fe0f-200d-1f468-1f3fd.png","sheet_x":20,"sheet_y":51,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FE":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F468-1F3FE","non_qualified":"1F469-1F3FF-200D-2764-200D-1F468-1F3FE","image":"1f469-1f3ff-200d-2764-fe0f-200d-1f468-1f3fe.png","sheet_x":20,"sheet_y":52,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FF":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F468-1F3FF","non_qualified":"1F469-1F3FF-200D-2764-200D-1F468-1F3FF","image":"1f469-1f3ff-200d-2764-fe0f-200d-1f468-1f3ff.png","sheet_x":20,"sheet_y":53,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"COUPLE WITH HEART: WOMAN, WOMAN","unified":"1F469-200D-2764-FE0F-200D-1F469","non_qualified":"1F469-200D-2764-200D-1F469","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-2764-fe0f-200d-1f469.png","sheet_x":20,"sheet_y":54,"short_name":"woman-heart-woman","short_names":["woman-heart-woman"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":518,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB-1F3FB":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F469-1F3FB","non_qualified":"1F469-1F3FB-200D-2764-200D-1F469-1F3FB","image":"1f469-1f3fb-200d-2764-fe0f-200d-1f469-1f3fb.png","sheet_x":20,"sheet_y":55,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FC":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F469-1F3FC","non_qualified":"1F469-1F3FB-200D-2764-200D-1F469-1F3FC","image":"1f469-1f3fb-200d-2764-fe0f-200d-1f469-1f3fc.png","sheet_x":20,"sheet_y":56,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FD":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F469-1F3FD","non_qualified":"1F469-1F3FB-200D-2764-200D-1F469-1F3FD","image":"1f469-1f3fb-200d-2764-fe0f-200d-1f469-1f3fd.png","sheet_x":20,"sheet_y":57,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FE":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F469-1F3FE","non_qualified":"1F469-1F3FB-200D-2764-200D-1F469-1F3FE","image":"1f469-1f3fb-200d-2764-fe0f-200d-1f469-1f3fe.png","sheet_x":20,"sheet_y":58,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FF":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F469-1F3FF","non_qualified":"1F469-1F3FB-200D-2764-200D-1F469-1F3FF","image":"1f469-1f3fb-200d-2764-fe0f-200d-1f469-1f3ff.png","sheet_x":20,"sheet_y":59,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FB":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F469-1F3FB","non_qualified":"1F469-1F3FC-200D-2764-200D-1F469-1F3FB","image":"1f469-1f3fc-200d-2764-fe0f-200d-1f469-1f3fb.png","sheet_x":20,"sheet_y":60,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FC":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F469-1F3FC","non_qualified":"1F469-1F3FC-200D-2764-200D-1F469-1F3FC","image":"1f469-1f3fc-200d-2764-fe0f-200d-1f469-1f3fc.png","sheet_x":20,"sheet_y":61,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FD":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F469-1F3FD","non_qualified":"1F469-1F3FC-200D-2764-200D-1F469-1F3FD","image":"1f469-1f3fc-200d-2764-fe0f-200d-1f469-1f3fd.png","sheet_x":21,"sheet_y":0,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FE":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F469-1F3FE","non_qualified":"1F469-1F3FC-200D-2764-200D-1F469-1F3FE","image":"1f469-1f3fc-200d-2764-fe0f-200d-1f469-1f3fe.png","sheet_x":21,"sheet_y":1,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FF":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F469-1F3FF","non_qualified":"1F469-1F3FC-200D-2764-200D-1F469-1F3FF","image":"1f469-1f3fc-200d-2764-fe0f-200d-1f469-1f3ff.png","sheet_x":21,"sheet_y":2,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FB":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F469-1F3FB","non_qualified":"1F469-1F3FD-200D-2764-200D-1F469-1F3FB","image":"1f469-1f3fd-200d-2764-fe0f-200d-1f469-1f3fb.png","sheet_x":21,"sheet_y":3,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FC":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F469-1F3FC","non_qualified":"1F469-1F3FD-200D-2764-200D-1F469-1F3FC","image":"1f469-1f3fd-200d-2764-fe0f-200d-1f469-1f3fc.png","sheet_x":21,"sheet_y":4,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FD":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F469-1F3FD","non_qualified":"1F469-1F3FD-200D-2764-200D-1F469-1F3FD","image":"1f469-1f3fd-200d-2764-fe0f-200d-1f469-1f3fd.png","sheet_x":21,"sheet_y":5,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FE":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F469-1F3FE","non_qualified":"1F469-1F3FD-200D-2764-200D-1F469-1F3FE","image":"1f469-1f3fd-200d-2764-fe0f-200d-1f469-1f3fe.png","sheet_x":21,"sheet_y":6,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FF":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F469-1F3FF","non_qualified":"1F469-1F3FD-200D-2764-200D-1F469-1F3FF","image":"1f469-1f3fd-200d-2764-fe0f-200d-1f469-1f3ff.png","sheet_x":21,"sheet_y":7,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FB":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F469-1F3FB","non_qualified":"1F469-1F3FE-200D-2764-200D-1F469-1F3FB","image":"1f469-1f3fe-200d-2764-fe0f-200d-1f469-1f3fb.png","sheet_x":21,"sheet_y":8,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FC":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F469-1F3FC","non_qualified":"1F469-1F3FE-200D-2764-200D-1F469-1F3FC","image":"1f469-1f3fe-200d-2764-fe0f-200d-1f469-1f3fc.png","sheet_x":21,"sheet_y":9,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FD":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F469-1F3FD","non_qualified":"1F469-1F3FE-200D-2764-200D-1F469-1F3FD","image":"1f469-1f3fe-200d-2764-fe0f-200d-1f469-1f3fd.png","sheet_x":21,"sheet_y":10,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FE":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F469-1F3FE","non_qualified":"1F469-1F3FE-200D-2764-200D-1F469-1F3FE","image":"1f469-1f3fe-200d-2764-fe0f-200d-1f469-1f3fe.png","sheet_x":21,"sheet_y":11,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FF":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F469-1F3FF","non_qualified":"1F469-1F3FE-200D-2764-200D-1F469-1F3FF","image":"1f469-1f3fe-200d-2764-fe0f-200d-1f469-1f3ff.png","sheet_x":21,"sheet_y":12,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FB":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F469-1F3FB","non_qualified":"1F469-1F3FF-200D-2764-200D-1F469-1F3FB","image":"1f469-1f3ff-200d-2764-fe0f-200d-1f469-1f3fb.png","sheet_x":21,"sheet_y":13,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FC":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F469-1F3FC","non_qualified":"1F469-1F3FF-200D-2764-200D-1F469-1F3FC","image":"1f469-1f3ff-200d-2764-fe0f-200d-1f469-1f3fc.png","sheet_x":21,"sheet_y":14,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FD":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F469-1F3FD","non_qualified":"1F469-1F3FF-200D-2764-200D-1F469-1F3FD","image":"1f469-1f3ff-200d-2764-fe0f-200d-1f469-1f3fd.png","sheet_x":21,"sheet_y":15,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FE":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F469-1F3FE","non_qualified":"1F469-1F3FF-200D-2764-200D-1F469-1F3FE","image":"1f469-1f3ff-200d-2764-fe0f-200d-1f469-1f3fe.png","sheet_x":21,"sheet_y":16,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FF":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F469-1F3FF","non_qualified":"1F469-1F3FF-200D-2764-200D-1F469-1F3FF","image":"1f469-1f3ff-200d-2764-fe0f-200d-1f469-1f3ff.png","sheet_x":21,"sheet_y":17,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"KISS: WOMAN, MAN","unified":"1F469-200D-2764-FE0F-200D-1F48B-200D-1F468","non_qualified":"1F469-200D-2764-200D-1F48B-200D-1F468","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-2764-fe0f-200d-1f48b-200d-1f468.png","sheet_x":21,"sheet_y":18,"short_name":"woman-kiss-man","short_names":["woman-kiss-man"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":512,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB-1F3FB":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FB","non_qualified":"1F469-1F3FB-200D-2764-200D-1F48B-200D-1F468-1F3FB","image":"1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb.png","sheet_x":21,"sheet_y":19,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FC":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FC","non_qualified":"1F469-1F3FB-200D-2764-200D-1F48B-200D-1F468-1F3FC","image":"1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc.png","sheet_x":21,"sheet_y":20,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FD":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FD","non_qualified":"1F469-1F3FB-200D-2764-200D-1F48B-200D-1F468-1F3FD","image":"1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd.png","sheet_x":21,"sheet_y":21,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FE":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FE","non_qualified":"1F469-1F3FB-200D-2764-200D-1F48B-200D-1F468-1F3FE","image":"1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe.png","sheet_x":21,"sheet_y":22,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FF":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FF","non_qualified":"1F469-1F3FB-200D-2764-200D-1F48B-200D-1F468-1F3FF","image":"1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff.png","sheet_x":21,"sheet_y":23,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FB":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FB","non_qualified":"1F469-1F3FC-200D-2764-200D-1F48B-200D-1F468-1F3FB","image":"1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb.png","sheet_x":21,"sheet_y":24,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FC":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FC","non_qualified":"1F469-1F3FC-200D-2764-200D-1F48B-200D-1F468-1F3FC","image":"1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc.png","sheet_x":21,"sheet_y":25,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FD":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FD","non_qualified":"1F469-1F3FC-200D-2764-200D-1F48B-200D-1F468-1F3FD","image":"1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd.png","sheet_x":21,"sheet_y":26,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FE":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FE","non_qualified":"1F469-1F3FC-200D-2764-200D-1F48B-200D-1F468-1F3FE","image":"1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe.png","sheet_x":21,"sheet_y":27,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FF":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FF","non_qualified":"1F469-1F3FC-200D-2764-200D-1F48B-200D-1F468-1F3FF","image":"1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff.png","sheet_x":21,"sheet_y":28,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FB":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FB","non_qualified":"1F469-1F3FD-200D-2764-200D-1F48B-200D-1F468-1F3FB","image":"1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb.png","sheet_x":21,"sheet_y":29,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FC":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FC","non_qualified":"1F469-1F3FD-200D-2764-200D-1F48B-200D-1F468-1F3FC","image":"1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc.png","sheet_x":21,"sheet_y":30,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FD":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FD","non_qualified":"1F469-1F3FD-200D-2764-200D-1F48B-200D-1F468-1F3FD","image":"1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd.png","sheet_x":21,"sheet_y":31,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FE":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FE","non_qualified":"1F469-1F3FD-200D-2764-200D-1F48B-200D-1F468-1F3FE","image":"1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe.png","sheet_x":21,"sheet_y":32,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FF":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FF","non_qualified":"1F469-1F3FD-200D-2764-200D-1F48B-200D-1F468-1F3FF","image":"1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff.png","sheet_x":21,"sheet_y":33,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FB":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FB","non_qualified":"1F469-1F3FE-200D-2764-200D-1F48B-200D-1F468-1F3FB","image":"1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb.png","sheet_x":21,"sheet_y":34,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FC":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FC","non_qualified":"1F469-1F3FE-200D-2764-200D-1F48B-200D-1F468-1F3FC","image":"1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc.png","sheet_x":21,"sheet_y":35,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FD":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FD","non_qualified":"1F469-1F3FE-200D-2764-200D-1F48B-200D-1F468-1F3FD","image":"1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd.png","sheet_x":21,"sheet_y":36,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FE":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FE","non_qualified":"1F469-1F3FE-200D-2764-200D-1F48B-200D-1F468-1F3FE","image":"1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe.png","sheet_x":21,"sheet_y":37,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FF":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FF","non_qualified":"1F469-1F3FE-200D-2764-200D-1F48B-200D-1F468-1F3FF","image":"1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff.png","sheet_x":21,"sheet_y":38,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FB":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FB","non_qualified":"1F469-1F3FF-200D-2764-200D-1F48B-200D-1F468-1F3FB","image":"1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fb.png","sheet_x":21,"sheet_y":39,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FC":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FC","non_qualified":"1F469-1F3FF-200D-2764-200D-1F48B-200D-1F468-1F3FC","image":"1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fc.png","sheet_x":21,"sheet_y":40,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FD":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FD","non_qualified":"1F469-1F3FF-200D-2764-200D-1F48B-200D-1F468-1F3FD","image":"1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fd.png","sheet_x":21,"sheet_y":41,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FE":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FE","non_qualified":"1F469-1F3FF-200D-2764-200D-1F48B-200D-1F468-1F3FE","image":"1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3fe.png","sheet_x":21,"sheet_y":42,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FF":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F468-1F3FF","non_qualified":"1F469-1F3FF-200D-2764-200D-1F48B-200D-1F468-1F3FF","image":"1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f468-1f3ff.png","sheet_x":21,"sheet_y":43,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"KISS: WOMAN, WOMAN","unified":"1F469-200D-2764-FE0F-200D-1F48B-200D-1F469","non_qualified":"1F469-200D-2764-200D-1F48B-200D-1F469","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f469-200d-2764-fe0f-200d-1f48b-200d-1f469.png","sheet_x":21,"sheet_y":44,"short_name":"woman-kiss-woman","short_names":["woman-kiss-woman"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":514,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB-1F3FB":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FB","non_qualified":"1F469-1F3FB-200D-2764-200D-1F48B-200D-1F469-1F3FB","image":"1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fb.png","sheet_x":21,"sheet_y":45,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FC":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FC","non_qualified":"1F469-1F3FB-200D-2764-200D-1F48B-200D-1F469-1F3FC","image":"1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fc.png","sheet_x":21,"sheet_y":46,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FD":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FD","non_qualified":"1F469-1F3FB-200D-2764-200D-1F48B-200D-1F469-1F3FD","image":"1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fd.png","sheet_x":21,"sheet_y":47,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FE":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FE","non_qualified":"1F469-1F3FB-200D-2764-200D-1F48B-200D-1F469-1F3FE","image":"1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fe.png","sheet_x":21,"sheet_y":48,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FF":{"unified":"1F469-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FF","non_qualified":"1F469-1F3FB-200D-2764-200D-1F48B-200D-1F469-1F3FF","image":"1f469-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3ff.png","sheet_x":21,"sheet_y":49,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FB":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FB","non_qualified":"1F469-1F3FC-200D-2764-200D-1F48B-200D-1F469-1F3FB","image":"1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fb.png","sheet_x":21,"sheet_y":50,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FC":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FC","non_qualified":"1F469-1F3FC-200D-2764-200D-1F48B-200D-1F469-1F3FC","image":"1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fc.png","sheet_x":21,"sheet_y":51,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FD":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FD","non_qualified":"1F469-1F3FC-200D-2764-200D-1F48B-200D-1F469-1F3FD","image":"1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fd.png","sheet_x":21,"sheet_y":52,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FE":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FE","non_qualified":"1F469-1F3FC-200D-2764-200D-1F48B-200D-1F469-1F3FE","image":"1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fe.png","sheet_x":21,"sheet_y":53,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FF":{"unified":"1F469-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FF","non_qualified":"1F469-1F3FC-200D-2764-200D-1F48B-200D-1F469-1F3FF","image":"1f469-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3ff.png","sheet_x":21,"sheet_y":54,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FB":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FB","non_qualified":"1F469-1F3FD-200D-2764-200D-1F48B-200D-1F469-1F3FB","image":"1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fb.png","sheet_x":21,"sheet_y":55,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FC":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FC","non_qualified":"1F469-1F3FD-200D-2764-200D-1F48B-200D-1F469-1F3FC","image":"1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fc.png","sheet_x":21,"sheet_y":56,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FD":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FD","non_qualified":"1F469-1F3FD-200D-2764-200D-1F48B-200D-1F469-1F3FD","image":"1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fd.png","sheet_x":21,"sheet_y":57,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FE":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FE","non_qualified":"1F469-1F3FD-200D-2764-200D-1F48B-200D-1F469-1F3FE","image":"1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fe.png","sheet_x":21,"sheet_y":58,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FF":{"unified":"1F469-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FF","non_qualified":"1F469-1F3FD-200D-2764-200D-1F48B-200D-1F469-1F3FF","image":"1f469-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3ff.png","sheet_x":21,"sheet_y":59,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FB":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FB","non_qualified":"1F469-1F3FE-200D-2764-200D-1F48B-200D-1F469-1F3FB","image":"1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fb.png","sheet_x":21,"sheet_y":60,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FC":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FC","non_qualified":"1F469-1F3FE-200D-2764-200D-1F48B-200D-1F469-1F3FC","image":"1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fc.png","sheet_x":21,"sheet_y":61,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FD":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FD","non_qualified":"1F469-1F3FE-200D-2764-200D-1F48B-200D-1F469-1F3FD","image":"1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fd.png","sheet_x":22,"sheet_y":0,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FE":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FE","non_qualified":"1F469-1F3FE-200D-2764-200D-1F48B-200D-1F469-1F3FE","image":"1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fe.png","sheet_x":22,"sheet_y":1,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FF":{"unified":"1F469-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FF","non_qualified":"1F469-1F3FE-200D-2764-200D-1F48B-200D-1F469-1F3FF","image":"1f469-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3ff.png","sheet_x":22,"sheet_y":2,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FB":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FB","non_qualified":"1F469-1F3FF-200D-2764-200D-1F48B-200D-1F469-1F3FB","image":"1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fb.png","sheet_x":22,"sheet_y":3,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FC":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FC","non_qualified":"1F469-1F3FF-200D-2764-200D-1F48B-200D-1F469-1F3FC","image":"1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fc.png","sheet_x":22,"sheet_y":4,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FD":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FD","non_qualified":"1F469-1F3FF-200D-2764-200D-1F48B-200D-1F469-1F3FD","image":"1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fd.png","sheet_x":22,"sheet_y":5,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FE":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FE","non_qualified":"1F469-1F3FF-200D-2764-200D-1F48B-200D-1F469-1F3FE","image":"1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3fe.png","sheet_x":22,"sheet_y":6,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FF":{"unified":"1F469-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F469-1F3FF","non_qualified":"1F469-1F3FF-200D-2764-200D-1F48B-200D-1F469-1F3FF","image":"1f469-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f469-1f3ff.png","sheet_x":22,"sheet_y":7,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN","unified":"1F469","non_qualified":null,"docomo":"E6F0","au":"E4FA","softbank":"E005","google":"FE19E","image":"1f469.png","sheet_x":22,"sheet_y":8,"short_name":"woman","short_names":["woman"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":244,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F469-1F3FB","non_qualified":null,"image":"1f469-1f3fb.png","sheet_x":22,"sheet_y":9,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F469-1F3FC","non_qualified":null,"image":"1f469-1f3fc.png","sheet_x":22,"sheet_y":10,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F469-1F3FD","non_qualified":null,"image":"1f469-1f3fd.png","sheet_x":22,"sheet_y":11,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F469-1F3FE","non_qualified":null,"image":"1f469-1f3fe.png","sheet_x":22,"sheet_y":12,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F469-1F3FF","non_qualified":null,"image":"1f469-1f3ff.png","sheet_x":22,"sheet_y":13,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"FAMILY","unified":"1F46A","non_qualified":null,"docomo":null,"au":"E501","softbank":null,"google":"FE19F","image":"1f46a.png","sheet_x":22,"sheet_y":14,"short_name":"family","short_names":["family"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-symbol","sort_order":548,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F468-200D-1F469-200D-1F466"},{"name":"MAN AND WOMAN HOLDING HANDS","unified":"1F46B","non_qualified":null,"docomo":null,"au":null,"softbank":"E428","google":"FE1A0","image":"1f46b.png","sheet_x":22,"sheet_y":15,"short_name":"man_and_woman_holding_hands","short_names":["man_and_woman_holding_hands","woman_and_man_holding_hands","couple"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":509,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F46B-1F3FB","non_qualified":null,"image":"1f46b-1f3fb.png","sheet_x":22,"sheet_y":16,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F46B-1F3FC","non_qualified":null,"image":"1f46b-1f3fc.png","sheet_x":22,"sheet_y":17,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F46B-1F3FD","non_qualified":null,"image":"1f46b-1f3fd.png","sheet_x":22,"sheet_y":18,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F46B-1F3FE","non_qualified":null,"image":"1f46b-1f3fe.png","sheet_x":22,"sheet_y":19,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F46B-1F3FF","non_qualified":null,"image":"1f46b-1f3ff.png","sheet_x":22,"sheet_y":20,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FC":{"unified":"1F469-1F3FB-200D-1F91D-200D-1F468-1F3FC","non_qualified":null,"image":"1f469-1f3fb-200d-1f91d-200d-1f468-1f3fc.png","sheet_x":22,"sheet_y":21,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FD":{"unified":"1F469-1F3FB-200D-1F91D-200D-1F468-1F3FD","non_qualified":null,"image":"1f469-1f3fb-200d-1f91d-200d-1f468-1f3fd.png","sheet_x":22,"sheet_y":22,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FE":{"unified":"1F469-1F3FB-200D-1F91D-200D-1F468-1F3FE","non_qualified":null,"image":"1f469-1f3fb-200d-1f91d-200d-1f468-1f3fe.png","sheet_x":22,"sheet_y":23,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FF":{"unified":"1F469-1F3FB-200D-1F91D-200D-1F468-1F3FF","non_qualified":null,"image":"1f469-1f3fb-200d-1f91d-200d-1f468-1f3ff.png","sheet_x":22,"sheet_y":24,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FB":{"unified":"1F469-1F3FC-200D-1F91D-200D-1F468-1F3FB","non_qualified":null,"image":"1f469-1f3fc-200d-1f91d-200d-1f468-1f3fb.png","sheet_x":22,"sheet_y":25,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FD":{"unified":"1F469-1F3FC-200D-1F91D-200D-1F468-1F3FD","non_qualified":null,"image":"1f469-1f3fc-200d-1f91d-200d-1f468-1f3fd.png","sheet_x":22,"sheet_y":26,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FE":{"unified":"1F469-1F3FC-200D-1F91D-200D-1F468-1F3FE","non_qualified":null,"image":"1f469-1f3fc-200d-1f91d-200d-1f468-1f3fe.png","sheet_x":22,"sheet_y":27,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FF":{"unified":"1F469-1F3FC-200D-1F91D-200D-1F468-1F3FF","non_qualified":null,"image":"1f469-1f3fc-200d-1f91d-200d-1f468-1f3ff.png","sheet_x":22,"sheet_y":28,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FB":{"unified":"1F469-1F3FD-200D-1F91D-200D-1F468-1F3FB","non_qualified":null,"image":"1f469-1f3fd-200d-1f91d-200d-1f468-1f3fb.png","sheet_x":22,"sheet_y":29,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FC":{"unified":"1F469-1F3FD-200D-1F91D-200D-1F468-1F3FC","non_qualified":null,"image":"1f469-1f3fd-200d-1f91d-200d-1f468-1f3fc.png","sheet_x":22,"sheet_y":30,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FE":{"unified":"1F469-1F3FD-200D-1F91D-200D-1F468-1F3FE","non_qualified":null,"image":"1f469-1f3fd-200d-1f91d-200d-1f468-1f3fe.png","sheet_x":22,"sheet_y":31,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FF":{"unified":"1F469-1F3FD-200D-1F91D-200D-1F468-1F3FF","non_qualified":null,"image":"1f469-1f3fd-200d-1f91d-200d-1f468-1f3ff.png","sheet_x":22,"sheet_y":32,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FB":{"unified":"1F469-1F3FE-200D-1F91D-200D-1F468-1F3FB","non_qualified":null,"image":"1f469-1f3fe-200d-1f91d-200d-1f468-1f3fb.png","sheet_x":22,"sheet_y":33,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FC":{"unified":"1F469-1F3FE-200D-1F91D-200D-1F468-1F3FC","non_qualified":null,"image":"1f469-1f3fe-200d-1f91d-200d-1f468-1f3fc.png","sheet_x":22,"sheet_y":34,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FD":{"unified":"1F469-1F3FE-200D-1F91D-200D-1F468-1F3FD","non_qualified":null,"image":"1f469-1f3fe-200d-1f91d-200d-1f468-1f3fd.png","sheet_x":22,"sheet_y":35,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FF":{"unified":"1F469-1F3FE-200D-1F91D-200D-1F468-1F3FF","non_qualified":null,"image":"1f469-1f3fe-200d-1f91d-200d-1f468-1f3ff.png","sheet_x":22,"sheet_y":36,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FB":{"unified":"1F469-1F3FF-200D-1F91D-200D-1F468-1F3FB","non_qualified":null,"image":"1f469-1f3ff-200d-1f91d-200d-1f468-1f3fb.png","sheet_x":22,"sheet_y":37,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FC":{"unified":"1F469-1F3FF-200D-1F91D-200D-1F468-1F3FC","non_qualified":null,"image":"1f469-1f3ff-200d-1f91d-200d-1f468-1f3fc.png","sheet_x":22,"sheet_y":38,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FD":{"unified":"1F469-1F3FF-200D-1F91D-200D-1F468-1F3FD","non_qualified":null,"image":"1f469-1f3ff-200d-1f91d-200d-1f468-1f3fd.png","sheet_x":22,"sheet_y":39,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FE":{"unified":"1F469-1F3FF-200D-1F91D-200D-1F468-1F3FE","non_qualified":null,"image":"1f469-1f3ff-200d-1f91d-200d-1f468-1f3fe.png","sheet_x":22,"sheet_y":40,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"TWO MEN HOLDING HANDS","unified":"1F46C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f46c.png","sheet_x":22,"sheet_y":41,"short_name":"two_men_holding_hands","short_names":["two_men_holding_hands","men_holding_hands"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":510,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F46C-1F3FB","non_qualified":null,"image":"1f46c-1f3fb.png","sheet_x":22,"sheet_y":42,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F46C-1F3FC","non_qualified":null,"image":"1f46c-1f3fc.png","sheet_x":22,"sheet_y":43,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F46C-1F3FD","non_qualified":null,"image":"1f46c-1f3fd.png","sheet_x":22,"sheet_y":44,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F46C-1F3FE","non_qualified":null,"image":"1f46c-1f3fe.png","sheet_x":22,"sheet_y":45,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F46C-1F3FF","non_qualified":null,"image":"1f46c-1f3ff.png","sheet_x":22,"sheet_y":46,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FC":{"unified":"1F468-1F3FB-200D-1F91D-200D-1F468-1F3FC","non_qualified":null,"image":"1f468-1f3fb-200d-1f91d-200d-1f468-1f3fc.png","sheet_x":22,"sheet_y":47,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FD":{"unified":"1F468-1F3FB-200D-1F91D-200D-1F468-1F3FD","non_qualified":null,"image":"1f468-1f3fb-200d-1f91d-200d-1f468-1f3fd.png","sheet_x":22,"sheet_y":48,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FE":{"unified":"1F468-1F3FB-200D-1F91D-200D-1F468-1F3FE","non_qualified":null,"image":"1f468-1f3fb-200d-1f91d-200d-1f468-1f3fe.png","sheet_x":22,"sheet_y":49,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FF":{"unified":"1F468-1F3FB-200D-1F91D-200D-1F468-1F3FF","non_qualified":null,"image":"1f468-1f3fb-200d-1f91d-200d-1f468-1f3ff.png","sheet_x":22,"sheet_y":50,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FB":{"unified":"1F468-1F3FC-200D-1F91D-200D-1F468-1F3FB","non_qualified":null,"image":"1f468-1f3fc-200d-1f91d-200d-1f468-1f3fb.png","sheet_x":22,"sheet_y":51,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FD":{"unified":"1F468-1F3FC-200D-1F91D-200D-1F468-1F3FD","non_qualified":null,"image":"1f468-1f3fc-200d-1f91d-200d-1f468-1f3fd.png","sheet_x":22,"sheet_y":52,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FE":{"unified":"1F468-1F3FC-200D-1F91D-200D-1F468-1F3FE","non_qualified":null,"image":"1f468-1f3fc-200d-1f91d-200d-1f468-1f3fe.png","sheet_x":22,"sheet_y":53,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FF":{"unified":"1F468-1F3FC-200D-1F91D-200D-1F468-1F3FF","non_qualified":null,"image":"1f468-1f3fc-200d-1f91d-200d-1f468-1f3ff.png","sheet_x":22,"sheet_y":54,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FB":{"unified":"1F468-1F3FD-200D-1F91D-200D-1F468-1F3FB","non_qualified":null,"image":"1f468-1f3fd-200d-1f91d-200d-1f468-1f3fb.png","sheet_x":22,"sheet_y":55,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FC":{"unified":"1F468-1F3FD-200D-1F91D-200D-1F468-1F3FC","non_qualified":null,"image":"1f468-1f3fd-200d-1f91d-200d-1f468-1f3fc.png","sheet_x":22,"sheet_y":56,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FE":{"unified":"1F468-1F3FD-200D-1F91D-200D-1F468-1F3FE","non_qualified":null,"image":"1f468-1f3fd-200d-1f91d-200d-1f468-1f3fe.png","sheet_x":22,"sheet_y":57,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FF":{"unified":"1F468-1F3FD-200D-1F91D-200D-1F468-1F3FF","non_qualified":null,"image":"1f468-1f3fd-200d-1f91d-200d-1f468-1f3ff.png","sheet_x":22,"sheet_y":58,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FB":{"unified":"1F468-1F3FE-200D-1F91D-200D-1F468-1F3FB","non_qualified":null,"image":"1f468-1f3fe-200d-1f91d-200d-1f468-1f3fb.png","sheet_x":22,"sheet_y":59,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FC":{"unified":"1F468-1F3FE-200D-1F91D-200D-1F468-1F3FC","non_qualified":null,"image":"1f468-1f3fe-200d-1f91d-200d-1f468-1f3fc.png","sheet_x":22,"sheet_y":60,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FD":{"unified":"1F468-1F3FE-200D-1F91D-200D-1F468-1F3FD","non_qualified":null,"image":"1f468-1f3fe-200d-1f91d-200d-1f468-1f3fd.png","sheet_x":22,"sheet_y":61,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FF":{"unified":"1F468-1F3FE-200D-1F91D-200D-1F468-1F3FF","non_qualified":null,"image":"1f468-1f3fe-200d-1f91d-200d-1f468-1f3ff.png","sheet_x":23,"sheet_y":0,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FB":{"unified":"1F468-1F3FF-200D-1F91D-200D-1F468-1F3FB","non_qualified":null,"image":"1f468-1f3ff-200d-1f91d-200d-1f468-1f3fb.png","sheet_x":23,"sheet_y":1,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FC":{"unified":"1F468-1F3FF-200D-1F91D-200D-1F468-1F3FC","non_qualified":null,"image":"1f468-1f3ff-200d-1f91d-200d-1f468-1f3fc.png","sheet_x":23,"sheet_y":2,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FD":{"unified":"1F468-1F3FF-200D-1F91D-200D-1F468-1F3FD","non_qualified":null,"image":"1f468-1f3ff-200d-1f91d-200d-1f468-1f3fd.png","sheet_x":23,"sheet_y":3,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FE":{"unified":"1F468-1F3FF-200D-1F91D-200D-1F468-1F3FE","non_qualified":null,"image":"1f468-1f3ff-200d-1f91d-200d-1f468-1f3fe.png","sheet_x":23,"sheet_y":4,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"TWO WOMEN HOLDING HANDS","unified":"1F46D","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f46d.png","sheet_x":23,"sheet_y":5,"short_name":"two_women_holding_hands","short_names":["two_women_holding_hands","women_holding_hands"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":508,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F46D-1F3FB","non_qualified":null,"image":"1f46d-1f3fb.png","sheet_x":23,"sheet_y":6,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F46D-1F3FC","non_qualified":null,"image":"1f46d-1f3fc.png","sheet_x":23,"sheet_y":7,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F46D-1F3FD","non_qualified":null,"image":"1f46d-1f3fd.png","sheet_x":23,"sheet_y":8,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F46D-1F3FE","non_qualified":null,"image":"1f46d-1f3fe.png","sheet_x":23,"sheet_y":9,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F46D-1F3FF","non_qualified":null,"image":"1f46d-1f3ff.png","sheet_x":23,"sheet_y":10,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FC":{"unified":"1F469-1F3FB-200D-1F91D-200D-1F469-1F3FC","non_qualified":null,"image":"1f469-1f3fb-200d-1f91d-200d-1f469-1f3fc.png","sheet_x":23,"sheet_y":11,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FD":{"unified":"1F469-1F3FB-200D-1F91D-200D-1F469-1F3FD","non_qualified":null,"image":"1f469-1f3fb-200d-1f91d-200d-1f469-1f3fd.png","sheet_x":23,"sheet_y":12,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FE":{"unified":"1F469-1F3FB-200D-1F91D-200D-1F469-1F3FE","non_qualified":null,"image":"1f469-1f3fb-200d-1f91d-200d-1f469-1f3fe.png","sheet_x":23,"sheet_y":13,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FF":{"unified":"1F469-1F3FB-200D-1F91D-200D-1F469-1F3FF","non_qualified":null,"image":"1f469-1f3fb-200d-1f91d-200d-1f469-1f3ff.png","sheet_x":23,"sheet_y":14,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FB":{"unified":"1F469-1F3FC-200D-1F91D-200D-1F469-1F3FB","non_qualified":null,"image":"1f469-1f3fc-200d-1f91d-200d-1f469-1f3fb.png","sheet_x":23,"sheet_y":15,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FD":{"unified":"1F469-1F3FC-200D-1F91D-200D-1F469-1F3FD","non_qualified":null,"image":"1f469-1f3fc-200d-1f91d-200d-1f469-1f3fd.png","sheet_x":23,"sheet_y":16,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FE":{"unified":"1F469-1F3FC-200D-1F91D-200D-1F469-1F3FE","non_qualified":null,"image":"1f469-1f3fc-200d-1f91d-200d-1f469-1f3fe.png","sheet_x":23,"sheet_y":17,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FF":{"unified":"1F469-1F3FC-200D-1F91D-200D-1F469-1F3FF","non_qualified":null,"image":"1f469-1f3fc-200d-1f91d-200d-1f469-1f3ff.png","sheet_x":23,"sheet_y":18,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FB":{"unified":"1F469-1F3FD-200D-1F91D-200D-1F469-1F3FB","non_qualified":null,"image":"1f469-1f3fd-200d-1f91d-200d-1f469-1f3fb.png","sheet_x":23,"sheet_y":19,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FC":{"unified":"1F469-1F3FD-200D-1F91D-200D-1F469-1F3FC","non_qualified":null,"image":"1f469-1f3fd-200d-1f91d-200d-1f469-1f3fc.png","sheet_x":23,"sheet_y":20,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FE":{"unified":"1F469-1F3FD-200D-1F91D-200D-1F469-1F3FE","non_qualified":null,"image":"1f469-1f3fd-200d-1f91d-200d-1f469-1f3fe.png","sheet_x":23,"sheet_y":21,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FF":{"unified":"1F469-1F3FD-200D-1F91D-200D-1F469-1F3FF","non_qualified":null,"image":"1f469-1f3fd-200d-1f91d-200d-1f469-1f3ff.png","sheet_x":23,"sheet_y":22,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FB":{"unified":"1F469-1F3FE-200D-1F91D-200D-1F469-1F3FB","non_qualified":null,"image":"1f469-1f3fe-200d-1f91d-200d-1f469-1f3fb.png","sheet_x":23,"sheet_y":23,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FC":{"unified":"1F469-1F3FE-200D-1F91D-200D-1F469-1F3FC","non_qualified":null,"image":"1f469-1f3fe-200d-1f91d-200d-1f469-1f3fc.png","sheet_x":23,"sheet_y":24,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FD":{"unified":"1F469-1F3FE-200D-1F91D-200D-1F469-1F3FD","non_qualified":null,"image":"1f469-1f3fe-200d-1f91d-200d-1f469-1f3fd.png","sheet_x":23,"sheet_y":25,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FF":{"unified":"1F469-1F3FE-200D-1F91D-200D-1F469-1F3FF","non_qualified":null,"image":"1f469-1f3fe-200d-1f91d-200d-1f469-1f3ff.png","sheet_x":23,"sheet_y":26,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FB":{"unified":"1F469-1F3FF-200D-1F91D-200D-1F469-1F3FB","non_qualified":null,"image":"1f469-1f3ff-200d-1f91d-200d-1f469-1f3fb.png","sheet_x":23,"sheet_y":27,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FC":{"unified":"1F469-1F3FF-200D-1F91D-200D-1F469-1F3FC","non_qualified":null,"image":"1f469-1f3ff-200d-1f91d-200d-1f469-1f3fc.png","sheet_x":23,"sheet_y":28,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FD":{"unified":"1F469-1F3FF-200D-1F91D-200D-1F469-1F3FD","non_qualified":null,"image":"1f469-1f3ff-200d-1f91d-200d-1f469-1f3fd.png","sheet_x":23,"sheet_y":29,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FE":{"unified":"1F469-1F3FF-200D-1F91D-200D-1F469-1F3FE","non_qualified":null,"image":"1f469-1f3ff-200d-1f91d-200d-1f469-1f3fe.png","sheet_x":23,"sheet_y":30,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN POLICE OFFICER","unified":"1F46E-200D-2640-FE0F","non_qualified":"1F46E-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f46e-200d-2640-fe0f.png","sheet_x":23,"sheet_y":31,"short_name":"female-police-officer","short_names":["female-police-officer"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":338,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F46E-1F3FB-200D-2640-FE0F","non_qualified":"1F46E-1F3FB-200D-2640","image":"1f46e-1f3fb-200d-2640-fe0f.png","sheet_x":23,"sheet_y":32,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F46E-1F3FC-200D-2640-FE0F","non_qualified":"1F46E-1F3FC-200D-2640","image":"1f46e-1f3fc-200d-2640-fe0f.png","sheet_x":23,"sheet_y":33,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F46E-1F3FD-200D-2640-FE0F","non_qualified":"1F46E-1F3FD-200D-2640","image":"1f46e-1f3fd-200d-2640-fe0f.png","sheet_x":23,"sheet_y":34,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F46E-1F3FE-200D-2640-FE0F","non_qualified":"1F46E-1F3FE-200D-2640","image":"1f46e-1f3fe-200d-2640-fe0f.png","sheet_x":23,"sheet_y":35,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F46E-1F3FF-200D-2640-FE0F","non_qualified":"1F46E-1F3FF-200D-2640","image":"1f46e-1f3ff-200d-2640-fe0f.png","sheet_x":23,"sheet_y":36,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN POLICE OFFICER","unified":"1F46E-200D-2642-FE0F","non_qualified":"1F46E-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f46e-200d-2642-fe0f.png","sheet_x":23,"sheet_y":37,"short_name":"male-police-officer","short_names":["male-police-officer"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":337,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F46E-1F3FB-200D-2642-FE0F","non_qualified":"1F46E-1F3FB-200D-2642","image":"1f46e-1f3fb-200d-2642-fe0f.png","sheet_x":23,"sheet_y":38,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F46E-1F3FC-200D-2642-FE0F","non_qualified":"1F46E-1F3FC-200D-2642","image":"1f46e-1f3fc-200d-2642-fe0f.png","sheet_x":23,"sheet_y":39,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F46E-1F3FD-200D-2642-FE0F","non_qualified":"1F46E-1F3FD-200D-2642","image":"1f46e-1f3fd-200d-2642-fe0f.png","sheet_x":23,"sheet_y":40,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F46E-1F3FE-200D-2642-FE0F","non_qualified":"1F46E-1F3FE-200D-2642","image":"1f46e-1f3fe-200d-2642-fe0f.png","sheet_x":23,"sheet_y":41,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F46E-1F3FF-200D-2642-FE0F","non_qualified":"1F46E-1F3FF-200D-2642","image":"1f46e-1f3ff-200d-2642-fe0f.png","sheet_x":23,"sheet_y":42,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F46E"},{"name":"POLICE OFFICER","unified":"1F46E","non_qualified":null,"docomo":null,"au":"E5DD","softbank":"E152","google":"FE1A1","image":"1f46e.png","sheet_x":23,"sheet_y":43,"short_name":"cop","short_names":["cop"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":336,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F46E-1F3FB","non_qualified":null,"image":"1f46e-1f3fb.png","sheet_x":23,"sheet_y":44,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F46E-1F3FC","non_qualified":null,"image":"1f46e-1f3fc.png","sheet_x":23,"sheet_y":45,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F46E-1F3FD","non_qualified":null,"image":"1f46e-1f3fd.png","sheet_x":23,"sheet_y":46,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F46E-1F3FE","non_qualified":null,"image":"1f46e-1f3fe.png","sheet_x":23,"sheet_y":47,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F46E-1F3FF","non_qualified":null,"image":"1f46e-1f3ff.png","sheet_x":23,"sheet_y":48,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F46E-200D-2642-FE0F"},{"name":"WOMEN WITH BUNNY EARS","unified":"1F46F-200D-2640-FE0F","non_qualified":"1F46F-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f46f-200d-2640-fe0f.png","sheet_x":23,"sheet_y":49,"short_name":"women-with-bunny-ears-partying","short_names":["women-with-bunny-ears-partying","woman-with-bunny-ears-partying"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":452,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F46F"},{"name":"MEN WITH BUNNY EARS","unified":"1F46F-200D-2642-FE0F","non_qualified":"1F46F-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f46f-200d-2642-fe0f.png","sheet_x":23,"sheet_y":50,"short_name":"men-with-bunny-ears-partying","short_names":["men-with-bunny-ears-partying","man-with-bunny-ears-partying"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":451,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WOMAN WITH BUNNY EARS","unified":"1F46F","non_qualified":null,"docomo":null,"au":"EADB","softbank":"E429","google":"FE1A2","image":"1f46f.png","sheet_x":23,"sheet_y":51,"short_name":"dancers","short_names":["dancers"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":450,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F46F-200D-2640-FE0F"},{"name":"WOMAN WITH VEIL","unified":"1F470-200D-2640-FE0F","non_qualified":"1F470-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f470-200d-2640-fe0f.png","sheet_x":23,"sheet_y":52,"short_name":"woman_with_veil","short_names":["woman_with_veil"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":362,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F470-1F3FB-200D-2640-FE0F","non_qualified":"1F470-1F3FB-200D-2640","image":"1f470-1f3fb-200d-2640-fe0f.png","sheet_x":23,"sheet_y":53,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F470-1F3FC-200D-2640-FE0F","non_qualified":"1F470-1F3FC-200D-2640","image":"1f470-1f3fc-200d-2640-fe0f.png","sheet_x":23,"sheet_y":54,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F470-1F3FD-200D-2640-FE0F","non_qualified":"1F470-1F3FD-200D-2640","image":"1f470-1f3fd-200d-2640-fe0f.png","sheet_x":23,"sheet_y":55,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F470-1F3FE-200D-2640-FE0F","non_qualified":"1F470-1F3FE-200D-2640","image":"1f470-1f3fe-200d-2640-fe0f.png","sheet_x":23,"sheet_y":56,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F470-1F3FF-200D-2640-FE0F","non_qualified":"1F470-1F3FF-200D-2640","image":"1f470-1f3ff-200d-2640-fe0f.png","sheet_x":23,"sheet_y":57,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN WITH VEIL","unified":"1F470-200D-2642-FE0F","non_qualified":"1F470-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f470-200d-2642-fe0f.png","sheet_x":23,"sheet_y":58,"short_name":"man_with_veil","short_names":["man_with_veil"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":361,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F470-1F3FB-200D-2642-FE0F","non_qualified":"1F470-1F3FB-200D-2642","image":"1f470-1f3fb-200d-2642-fe0f.png","sheet_x":23,"sheet_y":59,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F470-1F3FC-200D-2642-FE0F","non_qualified":"1F470-1F3FC-200D-2642","image":"1f470-1f3fc-200d-2642-fe0f.png","sheet_x":23,"sheet_y":60,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F470-1F3FD-200D-2642-FE0F","non_qualified":"1F470-1F3FD-200D-2642","image":"1f470-1f3fd-200d-2642-fe0f.png","sheet_x":23,"sheet_y":61,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F470-1F3FE-200D-2642-FE0F","non_qualified":"1F470-1F3FE-200D-2642","image":"1f470-1f3fe-200d-2642-fe0f.png","sheet_x":24,"sheet_y":0,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F470-1F3FF-200D-2642-FE0F","non_qualified":"1F470-1F3FF-200D-2642","image":"1f470-1f3ff-200d-2642-fe0f.png","sheet_x":24,"sheet_y":1,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"BRIDE WITH VEIL","unified":"1F470","non_qualified":null,"docomo":null,"au":"EAE9","softbank":null,"google":"FE1A3","image":"1f470.png","sheet_x":24,"sheet_y":2,"short_name":"bride_with_veil","short_names":["bride_with_veil"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":360,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F470-1F3FB","non_qualified":null,"image":"1f470-1f3fb.png","sheet_x":24,"sheet_y":3,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F470-1F3FC","non_qualified":null,"image":"1f470-1f3fc.png","sheet_x":24,"sheet_y":4,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F470-1F3FD","non_qualified":null,"image":"1f470-1f3fd.png","sheet_x":24,"sheet_y":5,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F470-1F3FE","non_qualified":null,"image":"1f470-1f3fe.png","sheet_x":24,"sheet_y":6,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F470-1F3FF","non_qualified":null,"image":"1f470-1f3ff.png","sheet_x":24,"sheet_y":7,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN: BLOND HAIR","unified":"1F471-200D-2640-FE0F","non_qualified":"1F471-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f471-200d-2640-fe0f.png","sheet_x":24,"sheet_y":8,"short_name":"blond-haired-woman","short_names":["blond-haired-woman"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":253,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F471-1F3FB-200D-2640-FE0F","non_qualified":"1F471-1F3FB-200D-2640","image":"1f471-1f3fb-200d-2640-fe0f.png","sheet_x":24,"sheet_y":9,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F471-1F3FC-200D-2640-FE0F","non_qualified":"1F471-1F3FC-200D-2640","image":"1f471-1f3fc-200d-2640-fe0f.png","sheet_x":24,"sheet_y":10,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F471-1F3FD-200D-2640-FE0F","non_qualified":"1F471-1F3FD-200D-2640","image":"1f471-1f3fd-200d-2640-fe0f.png","sheet_x":24,"sheet_y":11,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F471-1F3FE-200D-2640-FE0F","non_qualified":"1F471-1F3FE-200D-2640","image":"1f471-1f3fe-200d-2640-fe0f.png","sheet_x":24,"sheet_y":12,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F471-1F3FF-200D-2640-FE0F","non_qualified":"1F471-1F3FF-200D-2640","image":"1f471-1f3ff-200d-2640-fe0f.png","sheet_x":24,"sheet_y":13,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN: BLOND HAIR","unified":"1F471-200D-2642-FE0F","non_qualified":"1F471-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f471-200d-2642-fe0f.png","sheet_x":24,"sheet_y":14,"short_name":"blond-haired-man","short_names":["blond-haired-man"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":254,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F471-1F3FB-200D-2642-FE0F","non_qualified":"1F471-1F3FB-200D-2642","image":"1f471-1f3fb-200d-2642-fe0f.png","sheet_x":24,"sheet_y":15,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F471-1F3FC-200D-2642-FE0F","non_qualified":"1F471-1F3FC-200D-2642","image":"1f471-1f3fc-200d-2642-fe0f.png","sheet_x":24,"sheet_y":16,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F471-1F3FD-200D-2642-FE0F","non_qualified":"1F471-1F3FD-200D-2642","image":"1f471-1f3fd-200d-2642-fe0f.png","sheet_x":24,"sheet_y":17,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F471-1F3FE-200D-2642-FE0F","non_qualified":"1F471-1F3FE-200D-2642","image":"1f471-1f3fe-200d-2642-fe0f.png","sheet_x":24,"sheet_y":18,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F471-1F3FF-200D-2642-FE0F","non_qualified":"1F471-1F3FF-200D-2642","image":"1f471-1f3ff-200d-2642-fe0f.png","sheet_x":24,"sheet_y":19,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F471"},{"name":"PERSON WITH BLOND HAIR","unified":"1F471","non_qualified":null,"docomo":null,"au":"EB13","softbank":"E515","google":"FE1A4","image":"1f471.png","sheet_x":24,"sheet_y":20,"short_name":"person_with_blond_hair","short_names":["person_with_blond_hair"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":235,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F471-1F3FB","non_qualified":null,"image":"1f471-1f3fb.png","sheet_x":24,"sheet_y":21,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F471-1F3FC","non_qualified":null,"image":"1f471-1f3fc.png","sheet_x":24,"sheet_y":22,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F471-1F3FD","non_qualified":null,"image":"1f471-1f3fd.png","sheet_x":24,"sheet_y":23,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F471-1F3FE","non_qualified":null,"image":"1f471-1f3fe.png","sheet_x":24,"sheet_y":24,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F471-1F3FF","non_qualified":null,"image":"1f471-1f3ff.png","sheet_x":24,"sheet_y":25,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F471-200D-2642-FE0F"},{"name":"MAN WITH GUA PI MAO","unified":"1F472","non_qualified":null,"docomo":null,"au":"EB14","softbank":"E516","google":"FE1A5","image":"1f472.png","sheet_x":24,"sheet_y":26,"short_name":"man_with_gua_pi_mao","short_names":["man_with_gua_pi_mao"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":355,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F472-1F3FB","non_qualified":null,"image":"1f472-1f3fb.png","sheet_x":24,"sheet_y":27,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F472-1F3FC","non_qualified":null,"image":"1f472-1f3fc.png","sheet_x":24,"sheet_y":28,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F472-1F3FD","non_qualified":null,"image":"1f472-1f3fd.png","sheet_x":24,"sheet_y":29,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F472-1F3FE","non_qualified":null,"image":"1f472-1f3fe.png","sheet_x":24,"sheet_y":30,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F472-1F3FF","non_qualified":null,"image":"1f472-1f3ff.png","sheet_x":24,"sheet_y":31,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN WEARING TURBAN","unified":"1F473-200D-2640-FE0F","non_qualified":"1F473-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f473-200d-2640-fe0f.png","sheet_x":24,"sheet_y":32,"short_name":"woman-wearing-turban","short_names":["woman-wearing-turban"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":354,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F473-1F3FB-200D-2640-FE0F","non_qualified":"1F473-1F3FB-200D-2640","image":"1f473-1f3fb-200d-2640-fe0f.png","sheet_x":24,"sheet_y":33,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F473-1F3FC-200D-2640-FE0F","non_qualified":"1F473-1F3FC-200D-2640","image":"1f473-1f3fc-200d-2640-fe0f.png","sheet_x":24,"sheet_y":34,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F473-1F3FD-200D-2640-FE0F","non_qualified":"1F473-1F3FD-200D-2640","image":"1f473-1f3fd-200d-2640-fe0f.png","sheet_x":24,"sheet_y":35,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F473-1F3FE-200D-2640-FE0F","non_qualified":"1F473-1F3FE-200D-2640","image":"1f473-1f3fe-200d-2640-fe0f.png","sheet_x":24,"sheet_y":36,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F473-1F3FF-200D-2640-FE0F","non_qualified":"1F473-1F3FF-200D-2640","image":"1f473-1f3ff-200d-2640-fe0f.png","sheet_x":24,"sheet_y":37,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN WEARING TURBAN","unified":"1F473-200D-2642-FE0F","non_qualified":"1F473-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f473-200d-2642-fe0f.png","sheet_x":24,"sheet_y":38,"short_name":"man-wearing-turban","short_names":["man-wearing-turban"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":353,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F473-1F3FB-200D-2642-FE0F","non_qualified":"1F473-1F3FB-200D-2642","image":"1f473-1f3fb-200d-2642-fe0f.png","sheet_x":24,"sheet_y":39,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F473-1F3FC-200D-2642-FE0F","non_qualified":"1F473-1F3FC-200D-2642","image":"1f473-1f3fc-200d-2642-fe0f.png","sheet_x":24,"sheet_y":40,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F473-1F3FD-200D-2642-FE0F","non_qualified":"1F473-1F3FD-200D-2642","image":"1f473-1f3fd-200d-2642-fe0f.png","sheet_x":24,"sheet_y":41,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F473-1F3FE-200D-2642-FE0F","non_qualified":"1F473-1F3FE-200D-2642","image":"1f473-1f3fe-200d-2642-fe0f.png","sheet_x":24,"sheet_y":42,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F473-1F3FF-200D-2642-FE0F","non_qualified":"1F473-1F3FF-200D-2642","image":"1f473-1f3ff-200d-2642-fe0f.png","sheet_x":24,"sheet_y":43,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F473"},{"name":"MAN WITH TURBAN","unified":"1F473","non_qualified":null,"docomo":null,"au":"EB15","softbank":"E517","google":"FE1A6","image":"1f473.png","sheet_x":24,"sheet_y":44,"short_name":"man_with_turban","short_names":["man_with_turban"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":352,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F473-1F3FB","non_qualified":null,"image":"1f473-1f3fb.png","sheet_x":24,"sheet_y":45,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F473-1F3FC","non_qualified":null,"image":"1f473-1f3fc.png","sheet_x":24,"sheet_y":46,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F473-1F3FD","non_qualified":null,"image":"1f473-1f3fd.png","sheet_x":24,"sheet_y":47,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F473-1F3FE","non_qualified":null,"image":"1f473-1f3fe.png","sheet_x":24,"sheet_y":48,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F473-1F3FF","non_qualified":null,"image":"1f473-1f3ff.png","sheet_x":24,"sheet_y":49,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F473-200D-2642-FE0F"},{"name":"OLDER MAN","unified":"1F474","non_qualified":null,"docomo":null,"au":"EB16","softbank":"E518","google":"FE1A7","image":"1f474.png","sheet_x":24,"sheet_y":50,"short_name":"older_man","short_names":["older_man"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":256,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F474-1F3FB","non_qualified":null,"image":"1f474-1f3fb.png","sheet_x":24,"sheet_y":51,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F474-1F3FC","non_qualified":null,"image":"1f474-1f3fc.png","sheet_x":24,"sheet_y":52,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F474-1F3FD","non_qualified":null,"image":"1f474-1f3fd.png","sheet_x":24,"sheet_y":53,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F474-1F3FE","non_qualified":null,"image":"1f474-1f3fe.png","sheet_x":24,"sheet_y":54,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F474-1F3FF","non_qualified":null,"image":"1f474-1f3ff.png","sheet_x":24,"sheet_y":55,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"OLDER WOMAN","unified":"1F475","non_qualified":null,"docomo":null,"au":"EB17","softbank":"E519","google":"FE1A8","image":"1f475.png","sheet_x":24,"sheet_y":56,"short_name":"older_woman","short_names":["older_woman"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":257,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F475-1F3FB","non_qualified":null,"image":"1f475-1f3fb.png","sheet_x":24,"sheet_y":57,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F475-1F3FC","non_qualified":null,"image":"1f475-1f3fc.png","sheet_x":24,"sheet_y":58,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F475-1F3FD","non_qualified":null,"image":"1f475-1f3fd.png","sheet_x":24,"sheet_y":59,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F475-1F3FE","non_qualified":null,"image":"1f475-1f3fe.png","sheet_x":24,"sheet_y":60,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F475-1F3FF","non_qualified":null,"image":"1f475-1f3ff.png","sheet_x":24,"sheet_y":61,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"BABY","unified":"1F476","non_qualified":null,"docomo":null,"au":"EB18","softbank":"E51A","google":"FE1A9","image":"1f476.png","sheet_x":25,"sheet_y":0,"short_name":"baby","short_names":["baby"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":230,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F476-1F3FB","non_qualified":null,"image":"1f476-1f3fb.png","sheet_x":25,"sheet_y":1,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F476-1F3FC","non_qualified":null,"image":"1f476-1f3fc.png","sheet_x":25,"sheet_y":2,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F476-1F3FD","non_qualified":null,"image":"1f476-1f3fd.png","sheet_x":25,"sheet_y":3,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F476-1F3FE","non_qualified":null,"image":"1f476-1f3fe.png","sheet_x":25,"sheet_y":4,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F476-1F3FF","non_qualified":null,"image":"1f476-1f3ff.png","sheet_x":25,"sheet_y":5,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN CONSTRUCTION WORKER","unified":"1F477-200D-2640-FE0F","non_qualified":"1F477-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f477-200d-2640-fe0f.png","sheet_x":25,"sheet_y":6,"short_name":"female-construction-worker","short_names":["female-construction-worker"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":348,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F477-1F3FB-200D-2640-FE0F","non_qualified":"1F477-1F3FB-200D-2640","image":"1f477-1f3fb-200d-2640-fe0f.png","sheet_x":25,"sheet_y":7,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F477-1F3FC-200D-2640-FE0F","non_qualified":"1F477-1F3FC-200D-2640","image":"1f477-1f3fc-200d-2640-fe0f.png","sheet_x":25,"sheet_y":8,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F477-1F3FD-200D-2640-FE0F","non_qualified":"1F477-1F3FD-200D-2640","image":"1f477-1f3fd-200d-2640-fe0f.png","sheet_x":25,"sheet_y":9,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F477-1F3FE-200D-2640-FE0F","non_qualified":"1F477-1F3FE-200D-2640","image":"1f477-1f3fe-200d-2640-fe0f.png","sheet_x":25,"sheet_y":10,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F477-1F3FF-200D-2640-FE0F","non_qualified":"1F477-1F3FF-200D-2640","image":"1f477-1f3ff-200d-2640-fe0f.png","sheet_x":25,"sheet_y":11,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN CONSTRUCTION WORKER","unified":"1F477-200D-2642-FE0F","non_qualified":"1F477-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f477-200d-2642-fe0f.png","sheet_x":25,"sheet_y":12,"short_name":"male-construction-worker","short_names":["male-construction-worker"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":347,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F477-1F3FB-200D-2642-FE0F","non_qualified":"1F477-1F3FB-200D-2642","image":"1f477-1f3fb-200d-2642-fe0f.png","sheet_x":25,"sheet_y":13,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F477-1F3FC-200D-2642-FE0F","non_qualified":"1F477-1F3FC-200D-2642","image":"1f477-1f3fc-200d-2642-fe0f.png","sheet_x":25,"sheet_y":14,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F477-1F3FD-200D-2642-FE0F","non_qualified":"1F477-1F3FD-200D-2642","image":"1f477-1f3fd-200d-2642-fe0f.png","sheet_x":25,"sheet_y":15,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F477-1F3FE-200D-2642-FE0F","non_qualified":"1F477-1F3FE-200D-2642","image":"1f477-1f3fe-200d-2642-fe0f.png","sheet_x":25,"sheet_y":16,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F477-1F3FF-200D-2642-FE0F","non_qualified":"1F477-1F3FF-200D-2642","image":"1f477-1f3ff-200d-2642-fe0f.png","sheet_x":25,"sheet_y":17,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F477"},{"name":"CONSTRUCTION WORKER","unified":"1F477","non_qualified":null,"docomo":null,"au":"EB19","softbank":"E51B","google":"FE1AA","image":"1f477.png","sheet_x":25,"sheet_y":18,"short_name":"construction_worker","short_names":["construction_worker"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":346,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F477-1F3FB","non_qualified":null,"image":"1f477-1f3fb.png","sheet_x":25,"sheet_y":19,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F477-1F3FC","non_qualified":null,"image":"1f477-1f3fc.png","sheet_x":25,"sheet_y":20,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F477-1F3FD","non_qualified":null,"image":"1f477-1f3fd.png","sheet_x":25,"sheet_y":21,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F477-1F3FE","non_qualified":null,"image":"1f477-1f3fe.png","sheet_x":25,"sheet_y":22,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F477-1F3FF","non_qualified":null,"image":"1f477-1f3ff.png","sheet_x":25,"sheet_y":23,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F477-200D-2642-FE0F"},{"name":"PRINCESS","unified":"1F478","non_qualified":null,"docomo":null,"au":"EB1A","softbank":"E51C","google":"FE1AB","image":"1f478.png","sheet_x":25,"sheet_y":24,"short_name":"princess","short_names":["princess"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":351,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F478-1F3FB","non_qualified":null,"image":"1f478-1f3fb.png","sheet_x":25,"sheet_y":25,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F478-1F3FC","non_qualified":null,"image":"1f478-1f3fc.png","sheet_x":25,"sheet_y":26,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F478-1F3FD","non_qualified":null,"image":"1f478-1f3fd.png","sheet_x":25,"sheet_y":27,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F478-1F3FE","non_qualified":null,"image":"1f478-1f3fe.png","sheet_x":25,"sheet_y":28,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F478-1F3FF","non_qualified":null,"image":"1f478-1f3ff.png","sheet_x":25,"sheet_y":29,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"JAPANESE OGRE","unified":"1F479","non_qualified":null,"docomo":null,"au":"EB44","softbank":null,"google":"FE1AC","image":"1f479.png","sheet_x":25,"sheet_y":30,"short_name":"japanese_ogre","short_names":["japanese_ogre"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-costume","sort_order":112,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"JAPANESE GOBLIN","unified":"1F47A","non_qualified":null,"docomo":null,"au":"EB45","softbank":null,"google":"FE1AD","image":"1f47a.png","sheet_x":25,"sheet_y":31,"short_name":"japanese_goblin","short_names":["japanese_goblin"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-costume","sort_order":113,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GHOST","unified":"1F47B","non_qualified":null,"docomo":null,"au":"E4CB","softbank":"E11B","google":"FE1AE","image":"1f47b.png","sheet_x":25,"sheet_y":32,"short_name":"ghost","short_names":["ghost"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-costume","sort_order":114,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BABY ANGEL","unified":"1F47C","non_qualified":null,"docomo":null,"au":"E5BF","softbank":"E04E","google":"FE1AF","image":"1f47c.png","sheet_x":25,"sheet_y":33,"short_name":"angel","short_names":["angel"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":370,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F47C-1F3FB","non_qualified":null,"image":"1f47c-1f3fb.png","sheet_x":25,"sheet_y":34,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F47C-1F3FC","non_qualified":null,"image":"1f47c-1f3fc.png","sheet_x":25,"sheet_y":35,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F47C-1F3FD","non_qualified":null,"image":"1f47c-1f3fd.png","sheet_x":25,"sheet_y":36,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F47C-1F3FE","non_qualified":null,"image":"1f47c-1f3fe.png","sheet_x":25,"sheet_y":37,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F47C-1F3FF","non_qualified":null,"image":"1f47c-1f3ff.png","sheet_x":25,"sheet_y":38,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"EXTRATERRESTRIAL ALIEN","unified":"1F47D","non_qualified":null,"docomo":null,"au":"E50E","softbank":"E10C","google":"FE1B0","image":"1f47d.png","sheet_x":25,"sheet_y":39,"short_name":"alien","short_names":["alien"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-costume","sort_order":115,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ALIEN MONSTER","unified":"1F47E","non_qualified":null,"docomo":null,"au":"E4EC","softbank":"E12B","google":"FE1B1","image":"1f47e.png","sheet_x":25,"sheet_y":40,"short_name":"space_invader","short_names":["space_invader"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-costume","sort_order":116,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"IMP","unified":"1F47F","non_qualified":null,"docomo":null,"au":"E4EF","softbank":"E11A","google":"FE1B2","image":"1f47f.png","sheet_x":25,"sheet_y":41,"short_name":"imp","short_names":["imp"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-negative","sort_order":107,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SKULL","unified":"1F480","non_qualified":null,"docomo":null,"au":"E4F8","softbank":"E11C","google":"FE1B3","image":"1f480.png","sheet_x":25,"sheet_y":42,"short_name":"skull","short_names":["skull"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-negative","sort_order":108,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WOMAN TIPPING HAND","unified":"1F481-200D-2640-FE0F","non_qualified":"1F481-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f481-200d-2640-fe0f.png","sheet_x":25,"sheet_y":43,"short_name":"woman-tipping-hand","short_names":["woman-tipping-hand"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":272,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F481-1F3FB-200D-2640-FE0F","non_qualified":"1F481-1F3FB-200D-2640","image":"1f481-1f3fb-200d-2640-fe0f.png","sheet_x":25,"sheet_y":44,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F481-1F3FC-200D-2640-FE0F","non_qualified":"1F481-1F3FC-200D-2640","image":"1f481-1f3fc-200d-2640-fe0f.png","sheet_x":25,"sheet_y":45,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F481-1F3FD-200D-2640-FE0F","non_qualified":"1F481-1F3FD-200D-2640","image":"1f481-1f3fd-200d-2640-fe0f.png","sheet_x":25,"sheet_y":46,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F481-1F3FE-200D-2640-FE0F","non_qualified":"1F481-1F3FE-200D-2640","image":"1f481-1f3fe-200d-2640-fe0f.png","sheet_x":25,"sheet_y":47,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F481-1F3FF-200D-2640-FE0F","non_qualified":"1F481-1F3FF-200D-2640","image":"1f481-1f3ff-200d-2640-fe0f.png","sheet_x":25,"sheet_y":48,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F481"},{"name":"MAN TIPPING HAND","unified":"1F481-200D-2642-FE0F","non_qualified":"1F481-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f481-200d-2642-fe0f.png","sheet_x":25,"sheet_y":49,"short_name":"man-tipping-hand","short_names":["man-tipping-hand"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":271,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F481-1F3FB-200D-2642-FE0F","non_qualified":"1F481-1F3FB-200D-2642","image":"1f481-1f3fb-200d-2642-fe0f.png","sheet_x":25,"sheet_y":50,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F481-1F3FC-200D-2642-FE0F","non_qualified":"1F481-1F3FC-200D-2642","image":"1f481-1f3fc-200d-2642-fe0f.png","sheet_x":25,"sheet_y":51,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F481-1F3FD-200D-2642-FE0F","non_qualified":"1F481-1F3FD-200D-2642","image":"1f481-1f3fd-200d-2642-fe0f.png","sheet_x":25,"sheet_y":52,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F481-1F3FE-200D-2642-FE0F","non_qualified":"1F481-1F3FE-200D-2642","image":"1f481-1f3fe-200d-2642-fe0f.png","sheet_x":25,"sheet_y":53,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F481-1F3FF-200D-2642-FE0F","non_qualified":"1F481-1F3FF-200D-2642","image":"1f481-1f3ff-200d-2642-fe0f.png","sheet_x":25,"sheet_y":54,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"INFORMATION DESK PERSON","unified":"1F481","non_qualified":null,"docomo":null,"au":null,"softbank":"E253","google":"FE1B4","image":"1f481.png","sheet_x":25,"sheet_y":55,"short_name":"information_desk_person","short_names":["information_desk_person"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":270,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F481-1F3FB","non_qualified":null,"image":"1f481-1f3fb.png","sheet_x":25,"sheet_y":56,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F481-1F3FC","non_qualified":null,"image":"1f481-1f3fc.png","sheet_x":25,"sheet_y":57,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F481-1F3FD","non_qualified":null,"image":"1f481-1f3fd.png","sheet_x":25,"sheet_y":58,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F481-1F3FE","non_qualified":null,"image":"1f481-1f3fe.png","sheet_x":25,"sheet_y":59,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F481-1F3FF","non_qualified":null,"image":"1f481-1f3ff.png","sheet_x":25,"sheet_y":60,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F481-200D-2640-FE0F"},{"name":"WOMAN GUARD","unified":"1F482-200D-2640-FE0F","non_qualified":"1F482-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f482-200d-2640-fe0f.png","sheet_x":25,"sheet_y":61,"short_name":"female-guard","short_names":["female-guard"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":344,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F482-1F3FB-200D-2640-FE0F","non_qualified":"1F482-1F3FB-200D-2640","image":"1f482-1f3fb-200d-2640-fe0f.png","sheet_x":26,"sheet_y":0,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F482-1F3FC-200D-2640-FE0F","non_qualified":"1F482-1F3FC-200D-2640","image":"1f482-1f3fc-200d-2640-fe0f.png","sheet_x":26,"sheet_y":1,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F482-1F3FD-200D-2640-FE0F","non_qualified":"1F482-1F3FD-200D-2640","image":"1f482-1f3fd-200d-2640-fe0f.png","sheet_x":26,"sheet_y":2,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F482-1F3FE-200D-2640-FE0F","non_qualified":"1F482-1F3FE-200D-2640","image":"1f482-1f3fe-200d-2640-fe0f.png","sheet_x":26,"sheet_y":3,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F482-1F3FF-200D-2640-FE0F","non_qualified":"1F482-1F3FF-200D-2640","image":"1f482-1f3ff-200d-2640-fe0f.png","sheet_x":26,"sheet_y":4,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN GUARD","unified":"1F482-200D-2642-FE0F","non_qualified":"1F482-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f482-200d-2642-fe0f.png","sheet_x":26,"sheet_y":5,"short_name":"male-guard","short_names":["male-guard"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":343,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F482-1F3FB-200D-2642-FE0F","non_qualified":"1F482-1F3FB-200D-2642","image":"1f482-1f3fb-200d-2642-fe0f.png","sheet_x":26,"sheet_y":6,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F482-1F3FC-200D-2642-FE0F","non_qualified":"1F482-1F3FC-200D-2642","image":"1f482-1f3fc-200d-2642-fe0f.png","sheet_x":26,"sheet_y":7,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F482-1F3FD-200D-2642-FE0F","non_qualified":"1F482-1F3FD-200D-2642","image":"1f482-1f3fd-200d-2642-fe0f.png","sheet_x":26,"sheet_y":8,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F482-1F3FE-200D-2642-FE0F","non_qualified":"1F482-1F3FE-200D-2642","image":"1f482-1f3fe-200d-2642-fe0f.png","sheet_x":26,"sheet_y":9,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F482-1F3FF-200D-2642-FE0F","non_qualified":"1F482-1F3FF-200D-2642","image":"1f482-1f3ff-200d-2642-fe0f.png","sheet_x":26,"sheet_y":10,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F482"},{"name":"GUARDSMAN","unified":"1F482","non_qualified":null,"docomo":null,"au":null,"softbank":"E51E","google":"FE1B5","image":"1f482.png","sheet_x":26,"sheet_y":11,"short_name":"guardsman","short_names":["guardsman"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":342,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F482-1F3FB","non_qualified":null,"image":"1f482-1f3fb.png","sheet_x":26,"sheet_y":12,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F482-1F3FC","non_qualified":null,"image":"1f482-1f3fc.png","sheet_x":26,"sheet_y":13,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F482-1F3FD","non_qualified":null,"image":"1f482-1f3fd.png","sheet_x":26,"sheet_y":14,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F482-1F3FE","non_qualified":null,"image":"1f482-1f3fe.png","sheet_x":26,"sheet_y":15,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F482-1F3FF","non_qualified":null,"image":"1f482-1f3ff.png","sheet_x":26,"sheet_y":16,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F482-200D-2642-FE0F"},{"name":"DANCER","unified":"1F483","non_qualified":null,"docomo":null,"au":"EB1C","softbank":"E51F","google":"FE1B6","image":"1f483.png","sheet_x":26,"sheet_y":17,"short_name":"dancer","short_names":["dancer"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":447,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F483-1F3FB","non_qualified":null,"image":"1f483-1f3fb.png","sheet_x":26,"sheet_y":18,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F483-1F3FC","non_qualified":null,"image":"1f483-1f3fc.png","sheet_x":26,"sheet_y":19,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F483-1F3FD","non_qualified":null,"image":"1f483-1f3fd.png","sheet_x":26,"sheet_y":20,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F483-1F3FE","non_qualified":null,"image":"1f483-1f3fe.png","sheet_x":26,"sheet_y":21,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F483-1F3FF","non_qualified":null,"image":"1f483-1f3ff.png","sheet_x":26,"sheet_y":22,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"LIPSTICK","unified":"1F484","non_qualified":null,"docomo":"E710","au":"E509","softbank":"E31C","google":"FE195","image":"1f484.png","sheet_x":26,"sheet_y":23,"short_name":"lipstick","short_names":["lipstick"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1194,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NAIL POLISH","unified":"1F485","non_qualified":null,"docomo":null,"au":"EAA0","softbank":"E31D","google":"FE196","image":"1f485.png","sheet_x":26,"sheet_y":24,"short_name":"nail_care","short_names":["nail_care"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-prop","sort_order":210,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F485-1F3FB","non_qualified":null,"image":"1f485-1f3fb.png","sheet_x":26,"sheet_y":25,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F485-1F3FC","non_qualified":null,"image":"1f485-1f3fc.png","sheet_x":26,"sheet_y":26,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F485-1F3FD","non_qualified":null,"image":"1f485-1f3fd.png","sheet_x":26,"sheet_y":27,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F485-1F3FE","non_qualified":null,"image":"1f485-1f3fe.png","sheet_x":26,"sheet_y":28,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F485-1F3FF","non_qualified":null,"image":"1f485-1f3ff.png","sheet_x":26,"sheet_y":29,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN GETTING MASSAGE","unified":"1F486-200D-2640-FE0F","non_qualified":"1F486-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f486-200d-2640-fe0f.png","sheet_x":26,"sheet_y":30,"short_name":"woman-getting-massage","short_names":["woman-getting-massage"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":404,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F486-1F3FB-200D-2640-FE0F","non_qualified":"1F486-1F3FB-200D-2640","image":"1f486-1f3fb-200d-2640-fe0f.png","sheet_x":26,"sheet_y":31,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F486-1F3FC-200D-2640-FE0F","non_qualified":"1F486-1F3FC-200D-2640","image":"1f486-1f3fc-200d-2640-fe0f.png","sheet_x":26,"sheet_y":32,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F486-1F3FD-200D-2640-FE0F","non_qualified":"1F486-1F3FD-200D-2640","image":"1f486-1f3fd-200d-2640-fe0f.png","sheet_x":26,"sheet_y":33,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F486-1F3FE-200D-2640-FE0F","non_qualified":"1F486-1F3FE-200D-2640","image":"1f486-1f3fe-200d-2640-fe0f.png","sheet_x":26,"sheet_y":34,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F486-1F3FF-200D-2640-FE0F","non_qualified":"1F486-1F3FF-200D-2640","image":"1f486-1f3ff-200d-2640-fe0f.png","sheet_x":26,"sheet_y":35,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F486"},{"name":"MAN GETTING MASSAGE","unified":"1F486-200D-2642-FE0F","non_qualified":"1F486-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f486-200d-2642-fe0f.png","sheet_x":26,"sheet_y":36,"short_name":"man-getting-massage","short_names":["man-getting-massage"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":403,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F486-1F3FB-200D-2642-FE0F","non_qualified":"1F486-1F3FB-200D-2642","image":"1f486-1f3fb-200d-2642-fe0f.png","sheet_x":26,"sheet_y":37,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F486-1F3FC-200D-2642-FE0F","non_qualified":"1F486-1F3FC-200D-2642","image":"1f486-1f3fc-200d-2642-fe0f.png","sheet_x":26,"sheet_y":38,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F486-1F3FD-200D-2642-FE0F","non_qualified":"1F486-1F3FD-200D-2642","image":"1f486-1f3fd-200d-2642-fe0f.png","sheet_x":26,"sheet_y":39,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F486-1F3FE-200D-2642-FE0F","non_qualified":"1F486-1F3FE-200D-2642","image":"1f486-1f3fe-200d-2642-fe0f.png","sheet_x":26,"sheet_y":40,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F486-1F3FF-200D-2642-FE0F","non_qualified":"1F486-1F3FF-200D-2642","image":"1f486-1f3ff-200d-2642-fe0f.png","sheet_x":26,"sheet_y":41,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"FACE MASSAGE","unified":"1F486","non_qualified":null,"docomo":null,"au":"E50B","softbank":"E31E","google":"FE197","image":"1f486.png","sheet_x":26,"sheet_y":42,"short_name":"massage","short_names":["massage"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":402,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F486-1F3FB","non_qualified":null,"image":"1f486-1f3fb.png","sheet_x":26,"sheet_y":43,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F486-1F3FC","non_qualified":null,"image":"1f486-1f3fc.png","sheet_x":26,"sheet_y":44,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F486-1F3FD","non_qualified":null,"image":"1f486-1f3fd.png","sheet_x":26,"sheet_y":45,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F486-1F3FE","non_qualified":null,"image":"1f486-1f3fe.png","sheet_x":26,"sheet_y":46,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F486-1F3FF","non_qualified":null,"image":"1f486-1f3ff.png","sheet_x":26,"sheet_y":47,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F486-200D-2640-FE0F"},{"name":"WOMAN GETTING HAIRCUT","unified":"1F487-200D-2640-FE0F","non_qualified":"1F487-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f487-200d-2640-fe0f.png","sheet_x":26,"sheet_y":48,"short_name":"woman-getting-haircut","short_names":["woman-getting-haircut"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":407,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F487-1F3FB-200D-2640-FE0F","non_qualified":"1F487-1F3FB-200D-2640","image":"1f487-1f3fb-200d-2640-fe0f.png","sheet_x":26,"sheet_y":49,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F487-1F3FC-200D-2640-FE0F","non_qualified":"1F487-1F3FC-200D-2640","image":"1f487-1f3fc-200d-2640-fe0f.png","sheet_x":26,"sheet_y":50,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F487-1F3FD-200D-2640-FE0F","non_qualified":"1F487-1F3FD-200D-2640","image":"1f487-1f3fd-200d-2640-fe0f.png","sheet_x":26,"sheet_y":51,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F487-1F3FE-200D-2640-FE0F","non_qualified":"1F487-1F3FE-200D-2640","image":"1f487-1f3fe-200d-2640-fe0f.png","sheet_x":26,"sheet_y":52,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F487-1F3FF-200D-2640-FE0F","non_qualified":"1F487-1F3FF-200D-2640","image":"1f487-1f3ff-200d-2640-fe0f.png","sheet_x":26,"sheet_y":53,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F487"},{"name":"MAN GETTING HAIRCUT","unified":"1F487-200D-2642-FE0F","non_qualified":"1F487-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f487-200d-2642-fe0f.png","sheet_x":26,"sheet_y":54,"short_name":"man-getting-haircut","short_names":["man-getting-haircut"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":406,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F487-1F3FB-200D-2642-FE0F","non_qualified":"1F487-1F3FB-200D-2642","image":"1f487-1f3fb-200d-2642-fe0f.png","sheet_x":26,"sheet_y":55,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F487-1F3FC-200D-2642-FE0F","non_qualified":"1F487-1F3FC-200D-2642","image":"1f487-1f3fc-200d-2642-fe0f.png","sheet_x":26,"sheet_y":56,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F487-1F3FD-200D-2642-FE0F","non_qualified":"1F487-1F3FD-200D-2642","image":"1f487-1f3fd-200d-2642-fe0f.png","sheet_x":26,"sheet_y":57,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F487-1F3FE-200D-2642-FE0F","non_qualified":"1F487-1F3FE-200D-2642","image":"1f487-1f3fe-200d-2642-fe0f.png","sheet_x":26,"sheet_y":58,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F487-1F3FF-200D-2642-FE0F","non_qualified":"1F487-1F3FF-200D-2642","image":"1f487-1f3ff-200d-2642-fe0f.png","sheet_x":26,"sheet_y":59,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"HAIRCUT","unified":"1F487","non_qualified":null,"docomo":"E675","au":"EAA1","softbank":"E31F","google":"FE198","image":"1f487.png","sheet_x":26,"sheet_y":60,"short_name":"haircut","short_names":["haircut"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":405,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F487-1F3FB","non_qualified":null,"image":"1f487-1f3fb.png","sheet_x":26,"sheet_y":61,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F487-1F3FC","non_qualified":null,"image":"1f487-1f3fc.png","sheet_x":27,"sheet_y":0,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F487-1F3FD","non_qualified":null,"image":"1f487-1f3fd.png","sheet_x":27,"sheet_y":1,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F487-1F3FE","non_qualified":null,"image":"1f487-1f3fe.png","sheet_x":27,"sheet_y":2,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F487-1F3FF","non_qualified":null,"image":"1f487-1f3ff.png","sheet_x":27,"sheet_y":3,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F487-200D-2640-FE0F"},{"name":"BARBER POLE","unified":"1F488","non_qualified":null,"docomo":null,"au":"EAA2","softbank":"E320","google":"FE199","image":"1f488.png","sheet_x":27,"sheet_y":4,"short_name":"barber","short_names":["barber"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-other","sort_order":911,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SYRINGE","unified":"1F489","non_qualified":null,"docomo":null,"au":"E510","softbank":"E13B","google":"FE509","image":"1f489.png","sheet_x":27,"sheet_y":5,"short_name":"syringe","short_names":["syringe"],"text":null,"texts":null,"category":"Objects","subcategory":"medical","sort_order":1371,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PILL","unified":"1F48A","non_qualified":null,"docomo":null,"au":"EA9A","softbank":"E30F","google":"FE50A","image":"1f48a.png","sheet_x":27,"sheet_y":6,"short_name":"pill","short_names":["pill"],"text":null,"texts":null,"category":"Objects","subcategory":"medical","sort_order":1373,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"KISS MARK","unified":"1F48B","non_qualified":null,"docomo":"E6F9","au":"E4EB","softbank":"E003","google":"FE823","image":"1f48b.png","sheet_x":27,"sheet_y":7,"short_name":"kiss","short_names":["kiss"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":155,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LOVE LETTER","unified":"1F48C","non_qualified":null,"docomo":"E717","au":"EB78","softbank":null,"google":"FE824","image":"1f48c.png","sheet_x":27,"sheet_y":8,"short_name":"love_letter","short_names":["love_letter"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"heart","sort_order":130,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RING","unified":"1F48D","non_qualified":null,"docomo":"E71B","au":"E514","softbank":"E034","google":"FE825","image":"1f48d.png","sheet_x":27,"sheet_y":9,"short_name":"ring","short_names":["ring"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1195,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GEM STONE","unified":"1F48E","non_qualified":null,"docomo":"E71B","au":"E514","softbank":"E035","google":"FE826","image":"1f48e.png","sheet_x":27,"sheet_y":10,"short_name":"gem","short_names":["gem"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1196,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"KISS","unified":"1F48F","non_qualified":null,"docomo":"E6F9","au":"E5CA","softbank":"E111","google":"FE827","image":"1f48f.png","sheet_x":27,"sheet_y":11,"short_name":"couplekiss","short_names":["couplekiss"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":511,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F48F-1F3FB","non_qualified":null,"image":"1f48f-1f3fb.png","sheet_x":27,"sheet_y":12,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F48F-1F3FC","non_qualified":null,"image":"1f48f-1f3fc.png","sheet_x":27,"sheet_y":13,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F48F-1F3FD","non_qualified":null,"image":"1f48f-1f3fd.png","sheet_x":27,"sheet_y":14,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F48F-1F3FE","non_qualified":null,"image":"1f48f-1f3fe.png","sheet_x":27,"sheet_y":15,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F48F-1F3FF","non_qualified":null,"image":"1f48f-1f3ff.png","sheet_x":27,"sheet_y":16,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FC":{"unified":"1F9D1-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FC","non_qualified":"1F9D1-1F3FB-200D-2764-200D-1F48B-200D-1F9D1-1F3FC","image":"1f9d1-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fc.png","sheet_x":27,"sheet_y":17,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FD":{"unified":"1F9D1-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FD","non_qualified":"1F9D1-1F3FB-200D-2764-200D-1F48B-200D-1F9D1-1F3FD","image":"1f9d1-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fd.png","sheet_x":27,"sheet_y":18,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FE":{"unified":"1F9D1-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FE","non_qualified":"1F9D1-1F3FB-200D-2764-200D-1F48B-200D-1F9D1-1F3FE","image":"1f9d1-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fe.png","sheet_x":27,"sheet_y":19,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FF":{"unified":"1F9D1-1F3FB-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FF","non_qualified":"1F9D1-1F3FB-200D-2764-200D-1F48B-200D-1F9D1-1F3FF","image":"1f9d1-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3ff.png","sheet_x":27,"sheet_y":20,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FB":{"unified":"1F9D1-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FB","non_qualified":"1F9D1-1F3FC-200D-2764-200D-1F48B-200D-1F9D1-1F3FB","image":"1f9d1-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fb.png","sheet_x":27,"sheet_y":21,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FD":{"unified":"1F9D1-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FD","non_qualified":"1F9D1-1F3FC-200D-2764-200D-1F48B-200D-1F9D1-1F3FD","image":"1f9d1-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fd.png","sheet_x":27,"sheet_y":22,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FE":{"unified":"1F9D1-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FE","non_qualified":"1F9D1-1F3FC-200D-2764-200D-1F48B-200D-1F9D1-1F3FE","image":"1f9d1-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fe.png","sheet_x":27,"sheet_y":23,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FF":{"unified":"1F9D1-1F3FC-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FF","non_qualified":"1F9D1-1F3FC-200D-2764-200D-1F48B-200D-1F9D1-1F3FF","image":"1f9d1-1f3fc-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3ff.png","sheet_x":27,"sheet_y":24,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FB":{"unified":"1F9D1-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FB","non_qualified":"1F9D1-1F3FD-200D-2764-200D-1F48B-200D-1F9D1-1F3FB","image":"1f9d1-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fb.png","sheet_x":27,"sheet_y":25,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FC":{"unified":"1F9D1-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FC","non_qualified":"1F9D1-1F3FD-200D-2764-200D-1F48B-200D-1F9D1-1F3FC","image":"1f9d1-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fc.png","sheet_x":27,"sheet_y":26,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FE":{"unified":"1F9D1-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FE","non_qualified":"1F9D1-1F3FD-200D-2764-200D-1F48B-200D-1F9D1-1F3FE","image":"1f9d1-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fe.png","sheet_x":27,"sheet_y":27,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FF":{"unified":"1F9D1-1F3FD-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FF","non_qualified":"1F9D1-1F3FD-200D-2764-200D-1F48B-200D-1F9D1-1F3FF","image":"1f9d1-1f3fd-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3ff.png","sheet_x":27,"sheet_y":28,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FB":{"unified":"1F9D1-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FB","non_qualified":"1F9D1-1F3FE-200D-2764-200D-1F48B-200D-1F9D1-1F3FB","image":"1f9d1-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fb.png","sheet_x":27,"sheet_y":29,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FC":{"unified":"1F9D1-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FC","non_qualified":"1F9D1-1F3FE-200D-2764-200D-1F48B-200D-1F9D1-1F3FC","image":"1f9d1-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fc.png","sheet_x":27,"sheet_y":30,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FD":{"unified":"1F9D1-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FD","non_qualified":"1F9D1-1F3FE-200D-2764-200D-1F48B-200D-1F9D1-1F3FD","image":"1f9d1-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fd.png","sheet_x":27,"sheet_y":31,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FF":{"unified":"1F9D1-1F3FE-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FF","non_qualified":"1F9D1-1F3FE-200D-2764-200D-1F48B-200D-1F9D1-1F3FF","image":"1f9d1-1f3fe-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3ff.png","sheet_x":27,"sheet_y":32,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FB":{"unified":"1F9D1-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FB","non_qualified":"1F9D1-1F3FF-200D-2764-200D-1F48B-200D-1F9D1-1F3FB","image":"1f9d1-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fb.png","sheet_x":27,"sheet_y":33,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FC":{"unified":"1F9D1-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FC","non_qualified":"1F9D1-1F3FF-200D-2764-200D-1F48B-200D-1F9D1-1F3FC","image":"1f9d1-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fc.png","sheet_x":27,"sheet_y":34,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FD":{"unified":"1F9D1-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FD","non_qualified":"1F9D1-1F3FF-200D-2764-200D-1F48B-200D-1F9D1-1F3FD","image":"1f9d1-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fd.png","sheet_x":27,"sheet_y":35,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FE":{"unified":"1F9D1-1F3FF-200D-2764-FE0F-200D-1F48B-200D-1F9D1-1F3FE","non_qualified":"1F9D1-1F3FF-200D-2764-200D-1F48B-200D-1F9D1-1F3FE","image":"1f9d1-1f3ff-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fe.png","sheet_x":27,"sheet_y":36,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"BOUQUET","unified":"1F490","non_qualified":null,"docomo":null,"au":"EA95","softbank":"E306","google":"FE828","image":"1f490.png","sheet_x":27,"sheet_y":37,"short_name":"bouquet","short_names":["bouquet"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-flower","sort_order":684,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"COUPLE WITH HEART","unified":"1F491","non_qualified":null,"docomo":"E6ED","au":"EADA","softbank":"E425","google":"FE829","image":"1f491.png","sheet_x":27,"sheet_y":38,"short_name":"couple_with_heart","short_names":["couple_with_heart"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":515,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F491-1F3FB","non_qualified":null,"image":"1f491-1f3fb.png","sheet_x":27,"sheet_y":39,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F491-1F3FC","non_qualified":null,"image":"1f491-1f3fc.png","sheet_x":27,"sheet_y":40,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F491-1F3FD","non_qualified":null,"image":"1f491-1f3fd.png","sheet_x":27,"sheet_y":41,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F491-1F3FE","non_qualified":null,"image":"1f491-1f3fe.png","sheet_x":27,"sheet_y":42,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F491-1F3FF","non_qualified":null,"image":"1f491-1f3ff.png","sheet_x":27,"sheet_y":43,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FC":{"unified":"1F9D1-1F3FB-200D-2764-FE0F-200D-1F9D1-1F3FC","non_qualified":"1F9D1-1F3FB-200D-2764-200D-1F9D1-1F3FC","image":"1f9d1-1f3fb-200d-2764-fe0f-200d-1f9d1-1f3fc.png","sheet_x":27,"sheet_y":44,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FD":{"unified":"1F9D1-1F3FB-200D-2764-FE0F-200D-1F9D1-1F3FD","non_qualified":"1F9D1-1F3FB-200D-2764-200D-1F9D1-1F3FD","image":"1f9d1-1f3fb-200d-2764-fe0f-200d-1f9d1-1f3fd.png","sheet_x":27,"sheet_y":45,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FE":{"unified":"1F9D1-1F3FB-200D-2764-FE0F-200D-1F9D1-1F3FE","non_qualified":"1F9D1-1F3FB-200D-2764-200D-1F9D1-1F3FE","image":"1f9d1-1f3fb-200d-2764-fe0f-200d-1f9d1-1f3fe.png","sheet_x":27,"sheet_y":46,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FF":{"unified":"1F9D1-1F3FB-200D-2764-FE0F-200D-1F9D1-1F3FF","non_qualified":"1F9D1-1F3FB-200D-2764-200D-1F9D1-1F3FF","image":"1f9d1-1f3fb-200d-2764-fe0f-200d-1f9d1-1f3ff.png","sheet_x":27,"sheet_y":47,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FB":{"unified":"1F9D1-1F3FC-200D-2764-FE0F-200D-1F9D1-1F3FB","non_qualified":"1F9D1-1F3FC-200D-2764-200D-1F9D1-1F3FB","image":"1f9d1-1f3fc-200d-2764-fe0f-200d-1f9d1-1f3fb.png","sheet_x":27,"sheet_y":48,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FD":{"unified":"1F9D1-1F3FC-200D-2764-FE0F-200D-1F9D1-1F3FD","non_qualified":"1F9D1-1F3FC-200D-2764-200D-1F9D1-1F3FD","image":"1f9d1-1f3fc-200d-2764-fe0f-200d-1f9d1-1f3fd.png","sheet_x":27,"sheet_y":49,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FE":{"unified":"1F9D1-1F3FC-200D-2764-FE0F-200D-1F9D1-1F3FE","non_qualified":"1F9D1-1F3FC-200D-2764-200D-1F9D1-1F3FE","image":"1f9d1-1f3fc-200d-2764-fe0f-200d-1f9d1-1f3fe.png","sheet_x":27,"sheet_y":50,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FF":{"unified":"1F9D1-1F3FC-200D-2764-FE0F-200D-1F9D1-1F3FF","non_qualified":"1F9D1-1F3FC-200D-2764-200D-1F9D1-1F3FF","image":"1f9d1-1f3fc-200d-2764-fe0f-200d-1f9d1-1f3ff.png","sheet_x":27,"sheet_y":51,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FB":{"unified":"1F9D1-1F3FD-200D-2764-FE0F-200D-1F9D1-1F3FB","non_qualified":"1F9D1-1F3FD-200D-2764-200D-1F9D1-1F3FB","image":"1f9d1-1f3fd-200d-2764-fe0f-200d-1f9d1-1f3fb.png","sheet_x":27,"sheet_y":52,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FC":{"unified":"1F9D1-1F3FD-200D-2764-FE0F-200D-1F9D1-1F3FC","non_qualified":"1F9D1-1F3FD-200D-2764-200D-1F9D1-1F3FC","image":"1f9d1-1f3fd-200d-2764-fe0f-200d-1f9d1-1f3fc.png","sheet_x":27,"sheet_y":53,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FE":{"unified":"1F9D1-1F3FD-200D-2764-FE0F-200D-1F9D1-1F3FE","non_qualified":"1F9D1-1F3FD-200D-2764-200D-1F9D1-1F3FE","image":"1f9d1-1f3fd-200d-2764-fe0f-200d-1f9d1-1f3fe.png","sheet_x":27,"sheet_y":54,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FF":{"unified":"1F9D1-1F3FD-200D-2764-FE0F-200D-1F9D1-1F3FF","non_qualified":"1F9D1-1F3FD-200D-2764-200D-1F9D1-1F3FF","image":"1f9d1-1f3fd-200d-2764-fe0f-200d-1f9d1-1f3ff.png","sheet_x":27,"sheet_y":55,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FB":{"unified":"1F9D1-1F3FE-200D-2764-FE0F-200D-1F9D1-1F3FB","non_qualified":"1F9D1-1F3FE-200D-2764-200D-1F9D1-1F3FB","image":"1f9d1-1f3fe-200d-2764-fe0f-200d-1f9d1-1f3fb.png","sheet_x":27,"sheet_y":56,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FC":{"unified":"1F9D1-1F3FE-200D-2764-FE0F-200D-1F9D1-1F3FC","non_qualified":"1F9D1-1F3FE-200D-2764-200D-1F9D1-1F3FC","image":"1f9d1-1f3fe-200d-2764-fe0f-200d-1f9d1-1f3fc.png","sheet_x":27,"sheet_y":57,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FD":{"unified":"1F9D1-1F3FE-200D-2764-FE0F-200D-1F9D1-1F3FD","non_qualified":"1F9D1-1F3FE-200D-2764-200D-1F9D1-1F3FD","image":"1f9d1-1f3fe-200d-2764-fe0f-200d-1f9d1-1f3fd.png","sheet_x":27,"sheet_y":58,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FF":{"unified":"1F9D1-1F3FE-200D-2764-FE0F-200D-1F9D1-1F3FF","non_qualified":"1F9D1-1F3FE-200D-2764-200D-1F9D1-1F3FF","image":"1f9d1-1f3fe-200d-2764-fe0f-200d-1f9d1-1f3ff.png","sheet_x":27,"sheet_y":59,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FB":{"unified":"1F9D1-1F3FF-200D-2764-FE0F-200D-1F9D1-1F3FB","non_qualified":"1F9D1-1F3FF-200D-2764-200D-1F9D1-1F3FB","image":"1f9d1-1f3ff-200d-2764-fe0f-200d-1f9d1-1f3fb.png","sheet_x":27,"sheet_y":60,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FC":{"unified":"1F9D1-1F3FF-200D-2764-FE0F-200D-1F9D1-1F3FC","non_qualified":"1F9D1-1F3FF-200D-2764-200D-1F9D1-1F3FC","image":"1f9d1-1f3ff-200d-2764-fe0f-200d-1f9d1-1f3fc.png","sheet_x":27,"sheet_y":61,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FD":{"unified":"1F9D1-1F3FF-200D-2764-FE0F-200D-1F9D1-1F3FD","non_qualified":"1F9D1-1F3FF-200D-2764-200D-1F9D1-1F3FD","image":"1f9d1-1f3ff-200d-2764-fe0f-200d-1f9d1-1f3fd.png","sheet_x":28,"sheet_y":0,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FE":{"unified":"1F9D1-1F3FF-200D-2764-FE0F-200D-1F9D1-1F3FE","non_qualified":"1F9D1-1F3FF-200D-2764-200D-1F9D1-1F3FE","image":"1f9d1-1f3ff-200d-2764-fe0f-200d-1f9d1-1f3fe.png","sheet_x":28,"sheet_y":1,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WEDDING","unified":"1F492","non_qualified":null,"docomo":null,"au":"E5BB","softbank":"E43D","google":"FE82A","image":"1f492.png","sheet_x":28,"sheet_y":2,"short_name":"wedding","short_names":["wedding"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":887,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BEATING HEART","unified":"1F493","non_qualified":null,"docomo":"E6ED","au":"EB75","softbank":"E327","google":"FEB0D","image":"1f493.png","sheet_x":28,"sheet_y":3,"short_name":"heartbeat","short_names":["heartbeat"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"heart","sort_order":135,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BROKEN HEART","unified":"1F494","non_qualified":null,"docomo":"E6EE","au":"E477","softbank":"E023","google":"FEB0E","image":"1f494.png","sheet_x":28,"sheet_y":4,"short_name":"broken_heart","short_names":["broken_heart"],"text":"<\/3","texts":["<\/3"],"category":"Smileys & Emotion","subcategory":"heart","sort_order":140,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TWO HEARTS","unified":"1F495","non_qualified":null,"docomo":"E6EF","au":"E478","softbank":null,"google":"FEB0F","image":"1f495.png","sheet_x":28,"sheet_y":5,"short_name":"two_hearts","short_names":["two_hearts"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"heart","sort_order":137,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SPARKLING HEART","unified":"1F496","non_qualified":null,"docomo":"E6EC","au":"EAA6","softbank":null,"google":"FEB10","image":"1f496.png","sheet_x":28,"sheet_y":6,"short_name":"sparkling_heart","short_names":["sparkling_heart"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"heart","sort_order":133,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GROWING HEART","unified":"1F497","non_qualified":null,"docomo":"E6ED","au":"EB75","softbank":"E328","google":"FEB11","image":"1f497.png","sheet_x":28,"sheet_y":7,"short_name":"heartpulse","short_names":["heartpulse"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"heart","sort_order":134,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HEART WITH ARROW","unified":"1F498","non_qualified":null,"docomo":"E6EC","au":"E4EA","softbank":"E329","google":"FEB12","image":"1f498.png","sheet_x":28,"sheet_y":8,"short_name":"cupid","short_names":["cupid"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"heart","sort_order":131,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLUE HEART","unified":"1F499","non_qualified":null,"docomo":"E6EC","au":"EAA7","softbank":"E32A","google":"FEB13","image":"1f499.png","sheet_x":28,"sheet_y":9,"short_name":"blue_heart","short_names":["blue_heart"],"text":"<3","texts":null,"category":"Smileys & Emotion","subcategory":"heart","sort_order":148,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GREEN HEART","unified":"1F49A","non_qualified":null,"docomo":"E6EC","au":"EAA8","softbank":"E32B","google":"FEB14","image":"1f49a.png","sheet_x":28,"sheet_y":10,"short_name":"green_heart","short_names":["green_heart"],"text":"<3","texts":null,"category":"Smileys & Emotion","subcategory":"heart","sort_order":147,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"YELLOW HEART","unified":"1F49B","non_qualified":null,"docomo":"E6EC","au":"EAA9","softbank":"E32C","google":"FEB15","image":"1f49b.png","sheet_x":28,"sheet_y":11,"short_name":"yellow_heart","short_names":["yellow_heart"],"text":"<3","texts":null,"category":"Smileys & Emotion","subcategory":"heart","sort_order":146,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PURPLE HEART","unified":"1F49C","non_qualified":null,"docomo":"E6EC","au":"EAAA","softbank":"E32D","google":"FEB16","image":"1f49c.png","sheet_x":28,"sheet_y":12,"short_name":"purple_heart","short_names":["purple_heart"],"text":"<3","texts":null,"category":"Smileys & Emotion","subcategory":"heart","sort_order":150,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HEART WITH RIBBON","unified":"1F49D","non_qualified":null,"docomo":"E6EC","au":"EB54","softbank":"E437","google":"FEB17","image":"1f49d.png","sheet_x":28,"sheet_y":13,"short_name":"gift_heart","short_names":["gift_heart"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"heart","sort_order":132,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"REVOLVING HEARTS","unified":"1F49E","non_qualified":null,"docomo":"E6ED","au":"E5AF","softbank":null,"google":"FEB18","image":"1f49e.png","sheet_x":28,"sheet_y":14,"short_name":"revolving_hearts","short_names":["revolving_hearts"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"heart","sort_order":136,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HEART DECORATION","unified":"1F49F","non_qualified":null,"docomo":"E6F8","au":"E595","softbank":"E204","google":"FEB19","image":"1f49f.png","sheet_x":28,"sheet_y":15,"short_name":"heart_decoration","short_names":["heart_decoration"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"heart","sort_order":138,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DIAMOND SHAPE WITH A DOT INSIDE","unified":"1F4A0","non_qualified":null,"docomo":"E6F8","au":null,"softbank":null,"google":"FEB55","image":"1f4a0.png","sheet_x":28,"sheet_y":16,"short_name":"diamond_shape_with_a_dot_inside","short_names":["diamond_shape_with_a_dot_inside"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1631,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ELECTRIC LIGHT BULB","unified":"1F4A1","non_qualified":null,"docomo":"E6FB","au":"E476","softbank":"E10F","google":"FEB56","image":"1f4a1.png","sheet_x":28,"sheet_y":17,"short_name":"bulb","short_names":["bulb"],"text":null,"texts":null,"category":"Objects","subcategory":"light & video","sort_order":1258,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ANGER SYMBOL","unified":"1F4A2","non_qualified":null,"docomo":"E6FC","au":"E4E5","softbank":"E334","google":"FEB57","image":"1f4a2.png","sheet_x":28,"sheet_y":18,"short_name":"anger","short_names":["anger"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":157,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BOMB","unified":"1F4A3","non_qualified":null,"docomo":"E6FE","au":"E47A","softbank":"E311","google":"FEB58","image":"1f4a3.png","sheet_x":28,"sheet_y":19,"short_name":"bomb","short_names":["bomb"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1345,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SLEEPING SYMBOL","unified":"1F4A4","non_qualified":null,"docomo":"E701","au":"E475","softbank":"E13C","google":"FEB59","image":"1f4a4.png","sheet_x":28,"sheet_y":20,"short_name":"zzz","short_names":["zzz"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":168,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"COLLISION SYMBOL","unified":"1F4A5","non_qualified":null,"docomo":"E705","au":"E5B0","softbank":null,"google":"FEB5A","image":"1f4a5.png","sheet_x":28,"sheet_y":21,"short_name":"boom","short_names":["boom","collision"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":158,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SPLASHING SWEAT SYMBOL","unified":"1F4A6","non_qualified":null,"docomo":"E706","au":"E5B1","softbank":"E331","google":"FEB5B","image":"1f4a6.png","sheet_x":28,"sheet_y":22,"short_name":"sweat_drops","short_names":["sweat_drops"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":160,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DROPLET","unified":"1F4A7","non_qualified":null,"docomo":"E707","au":"E4E6","softbank":null,"google":"FEB5C","image":"1f4a7.png","sheet_x":28,"sheet_y":23,"short_name":"droplet","short_names":["droplet"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1063,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DASH SYMBOL","unified":"1F4A8","non_qualified":null,"docomo":"E708","au":"E4F4","softbank":"E330","google":"FEB5D","image":"1f4a8.png","sheet_x":28,"sheet_y":24,"short_name":"dash","short_names":["dash"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":161,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PILE OF POO","unified":"1F4A9","non_qualified":null,"docomo":null,"au":"E4F5","softbank":"E05A","google":"FE4F4","image":"1f4a9.png","sheet_x":28,"sheet_y":25,"short_name":"hankey","short_names":["hankey","poop","shit"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-costume","sort_order":110,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FLEXED BICEPS","unified":"1F4AA","non_qualified":null,"docomo":null,"au":"E4E9","softbank":"E14C","google":"FEB5E","image":"1f4aa.png","sheet_x":28,"sheet_y":26,"short_name":"muscle","short_names":["muscle"],"text":null,"texts":null,"category":"People & Body","subcategory":"body-parts","sort_order":212,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F4AA-1F3FB","non_qualified":null,"image":"1f4aa-1f3fb.png","sheet_x":28,"sheet_y":27,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F4AA-1F3FC","non_qualified":null,"image":"1f4aa-1f3fc.png","sheet_x":28,"sheet_y":28,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F4AA-1F3FD","non_qualified":null,"image":"1f4aa-1f3fd.png","sheet_x":28,"sheet_y":29,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F4AA-1F3FE","non_qualified":null,"image":"1f4aa-1f3fe.png","sheet_x":28,"sheet_y":30,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F4AA-1F3FF","non_qualified":null,"image":"1f4aa-1f3ff.png","sheet_x":28,"sheet_y":31,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"DIZZY SYMBOL","unified":"1F4AB","non_qualified":null,"docomo":null,"au":"EB5C","softbank":null,"google":"FEB5F","image":"1f4ab.png","sheet_x":28,"sheet_y":32,"short_name":"dizzy","short_names":["dizzy"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":159,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SPEECH BALLOON","unified":"1F4AC","non_qualified":null,"docomo":null,"au":"E4FD","softbank":null,"google":"FE532","image":"1f4ac.png","sheet_x":28,"sheet_y":33,"short_name":"speech_balloon","short_names":["speech_balloon"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":163,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"THOUGHT BALLOON","unified":"1F4AD","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f4ad.png","sheet_x":28,"sheet_y":34,"short_name":"thought_balloon","short_names":["thought_balloon"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":167,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WHITE FLOWER","unified":"1F4AE","non_qualified":null,"docomo":null,"au":"E4F0","softbank":null,"google":"FEB7A","image":"1f4ae.png","sheet_x":28,"sheet_y":35,"short_name":"white_flower","short_names":["white_flower"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-flower","sort_order":686,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HUNDRED POINTS SYMBOL","unified":"1F4AF","non_qualified":null,"docomo":null,"au":"E4F2","softbank":null,"google":"FEB7B","image":"1f4af.png","sheet_x":28,"sheet_y":36,"short_name":"100","short_names":["100"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":156,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MONEY BAG","unified":"1F4B0","non_qualified":null,"docomo":"E715","au":"E4C7","softbank":"E12F","google":"FE4DD","image":"1f4b0.png","sheet_x":28,"sheet_y":37,"short_name":"moneybag","short_names":["moneybag"],"text":null,"texts":null,"category":"Objects","subcategory":"money","sort_order":1279,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CURRENCY EXCHANGE","unified":"1F4B1","non_qualified":null,"docomo":null,"au":null,"softbank":"E149","google":"FE4DE","image":"1f4b1.png","sheet_x":28,"sheet_y":38,"short_name":"currency_exchange","short_names":["currency_exchange"],"text":null,"texts":null,"category":"Symbols","subcategory":"currency","sort_order":1526,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HEAVY DOLLAR SIGN","unified":"1F4B2","non_qualified":null,"docomo":"E715","au":"E579","softbank":null,"google":"FE4E0","image":"1f4b2.png","sheet_x":28,"sheet_y":39,"short_name":"heavy_dollar_sign","short_names":["heavy_dollar_sign"],"text":null,"texts":null,"category":"Symbols","subcategory":"currency","sort_order":1527,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CREDIT CARD","unified":"1F4B3","non_qualified":null,"docomo":null,"au":"E57C","softbank":null,"google":"FE4E1","image":"1f4b3.png","sheet_x":28,"sheet_y":40,"short_name":"credit_card","short_names":["credit_card"],"text":null,"texts":null,"category":"Objects","subcategory":"money","sort_order":1286,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BANKNOTE WITH YEN SIGN","unified":"1F4B4","non_qualified":null,"docomo":"E6D6","au":"E57D","softbank":null,"google":"FE4E2","image":"1f4b4.png","sheet_x":28,"sheet_y":41,"short_name":"yen","short_names":["yen"],"text":null,"texts":null,"category":"Objects","subcategory":"money","sort_order":1281,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BANKNOTE WITH DOLLAR SIGN","unified":"1F4B5","non_qualified":null,"docomo":"E715","au":"E585","softbank":null,"google":"FE4E3","image":"1f4b5.png","sheet_x":28,"sheet_y":42,"short_name":"dollar","short_names":["dollar"],"text":null,"texts":null,"category":"Objects","subcategory":"money","sort_order":1282,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BANKNOTE WITH EURO SIGN","unified":"1F4B6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f4b6.png","sheet_x":28,"sheet_y":43,"short_name":"euro","short_names":["euro"],"text":null,"texts":null,"category":"Objects","subcategory":"money","sort_order":1283,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BANKNOTE WITH POUND SIGN","unified":"1F4B7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f4b7.png","sheet_x":28,"sheet_y":44,"short_name":"pound","short_names":["pound"],"text":null,"texts":null,"category":"Objects","subcategory":"money","sort_order":1284,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MONEY WITH WINGS","unified":"1F4B8","non_qualified":null,"docomo":null,"au":"EB5B","softbank":null,"google":"FE4E4","image":"1f4b8.png","sheet_x":28,"sheet_y":45,"short_name":"money_with_wings","short_names":["money_with_wings"],"text":null,"texts":null,"category":"Objects","subcategory":"money","sort_order":1285,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CHART WITH UPWARDS TREND AND YEN SIGN","unified":"1F4B9","non_qualified":null,"docomo":null,"au":"E5DC","softbank":"E14A","google":"FE4DF","image":"1f4b9.png","sheet_x":28,"sheet_y":46,"short_name":"chart","short_names":["chart"],"text":null,"texts":null,"category":"Objects","subcategory":"money","sort_order":1288,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SEAT","unified":"1F4BA","non_qualified":null,"docomo":"E6B2","au":null,"softbank":"E11F","google":"FE537","image":"1f4ba.png","sheet_x":28,"sheet_y":47,"short_name":"seat","short_names":["seat"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-air","sort_order":977,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PERSONAL COMPUTER","unified":"1F4BB","non_qualified":null,"docomo":"E716","au":"E5B8","softbank":"E00C","google":"FE538","image":"1f4bb.png","sheet_x":28,"sheet_y":48,"short_name":"computer","short_names":["computer"],"text":null,"texts":null,"category":"Objects","subcategory":"computer","sort_order":1235,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BRIEFCASE","unified":"1F4BC","non_qualified":null,"docomo":"E682","au":"E5CE","softbank":"E11E","google":"FE53B","image":"1f4bc.png","sheet_x":28,"sheet_y":49,"short_name":"briefcase","short_names":["briefcase"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1309,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MINIDISC","unified":"1F4BD","non_qualified":null,"docomo":null,"au":"E582","softbank":"E316","google":"FE53C","image":"1f4bd.png","sheet_x":28,"sheet_y":50,"short_name":"minidisc","short_names":["minidisc"],"text":null,"texts":null,"category":"Objects","subcategory":"computer","sort_order":1241,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FLOPPY DISK","unified":"1F4BE","non_qualified":null,"docomo":null,"au":"E562","softbank":null,"google":"FE53D","image":"1f4be.png","sheet_x":28,"sheet_y":51,"short_name":"floppy_disk","short_names":["floppy_disk"],"text":null,"texts":null,"category":"Objects","subcategory":"computer","sort_order":1242,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"OPTICAL DISC","unified":"1F4BF","non_qualified":null,"docomo":"E68C","au":"E50C","softbank":"E126","google":"FE81D","image":"1f4bf.png","sheet_x":28,"sheet_y":52,"short_name":"cd","short_names":["cd"],"text":null,"texts":null,"category":"Objects","subcategory":"computer","sort_order":1243,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DVD","unified":"1F4C0","non_qualified":null,"docomo":"E68C","au":"E50C","softbank":"E127","google":"FE81E","image":"1f4c0.png","sheet_x":28,"sheet_y":53,"short_name":"dvd","short_names":["dvd"],"text":null,"texts":null,"category":"Objects","subcategory":"computer","sort_order":1244,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FILE FOLDER","unified":"1F4C1","non_qualified":null,"docomo":null,"au":"E58F","softbank":null,"google":"FE543","image":"1f4c1.png","sheet_x":28,"sheet_y":54,"short_name":"file_folder","short_names":["file_folder"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1310,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"OPEN FILE FOLDER","unified":"1F4C2","non_qualified":null,"docomo":null,"au":"E590","softbank":null,"google":"FE544","image":"1f4c2.png","sheet_x":28,"sheet_y":55,"short_name":"open_file_folder","short_names":["open_file_folder"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1311,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PAGE WITH CURL","unified":"1F4C3","non_qualified":null,"docomo":"E689","au":"E561","softbank":null,"google":"FE540","image":"1f4c3.png","sheet_x":28,"sheet_y":56,"short_name":"page_with_curl","short_names":["page_with_curl"],"text":null,"texts":null,"category":"Objects","subcategory":"book-paper","sort_order":1271,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PAGE FACING UP","unified":"1F4C4","non_qualified":null,"docomo":"E689","au":"E569","softbank":null,"google":"FE541","image":"1f4c4.png","sheet_x":28,"sheet_y":57,"short_name":"page_facing_up","short_names":["page_facing_up"],"text":null,"texts":null,"category":"Objects","subcategory":"book-paper","sort_order":1273,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CALENDAR","unified":"1F4C5","non_qualified":null,"docomo":null,"au":"E563","softbank":null,"google":"FE542","image":"1f4c5.png","sheet_x":28,"sheet_y":58,"short_name":"date","short_names":["date"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1313,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TEAR-OFF CALENDAR","unified":"1F4C6","non_qualified":null,"docomo":null,"au":"E56A","softbank":null,"google":"FE549","image":"1f4c6.png","sheet_x":28,"sheet_y":59,"short_name":"calendar","short_names":["calendar"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1314,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CARD INDEX","unified":"1F4C7","non_qualified":null,"docomo":"E683","au":"E56C","softbank":null,"google":"FE54D","image":"1f4c7.png","sheet_x":28,"sheet_y":60,"short_name":"card_index","short_names":["card_index"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1317,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CHART WITH UPWARDS TREND","unified":"1F4C8","non_qualified":null,"docomo":null,"au":"E575","softbank":null,"google":"FE54B","image":"1f4c8.png","sheet_x":28,"sheet_y":61,"short_name":"chart_with_upwards_trend","short_names":["chart_with_upwards_trend"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1318,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CHART WITH DOWNWARDS TREND","unified":"1F4C9","non_qualified":null,"docomo":null,"au":"E576","softbank":null,"google":"FE54C","image":"1f4c9.png","sheet_x":29,"sheet_y":0,"short_name":"chart_with_downwards_trend","short_names":["chart_with_downwards_trend"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1319,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BAR CHART","unified":"1F4CA","non_qualified":null,"docomo":null,"au":"E574","softbank":null,"google":"FE54A","image":"1f4ca.png","sheet_x":29,"sheet_y":1,"short_name":"bar_chart","short_names":["bar_chart"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1320,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLIPBOARD","unified":"1F4CB","non_qualified":null,"docomo":"E689","au":"E564","softbank":null,"google":"FE548","image":"1f4cb.png","sheet_x":29,"sheet_y":2,"short_name":"clipboard","short_names":["clipboard"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1321,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PUSHPIN","unified":"1F4CC","non_qualified":null,"docomo":null,"au":"E56D","softbank":null,"google":"FE54E","image":"1f4cc.png","sheet_x":29,"sheet_y":3,"short_name":"pushpin","short_names":["pushpin"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1322,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ROUND PUSHPIN","unified":"1F4CD","non_qualified":null,"docomo":null,"au":"E560","softbank":null,"google":"FE53F","image":"1f4cd.png","sheet_x":29,"sheet_y":4,"short_name":"round_pushpin","short_names":["round_pushpin"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1323,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PAPERCLIP","unified":"1F4CE","non_qualified":null,"docomo":"E730","au":"E4A0","softbank":null,"google":"FE53A","image":"1f4ce.png","sheet_x":29,"sheet_y":5,"short_name":"paperclip","short_names":["paperclip"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1324,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"STRAIGHT RULER","unified":"1F4CF","non_qualified":null,"docomo":null,"au":"E570","softbank":null,"google":"FE550","image":"1f4cf.png","sheet_x":29,"sheet_y":6,"short_name":"straight_ruler","short_names":["straight_ruler"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1326,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TRIANGULAR RULER","unified":"1F4D0","non_qualified":null,"docomo":null,"au":"E4A2","softbank":null,"google":"FE551","image":"1f4d0.png","sheet_x":29,"sheet_y":7,"short_name":"triangular_ruler","short_names":["triangular_ruler"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1327,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BOOKMARK TABS","unified":"1F4D1","non_qualified":null,"docomo":"E689","au":"EB0B","softbank":null,"google":"FE552","image":"1f4d1.png","sheet_x":29,"sheet_y":8,"short_name":"bookmark_tabs","short_names":["bookmark_tabs"],"text":null,"texts":null,"category":"Objects","subcategory":"book-paper","sort_order":1276,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LEDGER","unified":"1F4D2","non_qualified":null,"docomo":"E683","au":"E56E","softbank":null,"google":"FE54F","image":"1f4d2.png","sheet_x":29,"sheet_y":9,"short_name":"ledger","short_names":["ledger"],"text":null,"texts":null,"category":"Objects","subcategory":"book-paper","sort_order":1270,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NOTEBOOK","unified":"1F4D3","non_qualified":null,"docomo":"E683","au":"E56B","softbank":null,"google":"FE545","image":"1f4d3.png","sheet_x":29,"sheet_y":10,"short_name":"notebook","short_names":["notebook"],"text":null,"texts":null,"category":"Objects","subcategory":"book-paper","sort_order":1269,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NOTEBOOK WITH DECORATIVE COVER","unified":"1F4D4","non_qualified":null,"docomo":"E683","au":"E49D","softbank":null,"google":"FE547","image":"1f4d4.png","sheet_x":29,"sheet_y":11,"short_name":"notebook_with_decorative_cover","short_names":["notebook_with_decorative_cover"],"text":null,"texts":null,"category":"Objects","subcategory":"book-paper","sort_order":1262,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOSED BOOK","unified":"1F4D5","non_qualified":null,"docomo":"E683","au":"E568","softbank":null,"google":"FE502","image":"1f4d5.png","sheet_x":29,"sheet_y":12,"short_name":"closed_book","short_names":["closed_book"],"text":null,"texts":null,"category":"Objects","subcategory":"book-paper","sort_order":1263,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"OPEN BOOK","unified":"1F4D6","non_qualified":null,"docomo":"E683","au":"E49F","softbank":"E148","google":"FE546","image":"1f4d6.png","sheet_x":29,"sheet_y":13,"short_name":"book","short_names":["book","open_book"],"text":null,"texts":null,"category":"Objects","subcategory":"book-paper","sort_order":1264,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GREEN BOOK","unified":"1F4D7","non_qualified":null,"docomo":"E683","au":"E565","softbank":null,"google":"FE4FF","image":"1f4d7.png","sheet_x":29,"sheet_y":14,"short_name":"green_book","short_names":["green_book"],"text":null,"texts":null,"category":"Objects","subcategory":"book-paper","sort_order":1265,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLUE BOOK","unified":"1F4D8","non_qualified":null,"docomo":"E683","au":"E566","softbank":null,"google":"FE500","image":"1f4d8.png","sheet_x":29,"sheet_y":15,"short_name":"blue_book","short_names":["blue_book"],"text":null,"texts":null,"category":"Objects","subcategory":"book-paper","sort_order":1266,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ORANGE BOOK","unified":"1F4D9","non_qualified":null,"docomo":"E683","au":"E567","softbank":null,"google":"FE501","image":"1f4d9.png","sheet_x":29,"sheet_y":16,"short_name":"orange_book","short_names":["orange_book"],"text":null,"texts":null,"category":"Objects","subcategory":"book-paper","sort_order":1267,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BOOKS","unified":"1F4DA","non_qualified":null,"docomo":"E683","au":"E56F","softbank":null,"google":"FE503","image":"1f4da.png","sheet_x":29,"sheet_y":17,"short_name":"books","short_names":["books"],"text":null,"texts":null,"category":"Objects","subcategory":"book-paper","sort_order":1268,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NAME BADGE","unified":"1F4DB","non_qualified":null,"docomo":null,"au":"E51D","softbank":null,"google":"FE504","image":"1f4db.png","sheet_x":29,"sheet_y":18,"short_name":"name_badge","short_names":["name_badge"],"text":null,"texts":null,"category":"Symbols","subcategory":"other-symbol","sort_order":1532,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SCROLL","unified":"1F4DC","non_qualified":null,"docomo":"E70A","au":"E55F","softbank":null,"google":"FE4FD","image":"1f4dc.png","sheet_x":29,"sheet_y":19,"short_name":"scroll","short_names":["scroll"],"text":null,"texts":null,"category":"Objects","subcategory":"book-paper","sort_order":1272,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MEMO","unified":"1F4DD","non_qualified":null,"docomo":"E689","au":"EA92","softbank":"E301","google":"FE527","image":"1f4dd.png","sheet_x":29,"sheet_y":20,"short_name":"memo","short_names":["memo","pencil"],"text":null,"texts":null,"category":"Objects","subcategory":"writing","sort_order":1308,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TELEPHONE RECEIVER","unified":"1F4DE","non_qualified":null,"docomo":"E687","au":"E51E","softbank":null,"google":"FE524","image":"1f4de.png","sheet_x":29,"sheet_y":21,"short_name":"telephone_receiver","short_names":["telephone_receiver"],"text":null,"texts":null,"category":"Objects","subcategory":"phone","sort_order":1229,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PAGER","unified":"1F4DF","non_qualified":null,"docomo":"E65A","au":"E59B","softbank":null,"google":"FE522","image":"1f4df.png","sheet_x":29,"sheet_y":22,"short_name":"pager","short_names":["pager"],"text":null,"texts":null,"category":"Objects","subcategory":"phone","sort_order":1230,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FAX MACHINE","unified":"1F4E0","non_qualified":null,"docomo":"E6D0","au":"E520","softbank":"E00B","google":"FE528","image":"1f4e0.png","sheet_x":29,"sheet_y":23,"short_name":"fax","short_names":["fax"],"text":null,"texts":null,"category":"Objects","subcategory":"phone","sort_order":1231,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SATELLITE ANTENNA","unified":"1F4E1","non_qualified":null,"docomo":null,"au":"E4A8","softbank":"E14B","google":"FE531","image":"1f4e1.png","sheet_x":29,"sheet_y":24,"short_name":"satellite_antenna","short_names":["satellite_antenna"],"text":null,"texts":null,"category":"Objects","subcategory":"science","sort_order":1370,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PUBLIC ADDRESS LOUDSPEAKER","unified":"1F4E2","non_qualified":null,"docomo":null,"au":"E511","softbank":"E142","google":"FE52F","image":"1f4e2.png","sheet_x":29,"sheet_y":25,"short_name":"loudspeaker","short_names":["loudspeaker"],"text":null,"texts":null,"category":"Objects","subcategory":"sound","sort_order":1201,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CHEERING MEGAPHONE","unified":"1F4E3","non_qualified":null,"docomo":null,"au":"E511","softbank":"E317","google":"FE530","image":"1f4e3.png","sheet_x":29,"sheet_y":26,"short_name":"mega","short_names":["mega"],"text":null,"texts":null,"category":"Objects","subcategory":"sound","sort_order":1202,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"OUTBOX TRAY","unified":"1F4E4","non_qualified":null,"docomo":null,"au":"E592","softbank":null,"google":"FE533","image":"1f4e4.png","sheet_x":29,"sheet_y":27,"short_name":"outbox_tray","short_names":["outbox_tray"],"text":null,"texts":null,"category":"Objects","subcategory":"mail","sort_order":1293,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"INBOX TRAY","unified":"1F4E5","non_qualified":null,"docomo":null,"au":"E593","softbank":null,"google":"FE534","image":"1f4e5.png","sheet_x":29,"sheet_y":28,"short_name":"inbox_tray","short_names":["inbox_tray"],"text":null,"texts":null,"category":"Objects","subcategory":"mail","sort_order":1294,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PACKAGE","unified":"1F4E6","non_qualified":null,"docomo":"E685","au":"E51F","softbank":null,"google":"FE535","image":"1f4e6.png","sheet_x":29,"sheet_y":29,"short_name":"package","short_names":["package"],"text":null,"texts":null,"category":"Objects","subcategory":"mail","sort_order":1295,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"E-MAIL SYMBOL","unified":"1F4E7","non_qualified":null,"docomo":"E6D3","au":"EB71","softbank":null,"google":"FEB92","image":"1f4e7.png","sheet_x":29,"sheet_y":30,"short_name":"e-mail","short_names":["e-mail"],"text":null,"texts":null,"category":"Objects","subcategory":"mail","sort_order":1290,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"INCOMING ENVELOPE","unified":"1F4E8","non_qualified":null,"docomo":"E6CF","au":"E591","softbank":null,"google":"FE52A","image":"1f4e8.png","sheet_x":29,"sheet_y":31,"short_name":"incoming_envelope","short_names":["incoming_envelope"],"text":null,"texts":null,"category":"Objects","subcategory":"mail","sort_order":1291,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ENVELOPE WITH DOWNWARDS ARROW ABOVE","unified":"1F4E9","non_qualified":null,"docomo":"E6CF","au":"EB62","softbank":"E103","google":"FE52B","image":"1f4e9.png","sheet_x":29,"sheet_y":32,"short_name":"envelope_with_arrow","short_names":["envelope_with_arrow"],"text":null,"texts":null,"category":"Objects","subcategory":"mail","sort_order":1292,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOSED MAILBOX WITH LOWERED FLAG","unified":"1F4EA","non_qualified":null,"docomo":"E665","au":"E51B","softbank":null,"google":"FE52C","image":"1f4ea.png","sheet_x":29,"sheet_y":33,"short_name":"mailbox_closed","short_names":["mailbox_closed"],"text":null,"texts":null,"category":"Objects","subcategory":"mail","sort_order":1297,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOSED MAILBOX WITH RAISED FLAG","unified":"1F4EB","non_qualified":null,"docomo":"E665","au":"EB0A","softbank":"E101","google":"FE52D","image":"1f4eb.png","sheet_x":29,"sheet_y":34,"short_name":"mailbox","short_names":["mailbox"],"text":null,"texts":null,"category":"Objects","subcategory":"mail","sort_order":1296,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"OPEN MAILBOX WITH RAISED FLAG","unified":"1F4EC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f4ec.png","sheet_x":29,"sheet_y":35,"short_name":"mailbox_with_mail","short_names":["mailbox_with_mail"],"text":null,"texts":null,"category":"Objects","subcategory":"mail","sort_order":1298,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"OPEN MAILBOX WITH LOWERED FLAG","unified":"1F4ED","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f4ed.png","sheet_x":29,"sheet_y":36,"short_name":"mailbox_with_no_mail","short_names":["mailbox_with_no_mail"],"text":null,"texts":null,"category":"Objects","subcategory":"mail","sort_order":1299,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"POSTBOX","unified":"1F4EE","non_qualified":null,"docomo":"E665","au":"E51B","softbank":"E102","google":"FE52E","image":"1f4ee.png","sheet_x":29,"sheet_y":37,"short_name":"postbox","short_names":["postbox"],"text":null,"texts":null,"category":"Objects","subcategory":"mail","sort_order":1300,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"POSTAL HORN","unified":"1F4EF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f4ef.png","sheet_x":29,"sheet_y":38,"short_name":"postal_horn","short_names":["postal_horn"],"text":null,"texts":null,"category":"Objects","subcategory":"sound","sort_order":1203,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NEWSPAPER","unified":"1F4F0","non_qualified":null,"docomo":null,"au":"E58B","softbank":null,"google":"FE822","image":"1f4f0.png","sheet_x":29,"sheet_y":39,"short_name":"newspaper","short_names":["newspaper"],"text":null,"texts":null,"category":"Objects","subcategory":"book-paper","sort_order":1274,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MOBILE PHONE","unified":"1F4F1","non_qualified":null,"docomo":"E688","au":"E588","softbank":"E00A","google":"FE525","image":"1f4f1.png","sheet_x":29,"sheet_y":40,"short_name":"iphone","short_names":["iphone"],"text":null,"texts":null,"category":"Objects","subcategory":"phone","sort_order":1226,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MOBILE PHONE WITH RIGHTWARDS ARROW AT LEFT","unified":"1F4F2","non_qualified":null,"docomo":"E6CE","au":"EB08","softbank":"E104","google":"FE526","image":"1f4f2.png","sheet_x":29,"sheet_y":41,"short_name":"calling","short_names":["calling"],"text":null,"texts":null,"category":"Objects","subcategory":"phone","sort_order":1227,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"VIBRATION MODE","unified":"1F4F3","non_qualified":null,"docomo":null,"au":"EA90","softbank":"E250","google":"FE839","image":"1f4f3.png","sheet_x":29,"sheet_y":42,"short_name":"vibration_mode","short_names":["vibration_mode"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1508,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MOBILE PHONE OFF","unified":"1F4F4","non_qualified":null,"docomo":null,"au":"EA91","softbank":"E251","google":"FE83A","image":"1f4f4.png","sheet_x":29,"sheet_y":43,"short_name":"mobile_phone_off","short_names":["mobile_phone_off"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1509,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NO MOBILE PHONES","unified":"1F4F5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f4f5.png","sheet_x":29,"sheet_y":44,"short_name":"no_mobile_phones","short_names":["no_mobile_phones"],"text":null,"texts":null,"category":"Symbols","subcategory":"warning","sort_order":1434,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ANTENNA WITH BARS","unified":"1F4F6","non_qualified":null,"docomo":null,"au":"EA84","softbank":"E20B","google":"FE838","image":"1f4f6.png","sheet_x":29,"sheet_y":45,"short_name":"signal_strength","short_names":["signal_strength"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1506,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CAMERA","unified":"1F4F7","non_qualified":null,"docomo":"E681","au":"E515","softbank":"E008","google":"FE4EF","image":"1f4f7.png","sheet_x":29,"sheet_y":46,"short_name":"camera","short_names":["camera"],"text":null,"texts":null,"category":"Objects","subcategory":"light & video","sort_order":1251,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CAMERA WITH FLASH","unified":"1F4F8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f4f8.png","sheet_x":29,"sheet_y":47,"short_name":"camera_with_flash","short_names":["camera_with_flash"],"text":null,"texts":null,"category":"Objects","subcategory":"light & video","sort_order":1252,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"VIDEO CAMERA","unified":"1F4F9","non_qualified":null,"docomo":"E677","au":"E57E","softbank":null,"google":"FE4F9","image":"1f4f9.png","sheet_x":29,"sheet_y":48,"short_name":"video_camera","short_names":["video_camera"],"text":null,"texts":null,"category":"Objects","subcategory":"light & video","sort_order":1253,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TELEVISION","unified":"1F4FA","non_qualified":null,"docomo":"E68A","au":"E502","softbank":"E12A","google":"FE81C","image":"1f4fa.png","sheet_x":29,"sheet_y":49,"short_name":"tv","short_names":["tv"],"text":null,"texts":null,"category":"Objects","subcategory":"light & video","sort_order":1250,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RADIO","unified":"1F4FB","non_qualified":null,"docomo":null,"au":"E5B9","softbank":"E128","google":"FE81F","image":"1f4fb.png","sheet_x":29,"sheet_y":50,"short_name":"radio","short_names":["radio"],"text":null,"texts":null,"category":"Objects","subcategory":"music","sort_order":1214,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"VIDEOCASSETTE","unified":"1F4FC","non_qualified":null,"docomo":null,"au":"E580","softbank":"E129","google":"FE820","image":"1f4fc.png","sheet_x":29,"sheet_y":51,"short_name":"vhs","short_names":["vhs"],"text":null,"texts":null,"category":"Objects","subcategory":"light & video","sort_order":1254,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FILM PROJECTOR","unified":"1F4FD-FE0F","non_qualified":"1F4FD","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f4fd-fe0f.png","sheet_x":29,"sheet_y":52,"short_name":"film_projector","short_names":["film_projector"],"text":null,"texts":null,"category":"Objects","subcategory":"light & video","sort_order":1248,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PRAYER BEADS","unified":"1F4FF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f4ff.png","sheet_x":29,"sheet_y":53,"short_name":"prayer_beads","short_names":["prayer_beads"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1193,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TWISTED RIGHTWARDS ARROWS","unified":"1F500","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f500.png","sheet_x":29,"sheet_y":54,"short_name":"twisted_rightwards_arrows","short_names":["twisted_rightwards_arrows"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1485,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCKWISE RIGHTWARDS AND LEFTWARDS OPEN CIRCLE ARROWS","unified":"1F501","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f501.png","sheet_x":29,"sheet_y":55,"short_name":"repeat","short_names":["repeat"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1486,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCKWISE RIGHTWARDS AND LEFTWARDS OPEN CIRCLE ARROWS WITH CIRCLED ONE OVERLAY","unified":"1F502","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f502.png","sheet_x":29,"sheet_y":56,"short_name":"repeat_one","short_names":["repeat_one"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1487,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCKWISE DOWNWARDS AND UPWARDS OPEN CIRCLE ARROWS","unified":"1F503","non_qualified":null,"docomo":"E735","au":"EB0D","softbank":null,"google":"FEB91","image":"1f503.png","sheet_x":29,"sheet_y":57,"short_name":"arrows_clockwise","short_names":["arrows_clockwise"],"text":null,"texts":null,"category":"Symbols","subcategory":"arrow","sort_order":1452,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ANTICLOCKWISE DOWNWARDS AND UPWARDS OPEN CIRCLE ARROWS","unified":"1F504","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f504.png","sheet_x":29,"sheet_y":58,"short_name":"arrows_counterclockwise","short_names":["arrows_counterclockwise"],"text":null,"texts":null,"category":"Symbols","subcategory":"arrow","sort_order":1453,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LOW BRIGHTNESS SYMBOL","unified":"1F505","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f505.png","sheet_x":29,"sheet_y":59,"short_name":"low_brightness","short_names":["low_brightness"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1504,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HIGH BRIGHTNESS SYMBOL","unified":"1F506","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f506.png","sheet_x":29,"sheet_y":60,"short_name":"high_brightness","short_names":["high_brightness"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1505,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SPEAKER WITH CANCELLATION STROKE","unified":"1F507","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f507.png","sheet_x":29,"sheet_y":61,"short_name":"mute","short_names":["mute"],"text":null,"texts":null,"category":"Objects","subcategory":"sound","sort_order":1197,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SPEAKER","unified":"1F508","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f508.png","sheet_x":30,"sheet_y":0,"short_name":"speaker","short_names":["speaker"],"text":null,"texts":null,"category":"Objects","subcategory":"sound","sort_order":1198,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SPEAKER WITH ONE SOUND WAVE","unified":"1F509","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f509.png","sheet_x":30,"sheet_y":1,"short_name":"sound","short_names":["sound"],"text":null,"texts":null,"category":"Objects","subcategory":"sound","sort_order":1199,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SPEAKER WITH THREE SOUND WAVES","unified":"1F50A","non_qualified":null,"docomo":null,"au":"E511","softbank":"E141","google":"FE821","image":"1f50a.png","sheet_x":30,"sheet_y":2,"short_name":"loud_sound","short_names":["loud_sound"],"text":null,"texts":null,"category":"Objects","subcategory":"sound","sort_order":1200,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BATTERY","unified":"1F50B","non_qualified":null,"docomo":null,"au":"E584","softbank":null,"google":"FE4FC","image":"1f50b.png","sheet_x":30,"sheet_y":3,"short_name":"battery","short_names":["battery"],"text":null,"texts":null,"category":"Objects","subcategory":"computer","sort_order":1232,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ELECTRIC PLUG","unified":"1F50C","non_qualified":null,"docomo":null,"au":"E589","softbank":null,"google":"FE4FE","image":"1f50c.png","sheet_x":30,"sheet_y":4,"short_name":"electric_plug","short_names":["electric_plug"],"text":null,"texts":null,"category":"Objects","subcategory":"computer","sort_order":1234,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LEFT-POINTING MAGNIFYING GLASS","unified":"1F50D","non_qualified":null,"docomo":"E6DC","au":"E518","softbank":"E114","google":"FEB85","image":"1f50d.png","sheet_x":30,"sheet_y":5,"short_name":"mag","short_names":["mag"],"text":null,"texts":null,"category":"Objects","subcategory":"light & video","sort_order":1255,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RIGHT-POINTING MAGNIFYING GLASS","unified":"1F50E","non_qualified":null,"docomo":"E6DC","au":"EB05","softbank":null,"google":"FEB8D","image":"1f50e.png","sheet_x":30,"sheet_y":6,"short_name":"mag_right","short_names":["mag_right"],"text":null,"texts":null,"category":"Objects","subcategory":"light & video","sort_order":1256,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LOCK WITH INK PEN","unified":"1F50F","non_qualified":null,"docomo":"E6D9","au":"EB0C","softbank":null,"google":"FEB90","image":"1f50f.png","sheet_x":30,"sheet_y":7,"short_name":"lock_with_ink_pen","short_names":["lock_with_ink_pen"],"text":null,"texts":null,"category":"Objects","subcategory":"lock","sort_order":1334,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOSED LOCK WITH KEY","unified":"1F510","non_qualified":null,"docomo":"E6D9","au":"EAFC","softbank":null,"google":"FEB8A","image":"1f510.png","sheet_x":30,"sheet_y":8,"short_name":"closed_lock_with_key","short_names":["closed_lock_with_key"],"text":null,"texts":null,"category":"Objects","subcategory":"lock","sort_order":1335,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"KEY","unified":"1F511","non_qualified":null,"docomo":"E6D9","au":"E519","softbank":"E03F","google":"FEB82","image":"1f511.png","sheet_x":30,"sheet_y":9,"short_name":"key","short_names":["key"],"text":null,"texts":null,"category":"Objects","subcategory":"lock","sort_order":1336,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LOCK","unified":"1F512","non_qualified":null,"docomo":"E6D9","au":"E51C","softbank":"E144","google":"FEB86","image":"1f512.png","sheet_x":30,"sheet_y":10,"short_name":"lock","short_names":["lock"],"text":null,"texts":null,"category":"Objects","subcategory":"lock","sort_order":1332,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"OPEN LOCK","unified":"1F513","non_qualified":null,"docomo":"E6D9","au":"E51C","softbank":"E145","google":"FEB87","image":"1f513.png","sheet_x":30,"sheet_y":11,"short_name":"unlock","short_names":["unlock"],"text":null,"texts":null,"category":"Objects","subcategory":"lock","sort_order":1333,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BELL","unified":"1F514","non_qualified":null,"docomo":"E713","au":"E512","softbank":"E325","google":"FE4F2","image":"1f514.png","sheet_x":30,"sheet_y":12,"short_name":"bell","short_names":["bell"],"text":null,"texts":null,"category":"Objects","subcategory":"sound","sort_order":1204,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BELL WITH CANCELLATION STROKE","unified":"1F515","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f515.png","sheet_x":30,"sheet_y":13,"short_name":"no_bell","short_names":["no_bell"],"text":null,"texts":null,"category":"Objects","subcategory":"sound","sort_order":1205,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BOOKMARK","unified":"1F516","non_qualified":null,"docomo":null,"au":"EB07","softbank":null,"google":"FEB8F","image":"1f516.png","sheet_x":30,"sheet_y":14,"short_name":"bookmark","short_names":["bookmark"],"text":null,"texts":null,"category":"Objects","subcategory":"book-paper","sort_order":1277,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LINK SYMBOL","unified":"1F517","non_qualified":null,"docomo":null,"au":"E58A","softbank":null,"google":"FEB4B","image":"1f517.png","sheet_x":30,"sheet_y":15,"short_name":"link","short_names":["link"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1357,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RADIO BUTTON","unified":"1F518","non_qualified":null,"docomo":null,"au":"EB04","softbank":null,"google":"FEB8C","image":"1f518.png","sheet_x":30,"sheet_y":16,"short_name":"radio_button","short_names":["radio_button"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1632,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BACK WITH LEFTWARDS ARROW ABOVE","unified":"1F519","non_qualified":null,"docomo":null,"au":"EB06","softbank":null,"google":"FEB8E","image":"1f519.png","sheet_x":30,"sheet_y":17,"short_name":"back","short_names":["back"],"text":null,"texts":null,"category":"Symbols","subcategory":"arrow","sort_order":1454,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"END WITH LEFTWARDS ARROW ABOVE","unified":"1F51A","non_qualified":null,"docomo":"E6B9","au":null,"softbank":null,"google":"FE01A","image":"1f51a.png","sheet_x":30,"sheet_y":18,"short_name":"end","short_names":["end"],"text":null,"texts":null,"category":"Symbols","subcategory":"arrow","sort_order":1455,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ON WITH EXCLAMATION MARK WITH LEFT RIGHT ARROW ABOVE","unified":"1F51B","non_qualified":null,"docomo":"E6B8","au":null,"softbank":null,"google":"FE019","image":"1f51b.png","sheet_x":30,"sheet_y":19,"short_name":"on","short_names":["on"],"text":null,"texts":null,"category":"Symbols","subcategory":"arrow","sort_order":1456,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SOON WITH RIGHTWARDS ARROW ABOVE","unified":"1F51C","non_qualified":null,"docomo":"E6B7","au":null,"softbank":null,"google":"FE018","image":"1f51c.png","sheet_x":30,"sheet_y":20,"short_name":"soon","short_names":["soon"],"text":null,"texts":null,"category":"Symbols","subcategory":"arrow","sort_order":1457,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TOP WITH UPWARDS ARROW ABOVE","unified":"1F51D","non_qualified":null,"docomo":null,"au":null,"softbank":"E24C","google":"FEB42","image":"1f51d.png","sheet_x":30,"sheet_y":21,"short_name":"top","short_names":["top"],"text":null,"texts":null,"category":"Symbols","subcategory":"arrow","sort_order":1458,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NO ONE UNDER EIGHTEEN SYMBOL","unified":"1F51E","non_qualified":null,"docomo":null,"au":"EA83","softbank":"E207","google":"FEB25","image":"1f51e.png","sheet_x":30,"sheet_y":22,"short_name":"underage","short_names":["underage"],"text":null,"texts":null,"category":"Symbols","subcategory":"warning","sort_order":1435,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"KEYCAP TEN","unified":"1F51F","non_qualified":null,"docomo":null,"au":"E52B","softbank":null,"google":"FE83B","image":"1f51f.png","sheet_x":30,"sheet_y":23,"short_name":"keycap_ten","short_names":["keycap_ten"],"text":null,"texts":null,"category":"Symbols","subcategory":"keycap","sort_order":1561,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"INPUT SYMBOL FOR LATIN CAPITAL LETTERS","unified":"1F520","non_qualified":null,"docomo":null,"au":"EAFD","softbank":null,"google":"FEB7C","image":"1f520.png","sheet_x":30,"sheet_y":24,"short_name":"capital_abcd","short_names":["capital_abcd"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1562,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"INPUT SYMBOL FOR LATIN SMALL LETTERS","unified":"1F521","non_qualified":null,"docomo":null,"au":"EAFE","softbank":null,"google":"FEB7D","image":"1f521.png","sheet_x":30,"sheet_y":25,"short_name":"abcd","short_names":["abcd"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1563,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"INPUT SYMBOL FOR NUMBERS","unified":"1F522","non_qualified":null,"docomo":null,"au":"EAFF","softbank":null,"google":"FEB7E","image":"1f522.png","sheet_x":30,"sheet_y":26,"short_name":"1234","short_names":["1234"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1564,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"INPUT SYMBOL FOR SYMBOLS","unified":"1F523","non_qualified":null,"docomo":null,"au":"EB00","softbank":null,"google":"FEB7F","image":"1f523.png","sheet_x":30,"sheet_y":27,"short_name":"symbols","short_names":["symbols"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1565,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"INPUT SYMBOL FOR LATIN LETTERS","unified":"1F524","non_qualified":null,"docomo":null,"au":"EB55","softbank":null,"google":"FEB80","image":"1f524.png","sheet_x":30,"sheet_y":28,"short_name":"abc","short_names":["abc"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1566,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FIRE","unified":"1F525","non_qualified":null,"docomo":null,"au":"E47B","softbank":"E11D","google":"FE4F6","image":"1f525.png","sheet_x":30,"sheet_y":29,"short_name":"fire","short_names":["fire"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1062,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ELECTRIC TORCH","unified":"1F526","non_qualified":null,"docomo":"E6FB","au":"E583","softbank":null,"google":"FE4FB","image":"1f526.png","sheet_x":30,"sheet_y":30,"short_name":"flashlight","short_names":["flashlight"],"text":null,"texts":null,"category":"Objects","subcategory":"light & video","sort_order":1259,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WRENCH","unified":"1F527","non_qualified":null,"docomo":"E718","au":"E587","softbank":null,"google":"FE4C9","image":"1f527.png","sheet_x":30,"sheet_y":31,"short_name":"wrench","short_names":["wrench"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1350,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HAMMER","unified":"1F528","non_qualified":null,"docomo":null,"au":"E5CB","softbank":"E116","google":"FE4CA","image":"1f528.png","sheet_x":30,"sheet_y":32,"short_name":"hammer","short_names":["hammer"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1338,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NUT AND BOLT","unified":"1F529","non_qualified":null,"docomo":null,"au":"E581","softbank":null,"google":"FE4CB","image":"1f529.png","sheet_x":30,"sheet_y":33,"short_name":"nut_and_bolt","short_names":["nut_and_bolt"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1352,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HOCHO","unified":"1F52A","non_qualified":null,"docomo":null,"au":"E57F","softbank":null,"google":"FE4FA","image":"1f52a.png","sheet_x":30,"sheet_y":34,"short_name":"hocho","short_names":["hocho","knife"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"dishware","sort_order":844,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PISTOL","unified":"1F52B","non_qualified":null,"docomo":null,"au":"E50A","softbank":"E113","google":"FE4F5","image":"1f52b.png","sheet_x":30,"sheet_y":35,"short_name":"gun","short_names":["gun"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1122,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MICROSCOPE","unified":"1F52C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f52c.png","sheet_x":30,"sheet_y":36,"short_name":"microscope","short_names":["microscope"],"text":null,"texts":null,"category":"Objects","subcategory":"science","sort_order":1368,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TELESCOPE","unified":"1F52D","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f52d.png","sheet_x":30,"sheet_y":37,"short_name":"telescope","short_names":["telescope"],"text":null,"texts":null,"category":"Objects","subcategory":"science","sort_order":1369,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CRYSTAL BALL","unified":"1F52E","non_qualified":null,"docomo":null,"au":"EA8F","softbank":null,"google":"FE4F7","image":"1f52e.png","sheet_x":30,"sheet_y":38,"short_name":"crystal_ball","short_names":["crystal_ball"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1124,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SIX POINTED STAR WITH MIDDLE DOT","unified":"1F52F","non_qualified":null,"docomo":null,"au":"EA8F","softbank":"E23E","google":"FE4F8","image":"1f52f.png","sheet_x":30,"sheet_y":39,"short_name":"six_pointed_star","short_names":["six_pointed_star"],"text":null,"texts":null,"category":"Symbols","subcategory":"religion","sort_order":1470,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"JAPANESE SYMBOL FOR BEGINNER","unified":"1F530","non_qualified":null,"docomo":null,"au":"E480","softbank":"E209","google":"FE044","image":"1f530.png","sheet_x":30,"sheet_y":40,"short_name":"beginner","short_names":["beginner"],"text":null,"texts":null,"category":"Symbols","subcategory":"other-symbol","sort_order":1533,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TRIDENT EMBLEM","unified":"1F531","non_qualified":null,"docomo":"E71A","au":"E5C9","softbank":"E031","google":"FE4D2","image":"1f531.png","sheet_x":30,"sheet_y":41,"short_name":"trident","short_names":["trident"],"text":null,"texts":null,"category":"Symbols","subcategory":"other-symbol","sort_order":1531,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK SQUARE BUTTON","unified":"1F532","non_qualified":null,"docomo":"E69C","au":"E54B","softbank":"E21A","google":"FEB64","image":"1f532.png","sheet_x":30,"sheet_y":42,"short_name":"black_square_button","short_names":["black_square_button"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1634,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WHITE SQUARE BUTTON","unified":"1F533","non_qualified":null,"docomo":"E69C","au":"E54B","softbank":"E21B","google":"FEB67","image":"1f533.png","sheet_x":30,"sheet_y":43,"short_name":"white_square_button","short_names":["white_square_button"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1633,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LARGE RED CIRCLE","unified":"1F534","non_qualified":null,"docomo":"E69C","au":"E54A","softbank":"E219","google":"FEB63","image":"1f534.png","sheet_x":30,"sheet_y":44,"short_name":"red_circle","short_names":["red_circle"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1601,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LARGE BLUE CIRCLE","unified":"1F535","non_qualified":null,"docomo":"E69C","au":"E54B","softbank":null,"google":"FEB64","image":"1f535.png","sheet_x":30,"sheet_y":45,"short_name":"large_blue_circle","short_names":["large_blue_circle"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1605,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LARGE ORANGE DIAMOND","unified":"1F536","non_qualified":null,"docomo":null,"au":"E546","softbank":null,"google":"FEB73","image":"1f536.png","sheet_x":30,"sheet_y":46,"short_name":"large_orange_diamond","short_names":["large_orange_diamond"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1625,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LARGE BLUE DIAMOND","unified":"1F537","non_qualified":null,"docomo":null,"au":"E547","softbank":null,"google":"FEB74","image":"1f537.png","sheet_x":30,"sheet_y":47,"short_name":"large_blue_diamond","short_names":["large_blue_diamond"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1626,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SMALL ORANGE DIAMOND","unified":"1F538","non_qualified":null,"docomo":null,"au":"E536","softbank":null,"google":"FEB75","image":"1f538.png","sheet_x":30,"sheet_y":48,"short_name":"small_orange_diamond","short_names":["small_orange_diamond"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1627,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SMALL BLUE DIAMOND","unified":"1F539","non_qualified":null,"docomo":null,"au":"E537","softbank":null,"google":"FEB76","image":"1f539.png","sheet_x":30,"sheet_y":49,"short_name":"small_blue_diamond","short_names":["small_blue_diamond"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1628,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"UP-POINTING RED TRIANGLE","unified":"1F53A","non_qualified":null,"docomo":null,"au":"E55A","softbank":null,"google":"FEB78","image":"1f53a.png","sheet_x":30,"sheet_y":50,"short_name":"small_red_triangle","short_names":["small_red_triangle"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1629,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DOWN-POINTING RED TRIANGLE","unified":"1F53B","non_qualified":null,"docomo":null,"au":"E55B","softbank":null,"google":"FEB79","image":"1f53b.png","sheet_x":30,"sheet_y":51,"short_name":"small_red_triangle_down","short_names":["small_red_triangle_down"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1630,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"UP-POINTING SMALL RED TRIANGLE","unified":"1F53C","non_qualified":null,"docomo":null,"au":"E543","softbank":null,"google":"FEB01","image":"1f53c.png","sheet_x":30,"sheet_y":52,"short_name":"arrow_up_small","short_names":["arrow_up_small"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1495,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DOWN-POINTING SMALL RED TRIANGLE","unified":"1F53D","non_qualified":null,"docomo":null,"au":"E542","softbank":null,"google":"FEB00","image":"1f53d.png","sheet_x":30,"sheet_y":53,"short_name":"arrow_down_small","short_names":["arrow_down_small"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1497,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"OM","unified":"1F549-FE0F","non_qualified":"1F549","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f549-fe0f.png","sheet_x":30,"sheet_y":54,"short_name":"om_symbol","short_names":["om_symbol"],"text":null,"texts":null,"category":"Symbols","subcategory":"religion","sort_order":1461,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DOVE","unified":"1F54A-FE0F","non_qualified":"1F54A","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f54a-fe0f.png","sheet_x":30,"sheet_y":55,"short_name":"dove_of_peace","short_names":["dove_of_peace"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bird","sort_order":633,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"KAABA","unified":"1F54B","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f54b.png","sheet_x":30,"sheet_y":56,"short_name":"kaaba","short_names":["kaaba"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-religious","sort_order":895,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MOSQUE","unified":"1F54C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f54c.png","sheet_x":30,"sheet_y":57,"short_name":"mosque","short_names":["mosque"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-religious","sort_order":891,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SYNAGOGUE","unified":"1F54D","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f54d.png","sheet_x":30,"sheet_y":58,"short_name":"synagogue","short_names":["synagogue"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-religious","sort_order":893,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MENORAH WITH NINE BRANCHES","unified":"1F54E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f54e.png","sheet_x":30,"sheet_y":59,"short_name":"menorah_with_nine_branches","short_names":["menorah_with_nine_branches"],"text":null,"texts":null,"category":"Symbols","subcategory":"religion","sort_order":1469,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE ONE OCLOCK","unified":"1F550","non_qualified":null,"docomo":"E6BA","au":"E594","softbank":"E024","google":"FE01E","image":"1f550.png","sheet_x":30,"sheet_y":60,"short_name":"clock1","short_names":["clock1"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":996,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE TWO OCLOCK","unified":"1F551","non_qualified":null,"docomo":"E6BA","au":"E594","softbank":"E025","google":"FE01F","image":"1f551.png","sheet_x":30,"sheet_y":61,"short_name":"clock2","short_names":["clock2"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":998,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE THREE OCLOCK","unified":"1F552","non_qualified":null,"docomo":"E6BA","au":"E594","softbank":"E026","google":"FE020","image":"1f552.png","sheet_x":31,"sheet_y":0,"short_name":"clock3","short_names":["clock3"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":1000,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE FOUR OCLOCK","unified":"1F553","non_qualified":null,"docomo":"E6BA","au":"E594","softbank":"E027","google":"FE021","image":"1f553.png","sheet_x":31,"sheet_y":1,"short_name":"clock4","short_names":["clock4"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":1002,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE FIVE OCLOCK","unified":"1F554","non_qualified":null,"docomo":"E6BA","au":"E594","softbank":"E028","google":"FE022","image":"1f554.png","sheet_x":31,"sheet_y":2,"short_name":"clock5","short_names":["clock5"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":1004,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE SIX OCLOCK","unified":"1F555","non_qualified":null,"docomo":"E6BA","au":"E594","softbank":"E029","google":"FE023","image":"1f555.png","sheet_x":31,"sheet_y":3,"short_name":"clock6","short_names":["clock6"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":1006,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE SEVEN OCLOCK","unified":"1F556","non_qualified":null,"docomo":"E6BA","au":"E594","softbank":"E02A","google":"FE024","image":"1f556.png","sheet_x":31,"sheet_y":4,"short_name":"clock7","short_names":["clock7"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":1008,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE EIGHT OCLOCK","unified":"1F557","non_qualified":null,"docomo":"E6BA","au":"E594","softbank":"E02B","google":"FE025","image":"1f557.png","sheet_x":31,"sheet_y":5,"short_name":"clock8","short_names":["clock8"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":1010,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE NINE OCLOCK","unified":"1F558","non_qualified":null,"docomo":"E6BA","au":"E594","softbank":"E02C","google":"FE026","image":"1f558.png","sheet_x":31,"sheet_y":6,"short_name":"clock9","short_names":["clock9"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":1012,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE TEN OCLOCK","unified":"1F559","non_qualified":null,"docomo":"E6BA","au":"E594","softbank":"E02D","google":"FE027","image":"1f559.png","sheet_x":31,"sheet_y":7,"short_name":"clock10","short_names":["clock10"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":1014,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE ELEVEN OCLOCK","unified":"1F55A","non_qualified":null,"docomo":"E6BA","au":"E594","softbank":"E02E","google":"FE028","image":"1f55a.png","sheet_x":31,"sheet_y":8,"short_name":"clock11","short_names":["clock11"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":1016,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE TWELVE OCLOCK","unified":"1F55B","non_qualified":null,"docomo":"E6BA","au":"E594","softbank":"E02F","google":"FE029","image":"1f55b.png","sheet_x":31,"sheet_y":9,"short_name":"clock12","short_names":["clock12"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":994,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE ONE-THIRTY","unified":"1F55C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f55c.png","sheet_x":31,"sheet_y":10,"short_name":"clock130","short_names":["clock130"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":997,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE TWO-THIRTY","unified":"1F55D","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f55d.png","sheet_x":31,"sheet_y":11,"short_name":"clock230","short_names":["clock230"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":999,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE THREE-THIRTY","unified":"1F55E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f55e.png","sheet_x":31,"sheet_y":12,"short_name":"clock330","short_names":["clock330"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":1001,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE FOUR-THIRTY","unified":"1F55F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f55f.png","sheet_x":31,"sheet_y":13,"short_name":"clock430","short_names":["clock430"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":1003,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE FIVE-THIRTY","unified":"1F560","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f560.png","sheet_x":31,"sheet_y":14,"short_name":"clock530","short_names":["clock530"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":1005,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE SIX-THIRTY","unified":"1F561","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f561.png","sheet_x":31,"sheet_y":15,"short_name":"clock630","short_names":["clock630"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":1007,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE SEVEN-THIRTY","unified":"1F562","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f562.png","sheet_x":31,"sheet_y":16,"short_name":"clock730","short_names":["clock730"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":1009,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE EIGHT-THIRTY","unified":"1F563","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f563.png","sheet_x":31,"sheet_y":17,"short_name":"clock830","short_names":["clock830"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":1011,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE NINE-THIRTY","unified":"1F564","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f564.png","sheet_x":31,"sheet_y":18,"short_name":"clock930","short_names":["clock930"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":1013,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE TEN-THIRTY","unified":"1F565","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f565.png","sheet_x":31,"sheet_y":19,"short_name":"clock1030","short_names":["clock1030"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":1015,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE ELEVEN-THIRTY","unified":"1F566","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f566.png","sheet_x":31,"sheet_y":20,"short_name":"clock1130","short_names":["clock1130"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":1017,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOCK FACE TWELVE-THIRTY","unified":"1F567","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f567.png","sheet_x":31,"sheet_y":21,"short_name":"clock1230","short_names":["clock1230"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":995,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CANDLE","unified":"1F56F-FE0F","non_qualified":"1F56F","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f56f-fe0f.png","sheet_x":31,"sheet_y":22,"short_name":"candle","short_names":["candle"],"text":null,"texts":null,"category":"Objects","subcategory":"light & video","sort_order":1257,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MANTELPIECE CLOCK","unified":"1F570-FE0F","non_qualified":"1F570","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f570-fe0f.png","sheet_x":31,"sheet_y":23,"short_name":"mantelpiece_clock","short_names":["mantelpiece_clock"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":993,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HOLE","unified":"1F573-FE0F","non_qualified":"1F573","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f573-fe0f.png","sheet_x":31,"sheet_y":24,"short_name":"hole","short_names":["hole"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":162,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PERSON IN SUIT LEVITATING","unified":"1F574-FE0F","non_qualified":"1F574","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f574-fe0f.png","sheet_x":31,"sheet_y":25,"short_name":"man_in_business_suit_levitating","short_names":["man_in_business_suit_levitating"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":449,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F574-1F3FB","non_qualified":null,"image":"1f574-1f3fb.png","sheet_x":31,"sheet_y":26,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F574-1F3FC","non_qualified":null,"image":"1f574-1f3fc.png","sheet_x":31,"sheet_y":27,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F574-1F3FD","non_qualified":null,"image":"1f574-1f3fd.png","sheet_x":31,"sheet_y":28,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F574-1F3FE","non_qualified":null,"image":"1f574-1f3fe.png","sheet_x":31,"sheet_y":29,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F574-1F3FF","non_qualified":null,"image":"1f574-1f3ff.png","sheet_x":31,"sheet_y":30,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN DETECTIVE","unified":"1F575-FE0F-200D-2640-FE0F","non_qualified":"1F575-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f575-fe0f-200d-2640-fe0f.png","sheet_x":31,"sheet_y":31,"short_name":"female-detective","short_names":["female-detective"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":341,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"1F575-1F3FB-200D-2640-FE0F","non_qualified":"1F575-1F3FB-200D-2640","image":"1f575-1f3fb-200d-2640-fe0f.png","sheet_x":31,"sheet_y":32,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F575-1F3FC-200D-2640-FE0F","non_qualified":"1F575-1F3FC-200D-2640","image":"1f575-1f3fc-200d-2640-fe0f.png","sheet_x":31,"sheet_y":33,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F575-1F3FD-200D-2640-FE0F","non_qualified":"1F575-1F3FD-200D-2640","image":"1f575-1f3fd-200d-2640-fe0f.png","sheet_x":31,"sheet_y":34,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F575-1F3FE-200D-2640-FE0F","non_qualified":"1F575-1F3FE-200D-2640","image":"1f575-1f3fe-200d-2640-fe0f.png","sheet_x":31,"sheet_y":35,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F575-1F3FF-200D-2640-FE0F","non_qualified":"1F575-1F3FF-200D-2640","image":"1f575-1f3ff-200d-2640-fe0f.png","sheet_x":31,"sheet_y":36,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN DETECTIVE","unified":"1F575-FE0F-200D-2642-FE0F","non_qualified":"1F575-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f575-fe0f-200d-2642-fe0f.png","sheet_x":31,"sheet_y":37,"short_name":"male-detective","short_names":["male-detective"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":340,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"1F575-1F3FB-200D-2642-FE0F","non_qualified":"1F575-1F3FB-200D-2642","image":"1f575-1f3fb-200d-2642-fe0f.png","sheet_x":31,"sheet_y":38,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F575-1F3FC-200D-2642-FE0F","non_qualified":"1F575-1F3FC-200D-2642","image":"1f575-1f3fc-200d-2642-fe0f.png","sheet_x":31,"sheet_y":39,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F575-1F3FD-200D-2642-FE0F","non_qualified":"1F575-1F3FD-200D-2642","image":"1f575-1f3fd-200d-2642-fe0f.png","sheet_x":31,"sheet_y":40,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F575-1F3FE-200D-2642-FE0F","non_qualified":"1F575-1F3FE-200D-2642","image":"1f575-1f3fe-200d-2642-fe0f.png","sheet_x":31,"sheet_y":41,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F575-1F3FF-200D-2642-FE0F","non_qualified":"1F575-1F3FF-200D-2642","image":"1f575-1f3ff-200d-2642-fe0f.png","sheet_x":31,"sheet_y":42,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F575-FE0F"},{"name":"DETECTIVE","unified":"1F575-FE0F","non_qualified":"1F575","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f575-fe0f.png","sheet_x":31,"sheet_y":43,"short_name":"sleuth_or_spy","short_names":["sleuth_or_spy"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":339,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F575-1F3FB","non_qualified":null,"image":"1f575-1f3fb.png","sheet_x":31,"sheet_y":44,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F575-1F3FC","non_qualified":null,"image":"1f575-1f3fc.png","sheet_x":31,"sheet_y":45,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F575-1F3FD","non_qualified":null,"image":"1f575-1f3fd.png","sheet_x":31,"sheet_y":46,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F575-1F3FE","non_qualified":null,"image":"1f575-1f3fe.png","sheet_x":31,"sheet_y":47,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F575-1F3FF","non_qualified":null,"image":"1f575-1f3ff.png","sheet_x":31,"sheet_y":48,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F575-FE0F-200D-2642-FE0F"},{"name":"SUNGLASSES","unified":"1F576-FE0F","non_qualified":"1F576","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f576-fe0f.png","sheet_x":31,"sheet_y":49,"short_name":"dark_sunglasses","short_names":["dark_sunglasses"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1151,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SPIDER","unified":"1F577-FE0F","non_qualified":"1F577","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f577-fe0f.png","sheet_x":31,"sheet_y":50,"short_name":"spider","short_names":["spider"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bug","sort_order":677,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SPIDER WEB","unified":"1F578-FE0F","non_qualified":"1F578","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f578-fe0f.png","sheet_x":31,"sheet_y":51,"short_name":"spider_web","short_names":["spider_web"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bug","sort_order":678,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"JOYSTICK","unified":"1F579-FE0F","non_qualified":"1F579","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f579-fe0f.png","sheet_x":31,"sheet_y":52,"short_name":"joystick","short_names":["joystick"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1127,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MAN DANCING","unified":"1F57A","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f57a.png","sheet_x":31,"sheet_y":53,"short_name":"man_dancing","short_names":["man_dancing"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":448,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F57A-1F3FB","non_qualified":null,"image":"1f57a-1f3fb.png","sheet_x":31,"sheet_y":54,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F57A-1F3FC","non_qualified":null,"image":"1f57a-1f3fc.png","sheet_x":31,"sheet_y":55,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F57A-1F3FD","non_qualified":null,"image":"1f57a-1f3fd.png","sheet_x":31,"sheet_y":56,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F57A-1F3FE","non_qualified":null,"image":"1f57a-1f3fe.png","sheet_x":31,"sheet_y":57,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F57A-1F3FF","non_qualified":null,"image":"1f57a-1f3ff.png","sheet_x":31,"sheet_y":58,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"LINKED PAPERCLIPS","unified":"1F587-FE0F","non_qualified":"1F587","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f587-fe0f.png","sheet_x":31,"sheet_y":59,"short_name":"linked_paperclips","short_names":["linked_paperclips"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1325,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PEN","unified":"1F58A-FE0F","non_qualified":"1F58A","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f58a-fe0f.png","sheet_x":31,"sheet_y":60,"short_name":"lower_left_ballpoint_pen","short_names":["lower_left_ballpoint_pen"],"text":null,"texts":null,"category":"Objects","subcategory":"writing","sort_order":1305,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FOUNTAIN PEN","unified":"1F58B-FE0F","non_qualified":"1F58B","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f58b-fe0f.png","sheet_x":31,"sheet_y":61,"short_name":"lower_left_fountain_pen","short_names":["lower_left_fountain_pen"],"text":null,"texts":null,"category":"Objects","subcategory":"writing","sort_order":1304,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PAINTBRUSH","unified":"1F58C-FE0F","non_qualified":"1F58C","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f58c-fe0f.png","sheet_x":32,"sheet_y":0,"short_name":"lower_left_paintbrush","short_names":["lower_left_paintbrush"],"text":null,"texts":null,"category":"Objects","subcategory":"writing","sort_order":1306,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CRAYON","unified":"1F58D-FE0F","non_qualified":"1F58D","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f58d-fe0f.png","sheet_x":32,"sheet_y":1,"short_name":"lower_left_crayon","short_names":["lower_left_crayon"],"text":null,"texts":null,"category":"Objects","subcategory":"writing","sort_order":1307,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HAND WITH FINGERS SPLAYED","unified":"1F590-FE0F","non_qualified":"1F590","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f590-fe0f.png","sheet_x":32,"sheet_y":2,"short_name":"raised_hand_with_fingers_splayed","short_names":["raised_hand_with_fingers_splayed"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-open","sort_order":171,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F590-1F3FB","non_qualified":null,"image":"1f590-1f3fb.png","sheet_x":32,"sheet_y":3,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F590-1F3FC","non_qualified":null,"image":"1f590-1f3fc.png","sheet_x":32,"sheet_y":4,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F590-1F3FD","non_qualified":null,"image":"1f590-1f3fd.png","sheet_x":32,"sheet_y":5,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F590-1F3FE","non_qualified":null,"image":"1f590-1f3fe.png","sheet_x":32,"sheet_y":6,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F590-1F3FF","non_qualified":null,"image":"1f590-1f3ff.png","sheet_x":32,"sheet_y":7,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"REVERSED HAND WITH MIDDLE FINGER EXTENDED","unified":"1F595","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f595.png","sheet_x":32,"sheet_y":8,"short_name":"middle_finger","short_names":["middle_finger","reversed_hand_with_middle_finger_extended"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-single-finger","sort_order":192,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F595-1F3FB","non_qualified":null,"image":"1f595-1f3fb.png","sheet_x":32,"sheet_y":9,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F595-1F3FC","non_qualified":null,"image":"1f595-1f3fc.png","sheet_x":32,"sheet_y":10,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F595-1F3FD","non_qualified":null,"image":"1f595-1f3fd.png","sheet_x":32,"sheet_y":11,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F595-1F3FE","non_qualified":null,"image":"1f595-1f3fe.png","sheet_x":32,"sheet_y":12,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F595-1F3FF","non_qualified":null,"image":"1f595-1f3ff.png","sheet_x":32,"sheet_y":13,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"RAISED HAND WITH PART BETWEEN MIDDLE AND RING FINGERS","unified":"1F596","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f596.png","sheet_x":32,"sheet_y":14,"short_name":"spock-hand","short_names":["spock-hand"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-open","sort_order":173,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F596-1F3FB","non_qualified":null,"image":"1f596-1f3fb.png","sheet_x":32,"sheet_y":15,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F596-1F3FC","non_qualified":null,"image":"1f596-1f3fc.png","sheet_x":32,"sheet_y":16,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F596-1F3FD","non_qualified":null,"image":"1f596-1f3fd.png","sheet_x":32,"sheet_y":17,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F596-1F3FE","non_qualified":null,"image":"1f596-1f3fe.png","sheet_x":32,"sheet_y":18,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F596-1F3FF","non_qualified":null,"image":"1f596-1f3ff.png","sheet_x":32,"sheet_y":19,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"BLACK HEART","unified":"1F5A4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f5a4.png","sheet_x":32,"sheet_y":20,"short_name":"black_heart","short_names":["black_heart"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"heart","sort_order":152,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DESKTOP COMPUTER","unified":"1F5A5-FE0F","non_qualified":"1F5A5","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f5a5-fe0f.png","sheet_x":32,"sheet_y":21,"short_name":"desktop_computer","short_names":["desktop_computer"],"text":null,"texts":null,"category":"Objects","subcategory":"computer","sort_order":1236,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PRINTER","unified":"1F5A8-FE0F","non_qualified":"1F5A8","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f5a8-fe0f.png","sheet_x":32,"sheet_y":22,"short_name":"printer","short_names":["printer"],"text":null,"texts":null,"category":"Objects","subcategory":"computer","sort_order":1237,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"COMPUTER MOUSE","unified":"1F5B1-FE0F","non_qualified":"1F5B1","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f5b1-fe0f.png","sheet_x":32,"sheet_y":23,"short_name":"three_button_mouse","short_names":["three_button_mouse"],"text":null,"texts":null,"category":"Objects","subcategory":"computer","sort_order":1239,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TRACKBALL","unified":"1F5B2-FE0F","non_qualified":"1F5B2","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f5b2-fe0f.png","sheet_x":32,"sheet_y":24,"short_name":"trackball","short_names":["trackball"],"text":null,"texts":null,"category":"Objects","subcategory":"computer","sort_order":1240,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FRAMED PICTURE","unified":"1F5BC-FE0F","non_qualified":"1F5BC","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f5bc-fe0f.png","sheet_x":32,"sheet_y":25,"short_name":"frame_with_picture","short_names":["frame_with_picture"],"text":null,"texts":null,"category":"Activities","subcategory":"arts & crafts","sort_order":1144,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CARD INDEX DIVIDERS","unified":"1F5C2-FE0F","non_qualified":"1F5C2","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f5c2-fe0f.png","sheet_x":32,"sheet_y":26,"short_name":"card_index_dividers","short_names":["card_index_dividers"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1312,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CARD FILE BOX","unified":"1F5C3-FE0F","non_qualified":"1F5C3","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f5c3-fe0f.png","sheet_x":32,"sheet_y":27,"short_name":"card_file_box","short_names":["card_file_box"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1329,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FILE CABINET","unified":"1F5C4-FE0F","non_qualified":"1F5C4","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f5c4-fe0f.png","sheet_x":32,"sheet_y":28,"short_name":"file_cabinet","short_names":["file_cabinet"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1330,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WASTEBASKET","unified":"1F5D1-FE0F","non_qualified":"1F5D1","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f5d1-fe0f.png","sheet_x":32,"sheet_y":29,"short_name":"wastebasket","short_names":["wastebasket"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1331,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SPIRAL NOTEPAD","unified":"1F5D2-FE0F","non_qualified":"1F5D2","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f5d2-fe0f.png","sheet_x":32,"sheet_y":30,"short_name":"spiral_note_pad","short_names":["spiral_note_pad"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1315,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SPIRAL CALENDAR","unified":"1F5D3-FE0F","non_qualified":"1F5D3","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f5d3-fe0f.png","sheet_x":32,"sheet_y":31,"short_name":"spiral_calendar_pad","short_names":["spiral_calendar_pad"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1316,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLAMP","unified":"1F5DC-FE0F","non_qualified":"1F5DC","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f5dc-fe0f.png","sheet_x":32,"sheet_y":32,"short_name":"compression","short_names":["compression"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1354,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"OLD KEY","unified":"1F5DD-FE0F","non_qualified":"1F5DD","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f5dd-fe0f.png","sheet_x":32,"sheet_y":33,"short_name":"old_key","short_names":["old_key"],"text":null,"texts":null,"category":"Objects","subcategory":"lock","sort_order":1337,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ROLLED-UP NEWSPAPER","unified":"1F5DE-FE0F","non_qualified":"1F5DE","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f5de-fe0f.png","sheet_x":32,"sheet_y":34,"short_name":"rolled_up_newspaper","short_names":["rolled_up_newspaper"],"text":null,"texts":null,"category":"Objects","subcategory":"book-paper","sort_order":1275,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DAGGER","unified":"1F5E1-FE0F","non_qualified":"1F5E1","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f5e1-fe0f.png","sheet_x":32,"sheet_y":35,"short_name":"dagger_knife","short_names":["dagger_knife"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1343,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SPEAKING HEAD","unified":"1F5E3-FE0F","non_qualified":"1F5E3","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f5e3-fe0f.png","sheet_x":32,"sheet_y":36,"short_name":"speaking_head_in_silhouette","short_names":["speaking_head_in_silhouette"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-symbol","sort_order":544,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LEFT SPEECH BUBBLE","unified":"1F5E8-FE0F","non_qualified":"1F5E8","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f5e8-fe0f.png","sheet_x":32,"sheet_y":37,"short_name":"left_speech_bubble","short_names":["left_speech_bubble"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":165,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RIGHT ANGER BUBBLE","unified":"1F5EF-FE0F","non_qualified":"1F5EF","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f5ef-fe0f.png","sheet_x":32,"sheet_y":38,"short_name":"right_anger_bubble","short_names":["right_anger_bubble"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"emotion","sort_order":166,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BALLOT BOX WITH BALLOT","unified":"1F5F3-FE0F","non_qualified":"1F5F3","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f5f3-fe0f.png","sheet_x":32,"sheet_y":39,"short_name":"ballot_box_with_ballot","short_names":["ballot_box_with_ballot"],"text":null,"texts":null,"category":"Objects","subcategory":"mail","sort_order":1301,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WORLD MAP","unified":"1F5FA-FE0F","non_qualified":"1F5FA","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f5fa-fe0f.png","sheet_x":32,"sheet_y":40,"short_name":"world_map","short_names":["world_map"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-map","sort_order":851,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MOUNT FUJI","unified":"1F5FB","non_qualified":null,"docomo":"E740","au":"E5BD","softbank":"E03B","google":"FE4C3","image":"1f5fb.png","sheet_x":32,"sheet_y":41,"short_name":"mount_fuji","short_names":["mount_fuji"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-geographic","sort_order":857,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TOKYO TOWER","unified":"1F5FC","non_qualified":null,"docomo":null,"au":"E4C0","softbank":"E509","google":"FE4C4","image":"1f5fc.png","sheet_x":32,"sheet_y":42,"short_name":"tokyo_tower","short_names":["tokyo_tower"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":888,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"STATUE OF LIBERTY","unified":"1F5FD","non_qualified":null,"docomo":null,"au":null,"softbank":"E51D","google":"FE4C6","image":"1f5fd.png","sheet_x":32,"sheet_y":43,"short_name":"statue_of_liberty","short_names":["statue_of_liberty"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":889,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SILHOUETTE OF JAPAN","unified":"1F5FE","non_qualified":null,"docomo":null,"au":"E572","softbank":null,"google":"FE4C7","image":"1f5fe.png","sheet_x":32,"sheet_y":44,"short_name":"japan","short_names":["japan"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-map","sort_order":852,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MOYAI","unified":"1F5FF","non_qualified":null,"docomo":null,"au":"EB6C","softbank":null,"google":"FE4C8","image":"1f5ff.png","sheet_x":32,"sheet_y":45,"short_name":"moyai","short_names":["moyai"],"text":null,"texts":null,"category":"Objects","subcategory":"other-object","sort_order":1409,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GRINNING FACE","unified":"1F600","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f600.png","sheet_x":32,"sheet_y":46,"short_name":"grinning","short_names":["grinning"],"text":":D","texts":null,"category":"Smileys & Emotion","subcategory":"face-smiling","sort_order":1,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GRINNING FACE WITH SMILING EYES","unified":"1F601","non_qualified":null,"docomo":"E753","au":"EB80","softbank":"E404","google":"FE333","image":"1f601.png","sheet_x":32,"sheet_y":47,"short_name":"grin","short_names":["grin"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-smiling","sort_order":4,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE WITH TEARS OF JOY","unified":"1F602","non_qualified":null,"docomo":"E72A","au":"EB64","softbank":"E412","google":"FE334","image":"1f602.png","sheet_x":32,"sheet_y":48,"short_name":"joy","short_names":["joy"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-smiling","sort_order":8,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SMILING FACE WITH OPEN MOUTH","unified":"1F603","non_qualified":null,"docomo":"E6F0","au":"E471","softbank":"E057","google":"FE330","image":"1f603.png","sheet_x":32,"sheet_y":49,"short_name":"smiley","short_names":["smiley"],"text":":)","texts":["=)","=-)"],"category":"Smileys & Emotion","subcategory":"face-smiling","sort_order":2,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SMILING FACE WITH OPEN MOUTH AND SMILING EYES","unified":"1F604","non_qualified":null,"docomo":"E6F0","au":"E471","softbank":"E415","google":"FE338","image":"1f604.png","sheet_x":32,"sheet_y":50,"short_name":"smile","short_names":["smile"],"text":":)","texts":["C:","c:",":D",":-D"],"category":"Smileys & Emotion","subcategory":"face-smiling","sort_order":3,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SMILING FACE WITH OPEN MOUTH AND COLD SWEAT","unified":"1F605","non_qualified":null,"docomo":"E722","au":"E471-E5B1","softbank":null,"google":"FE331","image":"1f605.png","sheet_x":32,"sheet_y":51,"short_name":"sweat_smile","short_names":["sweat_smile"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-smiling","sort_order":6,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SMILING FACE WITH OPEN MOUTH AND TIGHTLY-CLOSED EYES","unified":"1F606","non_qualified":null,"docomo":"E72A","au":"EAC5","softbank":null,"google":"FE332","image":"1f606.png","sheet_x":32,"sheet_y":52,"short_name":"laughing","short_names":["laughing","satisfied"],"text":null,"texts":[":>",":->"],"category":"Smileys & Emotion","subcategory":"face-smiling","sort_order":5,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SMILING FACE WITH HALO","unified":"1F607","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f607.png","sheet_x":32,"sheet_y":53,"short_name":"innocent","short_names":["innocent"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-smiling","sort_order":14,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SMILING FACE WITH HORNS","unified":"1F608","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f608.png","sheet_x":32,"sheet_y":54,"short_name":"smiling_imp","short_names":["smiling_imp"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-negative","sort_order":106,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WINKING FACE","unified":"1F609","non_qualified":null,"docomo":"E729","au":"E5C3","softbank":"E405","google":"FE347","image":"1f609.png","sheet_x":32,"sheet_y":55,"short_name":"wink","short_names":["wink"],"text":";)","texts":[";)",";-)"],"category":"Smileys & Emotion","subcategory":"face-smiling","sort_order":12,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SMILING FACE WITH SMILING EYES","unified":"1F60A","non_qualified":null,"docomo":"E6F0","au":"EACD","softbank":"E056","google":"FE335","image":"1f60a.png","sheet_x":32,"sheet_y":56,"short_name":"blush","short_names":["blush"],"text":":)","texts":null,"category":"Smileys & Emotion","subcategory":"face-smiling","sort_order":13,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE SAVOURING DELICIOUS FOOD","unified":"1F60B","non_qualified":null,"docomo":"E752","au":"EACD","softbank":null,"google":"FE32B","image":"1f60b.png","sheet_x":32,"sheet_y":57,"short_name":"yum","short_names":["yum"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-tongue","sort_order":24,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RELIEVED FACE","unified":"1F60C","non_qualified":null,"docomo":"E721","au":"EAC5","softbank":"E40A","google":"FE33E","image":"1f60c.png","sheet_x":32,"sheet_y":58,"short_name":"relieved","short_names":["relieved"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-sleepy","sort_order":53,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SMILING FACE WITH HEART-SHAPED EYES","unified":"1F60D","non_qualified":null,"docomo":"E726","au":"E5C4","softbank":"E106","google":"FE327","image":"1f60d.png","sheet_x":32,"sheet_y":59,"short_name":"heart_eyes","short_names":["heart_eyes"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-affection","sort_order":16,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SMILING FACE WITH SUNGLASSES","unified":"1F60E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f60e.png","sheet_x":32,"sheet_y":60,"short_name":"sunglasses","short_names":["sunglasses"],"text":null,"texts":["8)"],"category":"Smileys & Emotion","subcategory":"face-glasses","sort_order":73,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SMIRKING FACE","unified":"1F60F","non_qualified":null,"docomo":"E72C","au":"EABF","softbank":"E402","google":"FE343","image":"1f60f.png","sheet_x":32,"sheet_y":61,"short_name":"smirk","short_names":["smirk"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-neutral-skeptical","sort_order":44,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NEUTRAL FACE","unified":"1F610","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f610.png","sheet_x":33,"sheet_y":0,"short_name":"neutral_face","short_names":["neutral_face"],"text":null,"texts":[":|",":-|"],"category":"Smileys & Emotion","subcategory":"face-neutral-skeptical","sort_order":39,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EXPRESSIONLESS FACE","unified":"1F611","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f611.png","sheet_x":33,"sheet_y":1,"short_name":"expressionless","short_names":["expressionless"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-neutral-skeptical","sort_order":40,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"UNAMUSED FACE","unified":"1F612","non_qualified":null,"docomo":"E725","au":"EAC9","softbank":"E40E","google":"FE326","image":"1f612.png","sheet_x":33,"sheet_y":2,"short_name":"unamused","short_names":["unamused"],"text":":(","texts":null,"category":"Smileys & Emotion","subcategory":"face-neutral-skeptical","sort_order":45,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE WITH COLD SWEAT","unified":"1F613","non_qualified":null,"docomo":"E723","au":"E5C6","softbank":"E108","google":"FE344","image":"1f613.png","sheet_x":33,"sheet_y":3,"short_name":"sweat","short_names":["sweat"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":98,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PENSIVE FACE","unified":"1F614","non_qualified":null,"docomo":"E720","au":"EAC0","softbank":"E403","google":"FE340","image":"1f614.png","sheet_x":33,"sheet_y":4,"short_name":"pensive","short_names":["pensive"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-sleepy","sort_order":54,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CONFUSED FACE","unified":"1F615","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f615.png","sheet_x":33,"sheet_y":5,"short_name":"confused","short_names":["confused"],"text":null,"texts":[":\\",":-\\",":\/",":-\/"],"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":76,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CONFOUNDED FACE","unified":"1F616","non_qualified":null,"docomo":"E6F3","au":"EAC3","softbank":"E407","google":"FE33F","image":"1f616.png","sheet_x":33,"sheet_y":6,"short_name":"confounded","short_names":["confounded"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":95,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"KISSING FACE","unified":"1F617","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f617.png","sheet_x":33,"sheet_y":7,"short_name":"kissing","short_names":["kissing"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-affection","sort_order":19,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE THROWING A KISS","unified":"1F618","non_qualified":null,"docomo":"E726","au":"EACF","softbank":"E418","google":"FE32C","image":"1f618.png","sheet_x":33,"sheet_y":8,"short_name":"kissing_heart","short_names":["kissing_heart"],"text":null,"texts":[":*",":-*"],"category":"Smileys & Emotion","subcategory":"face-affection","sort_order":18,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"KISSING FACE WITH SMILING EYES","unified":"1F619","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f619.png","sheet_x":33,"sheet_y":9,"short_name":"kissing_smiling_eyes","short_names":["kissing_smiling_eyes"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-affection","sort_order":22,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"KISSING FACE WITH CLOSED EYES","unified":"1F61A","non_qualified":null,"docomo":"E726","au":"EACE","softbank":"E417","google":"FE32D","image":"1f61a.png","sheet_x":33,"sheet_y":10,"short_name":"kissing_closed_eyes","short_names":["kissing_closed_eyes"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-affection","sort_order":21,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE WITH STUCK-OUT TONGUE","unified":"1F61B","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f61b.png","sheet_x":33,"sheet_y":11,"short_name":"stuck_out_tongue","short_names":["stuck_out_tongue"],"text":":p","texts":[":p",":-p",":P",":-P",":b",":-b"],"category":"Smileys & Emotion","subcategory":"face-tongue","sort_order":25,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE WITH STUCK-OUT TONGUE AND WINKING EYE","unified":"1F61C","non_qualified":null,"docomo":"E728","au":"E4E7","softbank":"E105","google":"FE329","image":"1f61c.png","sheet_x":33,"sheet_y":12,"short_name":"stuck_out_tongue_winking_eye","short_names":["stuck_out_tongue_winking_eye"],"text":";p","texts":[";p",";-p",";b",";-b",";P",";-P"],"category":"Smileys & Emotion","subcategory":"face-tongue","sort_order":26,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE WITH STUCK-OUT TONGUE AND TIGHTLY-CLOSED EYES","unified":"1F61D","non_qualified":null,"docomo":"E728","au":"E4E7","softbank":"E409","google":"FE32A","image":"1f61d.png","sheet_x":33,"sheet_y":13,"short_name":"stuck_out_tongue_closed_eyes","short_names":["stuck_out_tongue_closed_eyes"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-tongue","sort_order":28,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DISAPPOINTED FACE","unified":"1F61E","non_qualified":null,"docomo":"E6F2","au":"EAC0","softbank":"E058","google":"FE323","image":"1f61e.png","sheet_x":33,"sheet_y":14,"short_name":"disappointed","short_names":["disappointed"],"text":":(","texts":["):",":(",":-("],"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":97,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WORRIED FACE","unified":"1F61F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f61f.png","sheet_x":33,"sheet_y":15,"short_name":"worried","short_names":["worried"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":78,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ANGRY FACE","unified":"1F620","non_qualified":null,"docomo":"E6F1","au":"E472","softbank":"E059","google":"FE320","image":"1f620.png","sheet_x":33,"sheet_y":16,"short_name":"angry","short_names":["angry"],"text":null,"texts":[">:(",">:-("],"category":"Smileys & Emotion","subcategory":"face-negative","sort_order":104,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"POUTING FACE","unified":"1F621","non_qualified":null,"docomo":"E724","au":"EB5D","softbank":"E416","google":"FE33D","image":"1f621.png","sheet_x":33,"sheet_y":17,"short_name":"rage","short_names":["rage"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-negative","sort_order":103,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CRYING FACE","unified":"1F622","non_qualified":null,"docomo":"E72E","au":"EB69","softbank":"E413","google":"FE339","image":"1f622.png","sheet_x":33,"sheet_y":18,"short_name":"cry","short_names":["cry"],"text":":'(","texts":[":'("],"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":92,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PERSEVERING FACE","unified":"1F623","non_qualified":null,"docomo":"E72B","au":"EAC2","softbank":"E406","google":"FE33C","image":"1f623.png","sheet_x":33,"sheet_y":19,"short_name":"persevere","short_names":["persevere"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":96,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE WITH LOOK OF TRIUMPH","unified":"1F624","non_qualified":null,"docomo":"E753","au":"EAC1","softbank":null,"google":"FE328","image":"1f624.png","sheet_x":33,"sheet_y":20,"short_name":"triumph","short_names":["triumph"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-negative","sort_order":102,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DISAPPOINTED BUT RELIEVED FACE","unified":"1F625","non_qualified":null,"docomo":"E723","au":"E5C6","softbank":"E401","google":"FE345","image":"1f625.png","sheet_x":33,"sheet_y":21,"short_name":"disappointed_relieved","short_names":["disappointed_relieved"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":91,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FROWNING FACE WITH OPEN MOUTH","unified":"1F626","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f626.png","sheet_x":33,"sheet_y":22,"short_name":"frowning","short_names":["frowning"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":87,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ANGUISHED FACE","unified":"1F627","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f627.png","sheet_x":33,"sheet_y":23,"short_name":"anguished","short_names":["anguished"],"text":null,"texts":["D:"],"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":88,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FEARFUL FACE","unified":"1F628","non_qualified":null,"docomo":"E757","au":"EAC6","softbank":"E40B","google":"FE33B","image":"1f628.png","sheet_x":33,"sheet_y":24,"short_name":"fearful","short_names":["fearful"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":89,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WEARY FACE","unified":"1F629","non_qualified":null,"docomo":"E6F3","au":"EB67","softbank":null,"google":"FE321","image":"1f629.png","sheet_x":33,"sheet_y":25,"short_name":"weary","short_names":["weary"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":99,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SLEEPY FACE","unified":"1F62A","non_qualified":null,"docomo":"E701","au":"EAC4","softbank":"E408","google":"FE342","image":"1f62a.png","sheet_x":33,"sheet_y":26,"short_name":"sleepy","short_names":["sleepy"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-sleepy","sort_order":55,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TIRED FACE","unified":"1F62B","non_qualified":null,"docomo":"E72B","au":"E474","softbank":null,"google":"FE346","image":"1f62b.png","sheet_x":33,"sheet_y":27,"short_name":"tired_face","short_names":["tired_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":100,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GRIMACING FACE","unified":"1F62C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f62c.png","sheet_x":33,"sheet_y":28,"short_name":"grimacing","short_names":["grimacing"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-neutral-skeptical","sort_order":47,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LOUDLY CRYING FACE","unified":"1F62D","non_qualified":null,"docomo":"E72D","au":"E473","softbank":"E411","google":"FE33A","image":"1f62d.png","sheet_x":33,"sheet_y":29,"short_name":"sob","short_names":["sob"],"text":":'(","texts":null,"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":93,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE EXHALING","unified":"1F62E-200D-1F4A8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f62e-200d-1f4a8.png","sheet_x":33,"sheet_y":30,"short_name":"face_exhaling","short_names":["face_exhaling"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-neutral-skeptical","sort_order":48,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE WITH OPEN MOUTH","unified":"1F62E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f62e.png","sheet_x":33,"sheet_y":31,"short_name":"open_mouth","short_names":["open_mouth"],"text":null,"texts":[":o",":-o",":O",":-O"],"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":81,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HUSHED FACE","unified":"1F62F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f62f.png","sheet_x":33,"sheet_y":32,"short_name":"hushed","short_names":["hushed"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":82,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE WITH OPEN MOUTH AND COLD SWEAT","unified":"1F630","non_qualified":null,"docomo":"E723","au":"EACB","softbank":"E40F","google":"FE325","image":"1f630.png","sheet_x":33,"sheet_y":33,"short_name":"cold_sweat","short_names":["cold_sweat"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":90,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE SCREAMING IN FEAR","unified":"1F631","non_qualified":null,"docomo":"E757","au":"E5C5","softbank":"E107","google":"FE341","image":"1f631.png","sheet_x":33,"sheet_y":34,"short_name":"scream","short_names":["scream"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":94,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ASTONISHED FACE","unified":"1F632","non_qualified":null,"docomo":"E6F4","au":"EACA","softbank":"E410","google":"FE322","image":"1f632.png","sheet_x":33,"sheet_y":35,"short_name":"astonished","short_names":["astonished"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":83,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FLUSHED FACE","unified":"1F633","non_qualified":null,"docomo":"E72A","au":"EAC8","softbank":"E40D","google":"FE32F","image":"1f633.png","sheet_x":33,"sheet_y":36,"short_name":"flushed","short_names":["flushed"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":84,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SLEEPING FACE","unified":"1F634","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f634.png","sheet_x":33,"sheet_y":37,"short_name":"sleeping","short_names":["sleeping"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-sleepy","sort_order":57,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE WITH SPIRAL EYES","unified":"1F635-200D-1F4AB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f635-200d-1f4ab.png","sheet_x":33,"sheet_y":38,"short_name":"face_with_spiral_eyes","short_names":["face_with_spiral_eyes"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-unwell","sort_order":68,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DIZZY FACE","unified":"1F635","non_qualified":null,"docomo":"E6F4","au":"E5AE","softbank":null,"google":"FE324","image":"1f635.png","sheet_x":33,"sheet_y":39,"short_name":"dizzy_face","short_names":["dizzy_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-unwell","sort_order":67,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE IN CLOUDS","unified":"1F636-200D-1F32B-FE0F","non_qualified":"1F636-200D-1F32B","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f636-200d-1f32b-fe0f.png","sheet_x":33,"sheet_y":40,"short_name":"face_in_clouds","short_names":["face_in_clouds"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-neutral-skeptical","sort_order":43,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE WITHOUT MOUTH","unified":"1F636","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f636.png","sheet_x":33,"sheet_y":41,"short_name":"no_mouth","short_names":["no_mouth"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-neutral-skeptical","sort_order":41,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE WITH MEDICAL MASK","unified":"1F637","non_qualified":null,"docomo":null,"au":"EAC7","softbank":"E40C","google":"FE32E","image":"1f637.png","sheet_x":33,"sheet_y":42,"short_name":"mask","short_names":["mask"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-unwell","sort_order":58,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GRINNING CAT FACE WITH SMILING EYES","unified":"1F638","non_qualified":null,"docomo":"E753","au":"EB7F","softbank":null,"google":"FE349","image":"1f638.png","sheet_x":33,"sheet_y":43,"short_name":"smile_cat","short_names":["smile_cat"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"cat-face","sort_order":119,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CAT FACE WITH TEARS OF JOY","unified":"1F639","non_qualified":null,"docomo":"E72A","au":"EB63","softbank":null,"google":"FE34A","image":"1f639.png","sheet_x":33,"sheet_y":44,"short_name":"joy_cat","short_names":["joy_cat"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"cat-face","sort_order":120,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SMILING CAT FACE WITH OPEN MOUTH","unified":"1F63A","non_qualified":null,"docomo":"E6F0","au":"EB61","softbank":null,"google":"FE348","image":"1f63a.png","sheet_x":33,"sheet_y":45,"short_name":"smiley_cat","short_names":["smiley_cat"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"cat-face","sort_order":118,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SMILING CAT FACE WITH HEART-SHAPED EYES","unified":"1F63B","non_qualified":null,"docomo":"E726","au":"EB65","softbank":null,"google":"FE34C","image":"1f63b.png","sheet_x":33,"sheet_y":46,"short_name":"heart_eyes_cat","short_names":["heart_eyes_cat"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"cat-face","sort_order":121,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CAT FACE WITH WRY SMILE","unified":"1F63C","non_qualified":null,"docomo":"E753","au":"EB6A","softbank":null,"google":"FE34F","image":"1f63c.png","sheet_x":33,"sheet_y":47,"short_name":"smirk_cat","short_names":["smirk_cat"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"cat-face","sort_order":122,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"KISSING CAT FACE WITH CLOSED EYES","unified":"1F63D","non_qualified":null,"docomo":"E726","au":"EB60","softbank":null,"google":"FE34B","image":"1f63d.png","sheet_x":33,"sheet_y":48,"short_name":"kissing_cat","short_names":["kissing_cat"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"cat-face","sort_order":123,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"POUTING CAT FACE","unified":"1F63E","non_qualified":null,"docomo":"E724","au":"EB5E","softbank":null,"google":"FE34E","image":"1f63e.png","sheet_x":33,"sheet_y":49,"short_name":"pouting_cat","short_names":["pouting_cat"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"cat-face","sort_order":126,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CRYING CAT FACE","unified":"1F63F","non_qualified":null,"docomo":"E72E","au":"EB68","softbank":null,"google":"FE34D","image":"1f63f.png","sheet_x":33,"sheet_y":50,"short_name":"crying_cat_face","short_names":["crying_cat_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"cat-face","sort_order":125,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WEARY CAT FACE","unified":"1F640","non_qualified":null,"docomo":"E6F3","au":"EB66","softbank":null,"google":"FE350","image":"1f640.png","sheet_x":33,"sheet_y":51,"short_name":"scream_cat","short_names":["scream_cat"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"cat-face","sort_order":124,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SLIGHTLY FROWNING FACE","unified":"1F641","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f641.png","sheet_x":33,"sheet_y":52,"short_name":"slightly_frowning_face","short_names":["slightly_frowning_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":79,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HEAD SHAKING HORIZONTALLY","unified":"1F642-200D-2194-FE0F","non_qualified":"1F642-200D-2194","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f642-200d-2194-fe0f.png","sheet_x":33,"sheet_y":53,"short_name":"head_shaking_horizontally","short_names":["head_shaking_horizontally"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-neutral-skeptical","sort_order":51,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},{"name":"HEAD SHAKING VERTICALLY","unified":"1F642-200D-2195-FE0F","non_qualified":"1F642-200D-2195","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f642-200d-2195-fe0f.png","sheet_x":33,"sheet_y":54,"short_name":"head_shaking_vertically","short_names":["head_shaking_vertically"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-neutral-skeptical","sort_order":52,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},{"name":"SLIGHTLY SMILING FACE","unified":"1F642","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f642.png","sheet_x":33,"sheet_y":55,"short_name":"slightly_smiling_face","short_names":["slightly_smiling_face"],"text":null,"texts":[":)","(:",":-)"],"category":"Smileys & Emotion","subcategory":"face-smiling","sort_order":9,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"UPSIDE-DOWN FACE","unified":"1F643","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f643.png","sheet_x":33,"sheet_y":56,"short_name":"upside_down_face","short_names":["upside_down_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-smiling","sort_order":10,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE WITH ROLLING EYES","unified":"1F644","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f644.png","sheet_x":33,"sheet_y":57,"short_name":"face_with_rolling_eyes","short_names":["face_with_rolling_eyes"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-neutral-skeptical","sort_order":46,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WOMAN GESTURING NO","unified":"1F645-200D-2640-FE0F","non_qualified":"1F645-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f645-200d-2640-fe0f.png","sheet_x":33,"sheet_y":58,"short_name":"woman-gesturing-no","short_names":["woman-gesturing-no"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":266,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F645-1F3FB-200D-2640-FE0F","non_qualified":"1F645-1F3FB-200D-2640","image":"1f645-1f3fb-200d-2640-fe0f.png","sheet_x":33,"sheet_y":59,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F645-1F3FC-200D-2640-FE0F","non_qualified":"1F645-1F3FC-200D-2640","image":"1f645-1f3fc-200d-2640-fe0f.png","sheet_x":33,"sheet_y":60,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F645-1F3FD-200D-2640-FE0F","non_qualified":"1F645-1F3FD-200D-2640","image":"1f645-1f3fd-200d-2640-fe0f.png","sheet_x":33,"sheet_y":61,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F645-1F3FE-200D-2640-FE0F","non_qualified":"1F645-1F3FE-200D-2640","image":"1f645-1f3fe-200d-2640-fe0f.png","sheet_x":34,"sheet_y":0,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F645-1F3FF-200D-2640-FE0F","non_qualified":"1F645-1F3FF-200D-2640","image":"1f645-1f3ff-200d-2640-fe0f.png","sheet_x":34,"sheet_y":1,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F645"},{"name":"MAN GESTURING NO","unified":"1F645-200D-2642-FE0F","non_qualified":"1F645-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f645-200d-2642-fe0f.png","sheet_x":34,"sheet_y":2,"short_name":"man-gesturing-no","short_names":["man-gesturing-no"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":265,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F645-1F3FB-200D-2642-FE0F","non_qualified":"1F645-1F3FB-200D-2642","image":"1f645-1f3fb-200d-2642-fe0f.png","sheet_x":34,"sheet_y":3,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F645-1F3FC-200D-2642-FE0F","non_qualified":"1F645-1F3FC-200D-2642","image":"1f645-1f3fc-200d-2642-fe0f.png","sheet_x":34,"sheet_y":4,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F645-1F3FD-200D-2642-FE0F","non_qualified":"1F645-1F3FD-200D-2642","image":"1f645-1f3fd-200d-2642-fe0f.png","sheet_x":34,"sheet_y":5,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F645-1F3FE-200D-2642-FE0F","non_qualified":"1F645-1F3FE-200D-2642","image":"1f645-1f3fe-200d-2642-fe0f.png","sheet_x":34,"sheet_y":6,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F645-1F3FF-200D-2642-FE0F","non_qualified":"1F645-1F3FF-200D-2642","image":"1f645-1f3ff-200d-2642-fe0f.png","sheet_x":34,"sheet_y":7,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"FACE WITH NO GOOD GESTURE","unified":"1F645","non_qualified":null,"docomo":"E72F","au":"EAD7","softbank":"E423","google":"FE351","image":"1f645.png","sheet_x":34,"sheet_y":8,"short_name":"no_good","short_names":["no_good"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":264,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F645-1F3FB","non_qualified":null,"image":"1f645-1f3fb.png","sheet_x":34,"sheet_y":9,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F645-1F3FC","non_qualified":null,"image":"1f645-1f3fc.png","sheet_x":34,"sheet_y":10,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F645-1F3FD","non_qualified":null,"image":"1f645-1f3fd.png","sheet_x":34,"sheet_y":11,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F645-1F3FE","non_qualified":null,"image":"1f645-1f3fe.png","sheet_x":34,"sheet_y":12,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F645-1F3FF","non_qualified":null,"image":"1f645-1f3ff.png","sheet_x":34,"sheet_y":13,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F645-200D-2640-FE0F"},{"name":"WOMAN GESTURING OK","unified":"1F646-200D-2640-FE0F","non_qualified":"1F646-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f646-200d-2640-fe0f.png","sheet_x":34,"sheet_y":14,"short_name":"woman-gesturing-ok","short_names":["woman-gesturing-ok"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":269,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F646-1F3FB-200D-2640-FE0F","non_qualified":"1F646-1F3FB-200D-2640","image":"1f646-1f3fb-200d-2640-fe0f.png","sheet_x":34,"sheet_y":15,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F646-1F3FC-200D-2640-FE0F","non_qualified":"1F646-1F3FC-200D-2640","image":"1f646-1f3fc-200d-2640-fe0f.png","sheet_x":34,"sheet_y":16,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F646-1F3FD-200D-2640-FE0F","non_qualified":"1F646-1F3FD-200D-2640","image":"1f646-1f3fd-200d-2640-fe0f.png","sheet_x":34,"sheet_y":17,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F646-1F3FE-200D-2640-FE0F","non_qualified":"1F646-1F3FE-200D-2640","image":"1f646-1f3fe-200d-2640-fe0f.png","sheet_x":34,"sheet_y":18,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F646-1F3FF-200D-2640-FE0F","non_qualified":"1F646-1F3FF-200D-2640","image":"1f646-1f3ff-200d-2640-fe0f.png","sheet_x":34,"sheet_y":19,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F646"},{"name":"MAN GESTURING OK","unified":"1F646-200D-2642-FE0F","non_qualified":"1F646-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f646-200d-2642-fe0f.png","sheet_x":34,"sheet_y":20,"short_name":"man-gesturing-ok","short_names":["man-gesturing-ok"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":268,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F646-1F3FB-200D-2642-FE0F","non_qualified":"1F646-1F3FB-200D-2642","image":"1f646-1f3fb-200d-2642-fe0f.png","sheet_x":34,"sheet_y":21,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F646-1F3FC-200D-2642-FE0F","non_qualified":"1F646-1F3FC-200D-2642","image":"1f646-1f3fc-200d-2642-fe0f.png","sheet_x":34,"sheet_y":22,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F646-1F3FD-200D-2642-FE0F","non_qualified":"1F646-1F3FD-200D-2642","image":"1f646-1f3fd-200d-2642-fe0f.png","sheet_x":34,"sheet_y":23,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F646-1F3FE-200D-2642-FE0F","non_qualified":"1F646-1F3FE-200D-2642","image":"1f646-1f3fe-200d-2642-fe0f.png","sheet_x":34,"sheet_y":24,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F646-1F3FF-200D-2642-FE0F","non_qualified":"1F646-1F3FF-200D-2642","image":"1f646-1f3ff-200d-2642-fe0f.png","sheet_x":34,"sheet_y":25,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"FACE WITH OK GESTURE","unified":"1F646","non_qualified":null,"docomo":"E70B","au":"EAD8","softbank":"E424","google":"FE352","image":"1f646.png","sheet_x":34,"sheet_y":26,"short_name":"ok_woman","short_names":["ok_woman"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":267,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F646-1F3FB","non_qualified":null,"image":"1f646-1f3fb.png","sheet_x":34,"sheet_y":27,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F646-1F3FC","non_qualified":null,"image":"1f646-1f3fc.png","sheet_x":34,"sheet_y":28,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F646-1F3FD","non_qualified":null,"image":"1f646-1f3fd.png","sheet_x":34,"sheet_y":29,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F646-1F3FE","non_qualified":null,"image":"1f646-1f3fe.png","sheet_x":34,"sheet_y":30,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F646-1F3FF","non_qualified":null,"image":"1f646-1f3ff.png","sheet_x":34,"sheet_y":31,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F646-200D-2640-FE0F"},{"name":"WOMAN BOWING","unified":"1F647-200D-2640-FE0F","non_qualified":"1F647-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f647-200d-2640-fe0f.png","sheet_x":34,"sheet_y":32,"short_name":"woman-bowing","short_names":["woman-bowing"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":281,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F647-1F3FB-200D-2640-FE0F","non_qualified":"1F647-1F3FB-200D-2640","image":"1f647-1f3fb-200d-2640-fe0f.png","sheet_x":34,"sheet_y":33,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F647-1F3FC-200D-2640-FE0F","non_qualified":"1F647-1F3FC-200D-2640","image":"1f647-1f3fc-200d-2640-fe0f.png","sheet_x":34,"sheet_y":34,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F647-1F3FD-200D-2640-FE0F","non_qualified":"1F647-1F3FD-200D-2640","image":"1f647-1f3fd-200d-2640-fe0f.png","sheet_x":34,"sheet_y":35,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F647-1F3FE-200D-2640-FE0F","non_qualified":"1F647-1F3FE-200D-2640","image":"1f647-1f3fe-200d-2640-fe0f.png","sheet_x":34,"sheet_y":36,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F647-1F3FF-200D-2640-FE0F","non_qualified":"1F647-1F3FF-200D-2640","image":"1f647-1f3ff-200d-2640-fe0f.png","sheet_x":34,"sheet_y":37,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN BOWING","unified":"1F647-200D-2642-FE0F","non_qualified":"1F647-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f647-200d-2642-fe0f.png","sheet_x":34,"sheet_y":38,"short_name":"man-bowing","short_names":["man-bowing"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":280,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F647-1F3FB-200D-2642-FE0F","non_qualified":"1F647-1F3FB-200D-2642","image":"1f647-1f3fb-200d-2642-fe0f.png","sheet_x":34,"sheet_y":39,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F647-1F3FC-200D-2642-FE0F","non_qualified":"1F647-1F3FC-200D-2642","image":"1f647-1f3fc-200d-2642-fe0f.png","sheet_x":34,"sheet_y":40,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F647-1F3FD-200D-2642-FE0F","non_qualified":"1F647-1F3FD-200D-2642","image":"1f647-1f3fd-200d-2642-fe0f.png","sheet_x":34,"sheet_y":41,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F647-1F3FE-200D-2642-FE0F","non_qualified":"1F647-1F3FE-200D-2642","image":"1f647-1f3fe-200d-2642-fe0f.png","sheet_x":34,"sheet_y":42,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F647-1F3FF-200D-2642-FE0F","non_qualified":"1F647-1F3FF-200D-2642","image":"1f647-1f3ff-200d-2642-fe0f.png","sheet_x":34,"sheet_y":43,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"PERSON BOWING DEEPLY","unified":"1F647","non_qualified":null,"docomo":null,"au":"EAD9","softbank":"E426","google":"FE353","image":"1f647.png","sheet_x":34,"sheet_y":44,"short_name":"bow","short_names":["bow"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":279,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F647-1F3FB","non_qualified":null,"image":"1f647-1f3fb.png","sheet_x":34,"sheet_y":45,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F647-1F3FC","non_qualified":null,"image":"1f647-1f3fc.png","sheet_x":34,"sheet_y":46,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F647-1F3FD","non_qualified":null,"image":"1f647-1f3fd.png","sheet_x":34,"sheet_y":47,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F647-1F3FE","non_qualified":null,"image":"1f647-1f3fe.png","sheet_x":34,"sheet_y":48,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F647-1F3FF","non_qualified":null,"image":"1f647-1f3ff.png","sheet_x":34,"sheet_y":49,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"SEE-NO-EVIL MONKEY","unified":"1F648","non_qualified":null,"docomo":null,"au":"EB50","softbank":null,"google":"FE354","image":"1f648.png","sheet_x":34,"sheet_y":50,"short_name":"see_no_evil","short_names":["see_no_evil"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"monkey-face","sort_order":127,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HEAR-NO-EVIL MONKEY","unified":"1F649","non_qualified":null,"docomo":null,"au":"EB52","softbank":null,"google":"FE356","image":"1f649.png","sheet_x":34,"sheet_y":51,"short_name":"hear_no_evil","short_names":["hear_no_evil"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"monkey-face","sort_order":128,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SPEAK-NO-EVIL MONKEY","unified":"1F64A","non_qualified":null,"docomo":null,"au":"EB51","softbank":null,"google":"FE355","image":"1f64a.png","sheet_x":34,"sheet_y":52,"short_name":"speak_no_evil","short_names":["speak_no_evil"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"monkey-face","sort_order":129,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WOMAN RAISING HAND","unified":"1F64B-200D-2640-FE0F","non_qualified":"1F64B-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f64b-200d-2640-fe0f.png","sheet_x":34,"sheet_y":53,"short_name":"woman-raising-hand","short_names":["woman-raising-hand"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":275,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F64B-1F3FB-200D-2640-FE0F","non_qualified":"1F64B-1F3FB-200D-2640","image":"1f64b-1f3fb-200d-2640-fe0f.png","sheet_x":34,"sheet_y":54,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F64B-1F3FC-200D-2640-FE0F","non_qualified":"1F64B-1F3FC-200D-2640","image":"1f64b-1f3fc-200d-2640-fe0f.png","sheet_x":34,"sheet_y":55,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F64B-1F3FD-200D-2640-FE0F","non_qualified":"1F64B-1F3FD-200D-2640","image":"1f64b-1f3fd-200d-2640-fe0f.png","sheet_x":34,"sheet_y":56,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F64B-1F3FE-200D-2640-FE0F","non_qualified":"1F64B-1F3FE-200D-2640","image":"1f64b-1f3fe-200d-2640-fe0f.png","sheet_x":34,"sheet_y":57,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F64B-1F3FF-200D-2640-FE0F","non_qualified":"1F64B-1F3FF-200D-2640","image":"1f64b-1f3ff-200d-2640-fe0f.png","sheet_x":34,"sheet_y":58,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F64B"},{"name":"MAN RAISING HAND","unified":"1F64B-200D-2642-FE0F","non_qualified":"1F64B-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f64b-200d-2642-fe0f.png","sheet_x":34,"sheet_y":59,"short_name":"man-raising-hand","short_names":["man-raising-hand"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":274,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F64B-1F3FB-200D-2642-FE0F","non_qualified":"1F64B-1F3FB-200D-2642","image":"1f64b-1f3fb-200d-2642-fe0f.png","sheet_x":34,"sheet_y":60,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F64B-1F3FC-200D-2642-FE0F","non_qualified":"1F64B-1F3FC-200D-2642","image":"1f64b-1f3fc-200d-2642-fe0f.png","sheet_x":34,"sheet_y":61,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F64B-1F3FD-200D-2642-FE0F","non_qualified":"1F64B-1F3FD-200D-2642","image":"1f64b-1f3fd-200d-2642-fe0f.png","sheet_x":35,"sheet_y":0,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F64B-1F3FE-200D-2642-FE0F","non_qualified":"1F64B-1F3FE-200D-2642","image":"1f64b-1f3fe-200d-2642-fe0f.png","sheet_x":35,"sheet_y":1,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F64B-1F3FF-200D-2642-FE0F","non_qualified":"1F64B-1F3FF-200D-2642","image":"1f64b-1f3ff-200d-2642-fe0f.png","sheet_x":35,"sheet_y":2,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"HAPPY PERSON RAISING ONE HAND","unified":"1F64B","non_qualified":null,"docomo":null,"au":"EB85","softbank":null,"google":"FE357","image":"1f64b.png","sheet_x":35,"sheet_y":3,"short_name":"raising_hand","short_names":["raising_hand"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":273,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F64B-1F3FB","non_qualified":null,"image":"1f64b-1f3fb.png","sheet_x":35,"sheet_y":4,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F64B-1F3FC","non_qualified":null,"image":"1f64b-1f3fc.png","sheet_x":35,"sheet_y":5,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F64B-1F3FD","non_qualified":null,"image":"1f64b-1f3fd.png","sheet_x":35,"sheet_y":6,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F64B-1F3FE","non_qualified":null,"image":"1f64b-1f3fe.png","sheet_x":35,"sheet_y":7,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F64B-1F3FF","non_qualified":null,"image":"1f64b-1f3ff.png","sheet_x":35,"sheet_y":8,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F64B-200D-2640-FE0F"},{"name":"PERSON RAISING BOTH HANDS IN CELEBRATION","unified":"1F64C","non_qualified":null,"docomo":null,"au":"EB86","softbank":"E427","google":"FE358","image":"1f64c.png","sheet_x":35,"sheet_y":9,"short_name":"raised_hands","short_names":["raised_hands"],"text":null,"texts":null,"category":"People & Body","subcategory":"hands","sort_order":203,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F64C-1F3FB","non_qualified":null,"image":"1f64c-1f3fb.png","sheet_x":35,"sheet_y":10,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F64C-1F3FC","non_qualified":null,"image":"1f64c-1f3fc.png","sheet_x":35,"sheet_y":11,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F64C-1F3FD","non_qualified":null,"image":"1f64c-1f3fd.png","sheet_x":35,"sheet_y":12,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F64C-1F3FE","non_qualified":null,"image":"1f64c-1f3fe.png","sheet_x":35,"sheet_y":13,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F64C-1F3FF","non_qualified":null,"image":"1f64c-1f3ff.png","sheet_x":35,"sheet_y":14,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN FROWNING","unified":"1F64D-200D-2640-FE0F","non_qualified":"1F64D-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f64d-200d-2640-fe0f.png","sheet_x":35,"sheet_y":15,"short_name":"woman-frowning","short_names":["woman-frowning"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":260,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F64D-1F3FB-200D-2640-FE0F","non_qualified":"1F64D-1F3FB-200D-2640","image":"1f64d-1f3fb-200d-2640-fe0f.png","sheet_x":35,"sheet_y":16,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F64D-1F3FC-200D-2640-FE0F","non_qualified":"1F64D-1F3FC-200D-2640","image":"1f64d-1f3fc-200d-2640-fe0f.png","sheet_x":35,"sheet_y":17,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F64D-1F3FD-200D-2640-FE0F","non_qualified":"1F64D-1F3FD-200D-2640","image":"1f64d-1f3fd-200d-2640-fe0f.png","sheet_x":35,"sheet_y":18,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F64D-1F3FE-200D-2640-FE0F","non_qualified":"1F64D-1F3FE-200D-2640","image":"1f64d-1f3fe-200d-2640-fe0f.png","sheet_x":35,"sheet_y":19,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F64D-1F3FF-200D-2640-FE0F","non_qualified":"1F64D-1F3FF-200D-2640","image":"1f64d-1f3ff-200d-2640-fe0f.png","sheet_x":35,"sheet_y":20,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F64D"},{"name":"MAN FROWNING","unified":"1F64D-200D-2642-FE0F","non_qualified":"1F64D-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f64d-200d-2642-fe0f.png","sheet_x":35,"sheet_y":21,"short_name":"man-frowning","short_names":["man-frowning"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":259,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F64D-1F3FB-200D-2642-FE0F","non_qualified":"1F64D-1F3FB-200D-2642","image":"1f64d-1f3fb-200d-2642-fe0f.png","sheet_x":35,"sheet_y":22,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F64D-1F3FC-200D-2642-FE0F","non_qualified":"1F64D-1F3FC-200D-2642","image":"1f64d-1f3fc-200d-2642-fe0f.png","sheet_x":35,"sheet_y":23,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F64D-1F3FD-200D-2642-FE0F","non_qualified":"1F64D-1F3FD-200D-2642","image":"1f64d-1f3fd-200d-2642-fe0f.png","sheet_x":35,"sheet_y":24,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F64D-1F3FE-200D-2642-FE0F","non_qualified":"1F64D-1F3FE-200D-2642","image":"1f64d-1f3fe-200d-2642-fe0f.png","sheet_x":35,"sheet_y":25,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F64D-1F3FF-200D-2642-FE0F","non_qualified":"1F64D-1F3FF-200D-2642","image":"1f64d-1f3ff-200d-2642-fe0f.png","sheet_x":35,"sheet_y":26,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"PERSON FROWNING","unified":"1F64D","non_qualified":null,"docomo":"E6F3","au":"EB87","softbank":null,"google":"FE359","image":"1f64d.png","sheet_x":35,"sheet_y":27,"short_name":"person_frowning","short_names":["person_frowning"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":258,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F64D-1F3FB","non_qualified":null,"image":"1f64d-1f3fb.png","sheet_x":35,"sheet_y":28,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F64D-1F3FC","non_qualified":null,"image":"1f64d-1f3fc.png","sheet_x":35,"sheet_y":29,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F64D-1F3FD","non_qualified":null,"image":"1f64d-1f3fd.png","sheet_x":35,"sheet_y":30,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F64D-1F3FE","non_qualified":null,"image":"1f64d-1f3fe.png","sheet_x":35,"sheet_y":31,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F64D-1F3FF","non_qualified":null,"image":"1f64d-1f3ff.png","sheet_x":35,"sheet_y":32,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F64D-200D-2640-FE0F"},{"name":"WOMAN POUTING","unified":"1F64E-200D-2640-FE0F","non_qualified":"1F64E-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f64e-200d-2640-fe0f.png","sheet_x":35,"sheet_y":33,"short_name":"woman-pouting","short_names":["woman-pouting"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":263,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F64E-1F3FB-200D-2640-FE0F","non_qualified":"1F64E-1F3FB-200D-2640","image":"1f64e-1f3fb-200d-2640-fe0f.png","sheet_x":35,"sheet_y":34,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F64E-1F3FC-200D-2640-FE0F","non_qualified":"1F64E-1F3FC-200D-2640","image":"1f64e-1f3fc-200d-2640-fe0f.png","sheet_x":35,"sheet_y":35,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F64E-1F3FD-200D-2640-FE0F","non_qualified":"1F64E-1F3FD-200D-2640","image":"1f64e-1f3fd-200d-2640-fe0f.png","sheet_x":35,"sheet_y":36,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F64E-1F3FE-200D-2640-FE0F","non_qualified":"1F64E-1F3FE-200D-2640","image":"1f64e-1f3fe-200d-2640-fe0f.png","sheet_x":35,"sheet_y":37,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F64E-1F3FF-200D-2640-FE0F","non_qualified":"1F64E-1F3FF-200D-2640","image":"1f64e-1f3ff-200d-2640-fe0f.png","sheet_x":35,"sheet_y":38,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F64E"},{"name":"MAN POUTING","unified":"1F64E-200D-2642-FE0F","non_qualified":"1F64E-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f64e-200d-2642-fe0f.png","sheet_x":35,"sheet_y":39,"short_name":"man-pouting","short_names":["man-pouting"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":262,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F64E-1F3FB-200D-2642-FE0F","non_qualified":"1F64E-1F3FB-200D-2642","image":"1f64e-1f3fb-200d-2642-fe0f.png","sheet_x":35,"sheet_y":40,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F64E-1F3FC-200D-2642-FE0F","non_qualified":"1F64E-1F3FC-200D-2642","image":"1f64e-1f3fc-200d-2642-fe0f.png","sheet_x":35,"sheet_y":41,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F64E-1F3FD-200D-2642-FE0F","non_qualified":"1F64E-1F3FD-200D-2642","image":"1f64e-1f3fd-200d-2642-fe0f.png","sheet_x":35,"sheet_y":42,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F64E-1F3FE-200D-2642-FE0F","non_qualified":"1F64E-1F3FE-200D-2642","image":"1f64e-1f3fe-200d-2642-fe0f.png","sheet_x":35,"sheet_y":43,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F64E-1F3FF-200D-2642-FE0F","non_qualified":"1F64E-1F3FF-200D-2642","image":"1f64e-1f3ff-200d-2642-fe0f.png","sheet_x":35,"sheet_y":44,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"PERSON WITH POUTING FACE","unified":"1F64E","non_qualified":null,"docomo":"E6F1","au":"EB88","softbank":null,"google":"FE35A","image":"1f64e.png","sheet_x":35,"sheet_y":45,"short_name":"person_with_pouting_face","short_names":["person_with_pouting_face"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":261,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F64E-1F3FB","non_qualified":null,"image":"1f64e-1f3fb.png","sheet_x":35,"sheet_y":46,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F64E-1F3FC","non_qualified":null,"image":"1f64e-1f3fc.png","sheet_x":35,"sheet_y":47,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F64E-1F3FD","non_qualified":null,"image":"1f64e-1f3fd.png","sheet_x":35,"sheet_y":48,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F64E-1F3FE","non_qualified":null,"image":"1f64e-1f3fe.png","sheet_x":35,"sheet_y":49,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F64E-1F3FF","non_qualified":null,"image":"1f64e-1f3ff.png","sheet_x":35,"sheet_y":50,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F64E-200D-2640-FE0F"},{"name":"PERSON WITH FOLDED HANDS","unified":"1F64F","non_qualified":null,"docomo":null,"au":"EAD2","softbank":"E41D","google":"FE35B","image":"1f64f.png","sheet_x":35,"sheet_y":51,"short_name":"pray","short_names":["pray"],"text":null,"texts":null,"category":"People & Body","subcategory":"hands","sort_order":208,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F64F-1F3FB","non_qualified":null,"image":"1f64f-1f3fb.png","sheet_x":35,"sheet_y":52,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F64F-1F3FC","non_qualified":null,"image":"1f64f-1f3fc.png","sheet_x":35,"sheet_y":53,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F64F-1F3FD","non_qualified":null,"image":"1f64f-1f3fd.png","sheet_x":35,"sheet_y":54,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F64F-1F3FE","non_qualified":null,"image":"1f64f-1f3fe.png","sheet_x":35,"sheet_y":55,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F64F-1F3FF","non_qualified":null,"image":"1f64f-1f3ff.png","sheet_x":35,"sheet_y":56,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"ROCKET","unified":"1F680","non_qualified":null,"docomo":null,"au":"E5C8","softbank":"E10D","google":"FE7ED","image":"1f680.png","sheet_x":35,"sheet_y":57,"short_name":"rocket","short_names":["rocket"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-air","sort_order":983,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HELICOPTER","unified":"1F681","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f681.png","sheet_x":35,"sheet_y":58,"short_name":"helicopter","short_names":["helicopter"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-air","sort_order":978,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"STEAM LOCOMOTIVE","unified":"1F682","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f682.png","sheet_x":35,"sheet_y":59,"short_name":"steam_locomotive","short_names":["steam_locomotive"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":913,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RAILWAY CAR","unified":"1F683","non_qualified":null,"docomo":"E65B","au":"E4B5","softbank":"E01E","google":"FE7DF","image":"1f683.png","sheet_x":35,"sheet_y":60,"short_name":"railway_car","short_names":["railway_car"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":914,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HIGH-SPEED TRAIN","unified":"1F684","non_qualified":null,"docomo":"E65D","au":"E4B0","softbank":"E435","google":"FE7E2","image":"1f684.png","sheet_x":35,"sheet_y":61,"short_name":"bullettrain_side","short_names":["bullettrain_side"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":915,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HIGH-SPEED TRAIN WITH BULLET NOSE","unified":"1F685","non_qualified":null,"docomo":"E65D","au":"E4B0","softbank":"E01F","google":"FE7E3","image":"1f685.png","sheet_x":36,"sheet_y":0,"short_name":"bullettrain_front","short_names":["bullettrain_front"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":916,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TRAIN","unified":"1F686","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f686.png","sheet_x":36,"sheet_y":1,"short_name":"train2","short_names":["train2"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":917,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"METRO","unified":"1F687","non_qualified":null,"docomo":"E65C","au":"E5BC","softbank":"E434","google":"FE7E0","image":"1f687.png","sheet_x":36,"sheet_y":2,"short_name":"metro","short_names":["metro"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":918,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LIGHT RAIL","unified":"1F688","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f688.png","sheet_x":36,"sheet_y":3,"short_name":"light_rail","short_names":["light_rail"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":919,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"STATION","unified":"1F689","non_qualified":null,"docomo":null,"au":"EB6D","softbank":"E039","google":"FE7EC","image":"1f689.png","sheet_x":36,"sheet_y":4,"short_name":"station","short_names":["station"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":920,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TRAM","unified":"1F68A","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f68a.png","sheet_x":36,"sheet_y":5,"short_name":"tram","short_names":["tram"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":921,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TRAM CAR","unified":"1F68B","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f68b.png","sheet_x":36,"sheet_y":6,"short_name":"train","short_names":["train"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":924,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BUS","unified":"1F68C","non_qualified":null,"docomo":"E660","au":"E4AF","softbank":"E159","google":"FE7E6","image":"1f68c.png","sheet_x":36,"sheet_y":7,"short_name":"bus","short_names":["bus"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":925,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ONCOMING BUS","unified":"1F68D","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f68d.png","sheet_x":36,"sheet_y":8,"short_name":"oncoming_bus","short_names":["oncoming_bus"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":926,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TROLLEYBUS","unified":"1F68E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f68e.png","sheet_x":36,"sheet_y":9,"short_name":"trolleybus","short_names":["trolleybus"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":927,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BUS STOP","unified":"1F68F","non_qualified":null,"docomo":null,"au":"E4A7","softbank":"E150","google":"FE7E7","image":"1f68f.png","sheet_x":36,"sheet_y":10,"short_name":"busstop","short_names":["busstop"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":952,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MINIBUS","unified":"1F690","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f690.png","sheet_x":36,"sheet_y":11,"short_name":"minibus","short_names":["minibus"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":928,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"AMBULANCE","unified":"1F691","non_qualified":null,"docomo":null,"au":"EAE0","softbank":"E431","google":"FE7F3","image":"1f691.png","sheet_x":36,"sheet_y":12,"short_name":"ambulance","short_names":["ambulance"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":929,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FIRE ENGINE","unified":"1F692","non_qualified":null,"docomo":null,"au":"EADF","softbank":"E430","google":"FE7F2","image":"1f692.png","sheet_x":36,"sheet_y":13,"short_name":"fire_engine","short_names":["fire_engine"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":930,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"POLICE CAR","unified":"1F693","non_qualified":null,"docomo":null,"au":"EAE1","softbank":"E432","google":"FE7F4","image":"1f693.png","sheet_x":36,"sheet_y":14,"short_name":"police_car","short_names":["police_car"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":931,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ONCOMING POLICE CAR","unified":"1F694","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f694.png","sheet_x":36,"sheet_y":15,"short_name":"oncoming_police_car","short_names":["oncoming_police_car"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":932,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TAXI","unified":"1F695","non_qualified":null,"docomo":"E65E","au":"E4B1","softbank":"E15A","google":"FE7EF","image":"1f695.png","sheet_x":36,"sheet_y":16,"short_name":"taxi","short_names":["taxi"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":933,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ONCOMING TAXI","unified":"1F696","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f696.png","sheet_x":36,"sheet_y":17,"short_name":"oncoming_taxi","short_names":["oncoming_taxi"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":934,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"AUTOMOBILE","unified":"1F697","non_qualified":null,"docomo":"E65E","au":"E4B1","softbank":"E01B","google":"FE7E4","image":"1f697.png","sheet_x":36,"sheet_y":18,"short_name":"car","short_names":["car","red_car"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":935,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ONCOMING AUTOMOBILE","unified":"1F698","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f698.png","sheet_x":36,"sheet_y":19,"short_name":"oncoming_automobile","short_names":["oncoming_automobile"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":936,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RECREATIONAL VEHICLE","unified":"1F699","non_qualified":null,"docomo":"E65F","au":"E4B1","softbank":"E42E","google":"FE7E5","image":"1f699.png","sheet_x":36,"sheet_y":20,"short_name":"blue_car","short_names":["blue_car"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":937,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DELIVERY TRUCK","unified":"1F69A","non_qualified":null,"docomo":null,"au":"E4B2","softbank":"E42F","google":"FE7F1","image":"1f69a.png","sheet_x":36,"sheet_y":21,"short_name":"truck","short_names":["truck"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":939,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ARTICULATED LORRY","unified":"1F69B","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f69b.png","sheet_x":36,"sheet_y":22,"short_name":"articulated_lorry","short_names":["articulated_lorry"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":940,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TRACTOR","unified":"1F69C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f69c.png","sheet_x":36,"sheet_y":23,"short_name":"tractor","short_names":["tractor"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":941,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MONORAIL","unified":"1F69D","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f69d.png","sheet_x":36,"sheet_y":24,"short_name":"monorail","short_names":["monorail"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":922,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MOUNTAIN RAILWAY","unified":"1F69E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f69e.png","sheet_x":36,"sheet_y":25,"short_name":"mountain_railway","short_names":["mountain_railway"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":923,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SUSPENSION RAILWAY","unified":"1F69F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f69f.png","sheet_x":36,"sheet_y":26,"short_name":"suspension_railway","short_names":["suspension_railway"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-air","sort_order":979,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MOUNTAIN CABLEWAY","unified":"1F6A0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6a0.png","sheet_x":36,"sheet_y":27,"short_name":"mountain_cableway","short_names":["mountain_cableway"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-air","sort_order":980,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"AERIAL TRAMWAY","unified":"1F6A1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6a1.png","sheet_x":36,"sheet_y":28,"short_name":"aerial_tramway","short_names":["aerial_tramway"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-air","sort_order":981,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SHIP","unified":"1F6A2","non_qualified":null,"docomo":"E661","au":"EA82","softbank":"E202","google":"FE7E8","image":"1f6a2.png","sheet_x":36,"sheet_y":29,"short_name":"ship","short_names":["ship"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-water","sort_order":971,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WOMAN ROWING BOAT","unified":"1F6A3-200D-2640-FE0F","non_qualified":"1F6A3-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6a3-200d-2640-fe0f.png","sheet_x":36,"sheet_y":30,"short_name":"woman-rowing-boat","short_names":["woman-rowing-boat"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":471,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F6A3-1F3FB-200D-2640-FE0F","non_qualified":"1F6A3-1F3FB-200D-2640","image":"1f6a3-1f3fb-200d-2640-fe0f.png","sheet_x":36,"sheet_y":31,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F6A3-1F3FC-200D-2640-FE0F","non_qualified":"1F6A3-1F3FC-200D-2640","image":"1f6a3-1f3fc-200d-2640-fe0f.png","sheet_x":36,"sheet_y":32,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F6A3-1F3FD-200D-2640-FE0F","non_qualified":"1F6A3-1F3FD-200D-2640","image":"1f6a3-1f3fd-200d-2640-fe0f.png","sheet_x":36,"sheet_y":33,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F6A3-1F3FE-200D-2640-FE0F","non_qualified":"1F6A3-1F3FE-200D-2640","image":"1f6a3-1f3fe-200d-2640-fe0f.png","sheet_x":36,"sheet_y":34,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F6A3-1F3FF-200D-2640-FE0F","non_qualified":"1F6A3-1F3FF-200D-2640","image":"1f6a3-1f3ff-200d-2640-fe0f.png","sheet_x":36,"sheet_y":35,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN ROWING BOAT","unified":"1F6A3-200D-2642-FE0F","non_qualified":"1F6A3-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6a3-200d-2642-fe0f.png","sheet_x":36,"sheet_y":36,"short_name":"man-rowing-boat","short_names":["man-rowing-boat"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":470,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F6A3-1F3FB-200D-2642-FE0F","non_qualified":"1F6A3-1F3FB-200D-2642","image":"1f6a3-1f3fb-200d-2642-fe0f.png","sheet_x":36,"sheet_y":37,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F6A3-1F3FC-200D-2642-FE0F","non_qualified":"1F6A3-1F3FC-200D-2642","image":"1f6a3-1f3fc-200d-2642-fe0f.png","sheet_x":36,"sheet_y":38,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F6A3-1F3FD-200D-2642-FE0F","non_qualified":"1F6A3-1F3FD-200D-2642","image":"1f6a3-1f3fd-200d-2642-fe0f.png","sheet_x":36,"sheet_y":39,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F6A3-1F3FE-200D-2642-FE0F","non_qualified":"1F6A3-1F3FE-200D-2642","image":"1f6a3-1f3fe-200d-2642-fe0f.png","sheet_x":36,"sheet_y":40,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F6A3-1F3FF-200D-2642-FE0F","non_qualified":"1F6A3-1F3FF-200D-2642","image":"1f6a3-1f3ff-200d-2642-fe0f.png","sheet_x":36,"sheet_y":41,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F6A3"},{"name":"ROWBOAT","unified":"1F6A3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6a3.png","sheet_x":36,"sheet_y":42,"short_name":"rowboat","short_names":["rowboat"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":469,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F6A3-1F3FB","non_qualified":null,"image":"1f6a3-1f3fb.png","sheet_x":36,"sheet_y":43,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F6A3-1F3FC","non_qualified":null,"image":"1f6a3-1f3fc.png","sheet_x":36,"sheet_y":44,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F6A3-1F3FD","non_qualified":null,"image":"1f6a3-1f3fd.png","sheet_x":36,"sheet_y":45,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F6A3-1F3FE","non_qualified":null,"image":"1f6a3-1f3fe.png","sheet_x":36,"sheet_y":46,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F6A3-1F3FF","non_qualified":null,"image":"1f6a3-1f3ff.png","sheet_x":36,"sheet_y":47,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F6A3-200D-2642-FE0F"},{"name":"SPEEDBOAT","unified":"1F6A4","non_qualified":null,"docomo":"E6A3","au":"E4B4","softbank":"E135","google":"FE7EE","image":"1f6a4.png","sheet_x":36,"sheet_y":48,"short_name":"speedboat","short_names":["speedboat"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-water","sort_order":967,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HORIZONTAL TRAFFIC LIGHT","unified":"1F6A5","non_qualified":null,"docomo":"E66D","au":"E46A","softbank":"E14E","google":"FE7F7","image":"1f6a5.png","sheet_x":36,"sheet_y":49,"short_name":"traffic_light","short_names":["traffic_light"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":959,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"VERTICAL TRAFFIC LIGHT","unified":"1F6A6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6a6.png","sheet_x":36,"sheet_y":50,"short_name":"vertical_traffic_light","short_names":["vertical_traffic_light"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":960,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CONSTRUCTION SIGN","unified":"1F6A7","non_qualified":null,"docomo":null,"au":"E5D7","softbank":"E137","google":"FE7F8","image":"1f6a7.png","sheet_x":36,"sheet_y":51,"short_name":"construction","short_names":["construction"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":962,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"POLICE CARS REVOLVING LIGHT","unified":"1F6A8","non_qualified":null,"docomo":null,"au":"EB73","softbank":null,"google":"FE7F9","image":"1f6a8.png","sheet_x":36,"sheet_y":52,"short_name":"rotating_light","short_names":["rotating_light"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":958,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TRIANGULAR FLAG ON POST","unified":"1F6A9","non_qualified":null,"docomo":"E6DE","au":"EB2C","softbank":null,"google":"FEB22","image":"1f6a9.png","sheet_x":36,"sheet_y":53,"short_name":"triangular_flag_on_post","short_names":["triangular_flag_on_post"],"text":null,"texts":null,"category":"Flags","subcategory":"flag","sort_order":1636,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DOOR","unified":"1F6AA","non_qualified":null,"docomo":"E714","au":null,"softbank":null,"google":"FE4F3","image":"1f6aa.png","sheet_x":36,"sheet_y":54,"short_name":"door","short_names":["door"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1378,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NO ENTRY SIGN","unified":"1F6AB","non_qualified":null,"docomo":"E738","au":"E541","softbank":null,"google":"FEB48","image":"1f6ab.png","sheet_x":36,"sheet_y":55,"short_name":"no_entry_sign","short_names":["no_entry_sign"],"text":null,"texts":null,"category":"Symbols","subcategory":"warning","sort_order":1428,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SMOKING SYMBOL","unified":"1F6AC","non_qualified":null,"docomo":"E67F","au":"E47D","softbank":"E30E","google":"FEB1E","image":"1f6ac.png","sheet_x":36,"sheet_y":56,"short_name":"smoking","short_names":["smoking"],"text":null,"texts":null,"category":"Objects","subcategory":"other-object","sort_order":1403,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NO SMOKING SYMBOL","unified":"1F6AD","non_qualified":null,"docomo":"E680","au":"E47E","softbank":"E208","google":"FEB1F","image":"1f6ad.png","sheet_x":36,"sheet_y":57,"short_name":"no_smoking","short_names":["no_smoking"],"text":null,"texts":null,"category":"Symbols","subcategory":"warning","sort_order":1430,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PUT LITTER IN ITS PLACE SYMBOL","unified":"1F6AE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6ae.png","sheet_x":36,"sheet_y":58,"short_name":"put_litter_in_its_place","short_names":["put_litter_in_its_place"],"text":null,"texts":null,"category":"Symbols","subcategory":"transport-sign","sort_order":1413,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DO NOT LITTER SYMBOL","unified":"1F6AF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6af.png","sheet_x":36,"sheet_y":59,"short_name":"do_not_litter","short_names":["do_not_litter"],"text":null,"texts":null,"category":"Symbols","subcategory":"warning","sort_order":1431,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"POTABLE WATER SYMBOL","unified":"1F6B0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6b0.png","sheet_x":36,"sheet_y":60,"short_name":"potable_water","short_names":["potable_water"],"text":null,"texts":null,"category":"Symbols","subcategory":"transport-sign","sort_order":1414,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NON-POTABLE WATER SYMBOL","unified":"1F6B1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6b1.png","sheet_x":36,"sheet_y":61,"short_name":"non-potable_water","short_names":["non-potable_water"],"text":null,"texts":null,"category":"Symbols","subcategory":"warning","sort_order":1432,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BICYCLE","unified":"1F6B2","non_qualified":null,"docomo":"E71D","au":"E4AE","softbank":"E136","google":"FE7EB","image":"1f6b2.png","sheet_x":37,"sheet_y":0,"short_name":"bike","short_names":["bike"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":948,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NO BICYCLES","unified":"1F6B3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6b3.png","sheet_x":37,"sheet_y":1,"short_name":"no_bicycles","short_names":["no_bicycles"],"text":null,"texts":null,"category":"Symbols","subcategory":"warning","sort_order":1429,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WOMAN BIKING","unified":"1F6B4-200D-2640-FE0F","non_qualified":"1F6B4-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6b4-200d-2640-fe0f.png","sheet_x":37,"sheet_y":2,"short_name":"woman-biking","short_names":["woman-biking"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":483,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F6B4-1F3FB-200D-2640-FE0F","non_qualified":"1F6B4-1F3FB-200D-2640","image":"1f6b4-1f3fb-200d-2640-fe0f.png","sheet_x":37,"sheet_y":3,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F6B4-1F3FC-200D-2640-FE0F","non_qualified":"1F6B4-1F3FC-200D-2640","image":"1f6b4-1f3fc-200d-2640-fe0f.png","sheet_x":37,"sheet_y":4,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F6B4-1F3FD-200D-2640-FE0F","non_qualified":"1F6B4-1F3FD-200D-2640","image":"1f6b4-1f3fd-200d-2640-fe0f.png","sheet_x":37,"sheet_y":5,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F6B4-1F3FE-200D-2640-FE0F","non_qualified":"1F6B4-1F3FE-200D-2640","image":"1f6b4-1f3fe-200d-2640-fe0f.png","sheet_x":37,"sheet_y":6,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F6B4-1F3FF-200D-2640-FE0F","non_qualified":"1F6B4-1F3FF-200D-2640","image":"1f6b4-1f3ff-200d-2640-fe0f.png","sheet_x":37,"sheet_y":7,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN BIKING","unified":"1F6B4-200D-2642-FE0F","non_qualified":"1F6B4-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6b4-200d-2642-fe0f.png","sheet_x":37,"sheet_y":8,"short_name":"man-biking","short_names":["man-biking"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":482,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F6B4-1F3FB-200D-2642-FE0F","non_qualified":"1F6B4-1F3FB-200D-2642","image":"1f6b4-1f3fb-200d-2642-fe0f.png","sheet_x":37,"sheet_y":9,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F6B4-1F3FC-200D-2642-FE0F","non_qualified":"1F6B4-1F3FC-200D-2642","image":"1f6b4-1f3fc-200d-2642-fe0f.png","sheet_x":37,"sheet_y":10,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F6B4-1F3FD-200D-2642-FE0F","non_qualified":"1F6B4-1F3FD-200D-2642","image":"1f6b4-1f3fd-200d-2642-fe0f.png","sheet_x":37,"sheet_y":11,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F6B4-1F3FE-200D-2642-FE0F","non_qualified":"1F6B4-1F3FE-200D-2642","image":"1f6b4-1f3fe-200d-2642-fe0f.png","sheet_x":37,"sheet_y":12,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F6B4-1F3FF-200D-2642-FE0F","non_qualified":"1F6B4-1F3FF-200D-2642","image":"1f6b4-1f3ff-200d-2642-fe0f.png","sheet_x":37,"sheet_y":13,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F6B4"},{"name":"BICYCLIST","unified":"1F6B4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6b4.png","sheet_x":37,"sheet_y":14,"short_name":"bicyclist","short_names":["bicyclist"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":481,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F6B4-1F3FB","non_qualified":null,"image":"1f6b4-1f3fb.png","sheet_x":37,"sheet_y":15,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F6B4-1F3FC","non_qualified":null,"image":"1f6b4-1f3fc.png","sheet_x":37,"sheet_y":16,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F6B4-1F3FD","non_qualified":null,"image":"1f6b4-1f3fd.png","sheet_x":37,"sheet_y":17,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F6B4-1F3FE","non_qualified":null,"image":"1f6b4-1f3fe.png","sheet_x":37,"sheet_y":18,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F6B4-1F3FF","non_qualified":null,"image":"1f6b4-1f3ff.png","sheet_x":37,"sheet_y":19,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F6B4-200D-2642-FE0F"},{"name":"WOMAN MOUNTAIN BIKING","unified":"1F6B5-200D-2640-FE0F","non_qualified":"1F6B5-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6b5-200d-2640-fe0f.png","sheet_x":37,"sheet_y":20,"short_name":"woman-mountain-biking","short_names":["woman-mountain-biking"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":486,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F6B5-1F3FB-200D-2640-FE0F","non_qualified":"1F6B5-1F3FB-200D-2640","image":"1f6b5-1f3fb-200d-2640-fe0f.png","sheet_x":37,"sheet_y":21,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F6B5-1F3FC-200D-2640-FE0F","non_qualified":"1F6B5-1F3FC-200D-2640","image":"1f6b5-1f3fc-200d-2640-fe0f.png","sheet_x":37,"sheet_y":22,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F6B5-1F3FD-200D-2640-FE0F","non_qualified":"1F6B5-1F3FD-200D-2640","image":"1f6b5-1f3fd-200d-2640-fe0f.png","sheet_x":37,"sheet_y":23,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F6B5-1F3FE-200D-2640-FE0F","non_qualified":"1F6B5-1F3FE-200D-2640","image":"1f6b5-1f3fe-200d-2640-fe0f.png","sheet_x":37,"sheet_y":24,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F6B5-1F3FF-200D-2640-FE0F","non_qualified":"1F6B5-1F3FF-200D-2640","image":"1f6b5-1f3ff-200d-2640-fe0f.png","sheet_x":37,"sheet_y":25,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN MOUNTAIN BIKING","unified":"1F6B5-200D-2642-FE0F","non_qualified":"1F6B5-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6b5-200d-2642-fe0f.png","sheet_x":37,"sheet_y":26,"short_name":"man-mountain-biking","short_names":["man-mountain-biking"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":485,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F6B5-1F3FB-200D-2642-FE0F","non_qualified":"1F6B5-1F3FB-200D-2642","image":"1f6b5-1f3fb-200d-2642-fe0f.png","sheet_x":37,"sheet_y":27,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F6B5-1F3FC-200D-2642-FE0F","non_qualified":"1F6B5-1F3FC-200D-2642","image":"1f6b5-1f3fc-200d-2642-fe0f.png","sheet_x":37,"sheet_y":28,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F6B5-1F3FD-200D-2642-FE0F","non_qualified":"1F6B5-1F3FD-200D-2642","image":"1f6b5-1f3fd-200d-2642-fe0f.png","sheet_x":37,"sheet_y":29,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F6B5-1F3FE-200D-2642-FE0F","non_qualified":"1F6B5-1F3FE-200D-2642","image":"1f6b5-1f3fe-200d-2642-fe0f.png","sheet_x":37,"sheet_y":30,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F6B5-1F3FF-200D-2642-FE0F","non_qualified":"1F6B5-1F3FF-200D-2642","image":"1f6b5-1f3ff-200d-2642-fe0f.png","sheet_x":37,"sheet_y":31,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F6B5"},{"name":"MOUNTAIN BICYCLIST","unified":"1F6B5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6b5.png","sheet_x":37,"sheet_y":32,"short_name":"mountain_bicyclist","short_names":["mountain_bicyclist"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":484,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F6B5-1F3FB","non_qualified":null,"image":"1f6b5-1f3fb.png","sheet_x":37,"sheet_y":33,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F6B5-1F3FC","non_qualified":null,"image":"1f6b5-1f3fc.png","sheet_x":37,"sheet_y":34,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F6B5-1F3FD","non_qualified":null,"image":"1f6b5-1f3fd.png","sheet_x":37,"sheet_y":35,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F6B5-1F3FE","non_qualified":null,"image":"1f6b5-1f3fe.png","sheet_x":37,"sheet_y":36,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F6B5-1F3FF","non_qualified":null,"image":"1f6b5-1f3ff.png","sheet_x":37,"sheet_y":37,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F6B5-200D-2642-FE0F"},{"name":"WOMAN WALKING","unified":"1F6B6-200D-2640-FE0F","non_qualified":"1F6B6-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6b6-200d-2640-fe0f.png","sheet_x":37,"sheet_y":38,"short_name":"woman-walking","short_names":["woman-walking"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":410,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F6B6-1F3FB-200D-2640-FE0F","non_qualified":"1F6B6-1F3FB-200D-2640","image":"1f6b6-1f3fb-200d-2640-fe0f.png","sheet_x":37,"sheet_y":39,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F6B6-1F3FC-200D-2640-FE0F","non_qualified":"1F6B6-1F3FC-200D-2640","image":"1f6b6-1f3fc-200d-2640-fe0f.png","sheet_x":37,"sheet_y":40,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F6B6-1F3FD-200D-2640-FE0F","non_qualified":"1F6B6-1F3FD-200D-2640","image":"1f6b6-1f3fd-200d-2640-fe0f.png","sheet_x":37,"sheet_y":41,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F6B6-1F3FE-200D-2640-FE0F","non_qualified":"1F6B6-1F3FE-200D-2640","image":"1f6b6-1f3fe-200d-2640-fe0f.png","sheet_x":37,"sheet_y":42,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F6B6-1F3FF-200D-2640-FE0F","non_qualified":"1F6B6-1F3FF-200D-2640","image":"1f6b6-1f3ff-200d-2640-fe0f.png","sheet_x":37,"sheet_y":43,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN WALKING FACING RIGHT","unified":"1F6B6-200D-2640-FE0F-200D-27A1-FE0F","non_qualified":"1F6B6-200D-2640-200D-27A1","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6b6-200d-2640-fe0f-200d-27a1-fe0f.png","sheet_x":37,"sheet_y":44,"short_name":"woman_walking_facing_right","short_names":["woman_walking_facing_right"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":412,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"1F6B6-1F3FB-200D-2640-FE0F-200D-27A1-FE0F","non_qualified":"1F6B6-1F3FB-200D-2640-200D-27A1","image":"1f6b6-1f3fb-200d-2640-fe0f-200d-27a1-fe0f.png","sheet_x":37,"sheet_y":45,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FC":{"unified":"1F6B6-1F3FC-200D-2640-FE0F-200D-27A1-FE0F","non_qualified":"1F6B6-1F3FC-200D-2640-200D-27A1","image":"1f6b6-1f3fc-200d-2640-fe0f-200d-27a1-fe0f.png","sheet_x":37,"sheet_y":46,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FD":{"unified":"1F6B6-1F3FD-200D-2640-FE0F-200D-27A1-FE0F","non_qualified":"1F6B6-1F3FD-200D-2640-200D-27A1","image":"1f6b6-1f3fd-200d-2640-fe0f-200d-27a1-fe0f.png","sheet_x":37,"sheet_y":47,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FE":{"unified":"1F6B6-1F3FE-200D-2640-FE0F-200D-27A1-FE0F","non_qualified":"1F6B6-1F3FE-200D-2640-200D-27A1","image":"1f6b6-1f3fe-200d-2640-fe0f-200d-27a1-fe0f.png","sheet_x":37,"sheet_y":48,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FF":{"unified":"1F6B6-1F3FF-200D-2640-FE0F-200D-27A1-FE0F","non_qualified":"1F6B6-1F3FF-200D-2640-200D-27A1","image":"1f6b6-1f3ff-200d-2640-fe0f-200d-27a1-fe0f.png","sheet_x":37,"sheet_y":49,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false}}},{"name":"MAN WALKING","unified":"1F6B6-200D-2642-FE0F","non_qualified":"1F6B6-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6b6-200d-2642-fe0f.png","sheet_x":37,"sheet_y":50,"short_name":"man-walking","short_names":["man-walking"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":409,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F6B6-1F3FB-200D-2642-FE0F","non_qualified":"1F6B6-1F3FB-200D-2642","image":"1f6b6-1f3fb-200d-2642-fe0f.png","sheet_x":37,"sheet_y":51,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F6B6-1F3FC-200D-2642-FE0F","non_qualified":"1F6B6-1F3FC-200D-2642","image":"1f6b6-1f3fc-200d-2642-fe0f.png","sheet_x":37,"sheet_y":52,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F6B6-1F3FD-200D-2642-FE0F","non_qualified":"1F6B6-1F3FD-200D-2642","image":"1f6b6-1f3fd-200d-2642-fe0f.png","sheet_x":37,"sheet_y":53,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F6B6-1F3FE-200D-2642-FE0F","non_qualified":"1F6B6-1F3FE-200D-2642","image":"1f6b6-1f3fe-200d-2642-fe0f.png","sheet_x":37,"sheet_y":54,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F6B6-1F3FF-200D-2642-FE0F","non_qualified":"1F6B6-1F3FF-200D-2642","image":"1f6b6-1f3ff-200d-2642-fe0f.png","sheet_x":37,"sheet_y":55,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"1F6B6"},{"name":"MAN WALKING FACING RIGHT","unified":"1F6B6-200D-2642-FE0F-200D-27A1-FE0F","non_qualified":"1F6B6-200D-2642-200D-27A1","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6b6-200d-2642-fe0f-200d-27a1-fe0f.png","sheet_x":37,"sheet_y":56,"short_name":"man_walking_facing_right","short_names":["man_walking_facing_right"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":413,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"1F6B6-1F3FB-200D-2642-FE0F-200D-27A1-FE0F","non_qualified":"1F6B6-1F3FB-200D-2642-200D-27A1","image":"1f6b6-1f3fb-200d-2642-fe0f-200d-27a1-fe0f.png","sheet_x":37,"sheet_y":57,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FC":{"unified":"1F6B6-1F3FC-200D-2642-FE0F-200D-27A1-FE0F","non_qualified":"1F6B6-1F3FC-200D-2642-200D-27A1","image":"1f6b6-1f3fc-200d-2642-fe0f-200d-27a1-fe0f.png","sheet_x":37,"sheet_y":58,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FD":{"unified":"1F6B6-1F3FD-200D-2642-FE0F-200D-27A1-FE0F","non_qualified":"1F6B6-1F3FD-200D-2642-200D-27A1","image":"1f6b6-1f3fd-200d-2642-fe0f-200d-27a1-fe0f.png","sheet_x":37,"sheet_y":59,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FE":{"unified":"1F6B6-1F3FE-200D-2642-FE0F-200D-27A1-FE0F","non_qualified":"1F6B6-1F3FE-200D-2642-200D-27A1","image":"1f6b6-1f3fe-200d-2642-fe0f-200d-27a1-fe0f.png","sheet_x":37,"sheet_y":60,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FF":{"unified":"1F6B6-1F3FF-200D-2642-FE0F-200D-27A1-FE0F","non_qualified":"1F6B6-1F3FF-200D-2642-200D-27A1","image":"1f6b6-1f3ff-200d-2642-fe0f-200d-27a1-fe0f.png","sheet_x":37,"sheet_y":61,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false}}},{"name":"PERSON WALKING FACING RIGHT","unified":"1F6B6-200D-27A1-FE0F","non_qualified":"1F6B6-200D-27A1","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6b6-200d-27a1-fe0f.png","sheet_x":38,"sheet_y":0,"short_name":"person_walking_facing_right","short_names":["person_walking_facing_right"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":411,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"1F6B6-1F3FB-200D-27A1-FE0F","non_qualified":"1F6B6-1F3FB-200D-27A1","image":"1f6b6-1f3fb-200d-27a1-fe0f.png","sheet_x":38,"sheet_y":1,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FC":{"unified":"1F6B6-1F3FC-200D-27A1-FE0F","non_qualified":"1F6B6-1F3FC-200D-27A1","image":"1f6b6-1f3fc-200d-27a1-fe0f.png","sheet_x":38,"sheet_y":2,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FD":{"unified":"1F6B6-1F3FD-200D-27A1-FE0F","non_qualified":"1F6B6-1F3FD-200D-27A1","image":"1f6b6-1f3fd-200d-27a1-fe0f.png","sheet_x":38,"sheet_y":3,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FE":{"unified":"1F6B6-1F3FE-200D-27A1-FE0F","non_qualified":"1F6B6-1F3FE-200D-27A1","image":"1f6b6-1f3fe-200d-27a1-fe0f.png","sheet_x":38,"sheet_y":4,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FF":{"unified":"1F6B6-1F3FF-200D-27A1-FE0F","non_qualified":"1F6B6-1F3FF-200D-27A1","image":"1f6b6-1f3ff-200d-27a1-fe0f.png","sheet_x":38,"sheet_y":5,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false}}},{"name":"PEDESTRIAN","unified":"1F6B6","non_qualified":null,"docomo":"E733","au":"EB72","softbank":"E201","google":"FE7F0","image":"1f6b6.png","sheet_x":38,"sheet_y":6,"short_name":"walking","short_names":["walking"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":408,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F6B6-1F3FB","non_qualified":null,"image":"1f6b6-1f3fb.png","sheet_x":38,"sheet_y":7,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F6B6-1F3FC","non_qualified":null,"image":"1f6b6-1f3fc.png","sheet_x":38,"sheet_y":8,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F6B6-1F3FD","non_qualified":null,"image":"1f6b6-1f3fd.png","sheet_x":38,"sheet_y":9,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F6B6-1F3FE","non_qualified":null,"image":"1f6b6-1f3fe.png","sheet_x":38,"sheet_y":10,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F6B6-1F3FF","non_qualified":null,"image":"1f6b6-1f3ff.png","sheet_x":38,"sheet_y":11,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"1F6B6-200D-2642-FE0F"},{"name":"NO PEDESTRIANS","unified":"1F6B7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6b7.png","sheet_x":38,"sheet_y":12,"short_name":"no_pedestrians","short_names":["no_pedestrians"],"text":null,"texts":null,"category":"Symbols","subcategory":"warning","sort_order":1433,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CHILDREN CROSSING","unified":"1F6B8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6b8.png","sheet_x":38,"sheet_y":13,"short_name":"children_crossing","short_names":["children_crossing"],"text":null,"texts":null,"category":"Symbols","subcategory":"warning","sort_order":1426,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MENS SYMBOL","unified":"1F6B9","non_qualified":null,"docomo":null,"au":null,"softbank":"E138","google":"FEB33","image":"1f6b9.png","sheet_x":38,"sheet_y":14,"short_name":"mens","short_names":["mens"],"text":null,"texts":null,"category":"Symbols","subcategory":"transport-sign","sort_order":1416,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WOMENS SYMBOL","unified":"1F6BA","non_qualified":null,"docomo":null,"au":null,"softbank":"E139","google":"FEB34","image":"1f6ba.png","sheet_x":38,"sheet_y":15,"short_name":"womens","short_names":["womens"],"text":null,"texts":null,"category":"Symbols","subcategory":"transport-sign","sort_order":1417,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RESTROOM","unified":"1F6BB","non_qualified":null,"docomo":"E66E","au":"E4A5","softbank":"E151","google":"FE506","image":"1f6bb.png","sheet_x":38,"sheet_y":16,"short_name":"restroom","short_names":["restroom"],"text":null,"texts":null,"category":"Symbols","subcategory":"transport-sign","sort_order":1418,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BABY SYMBOL","unified":"1F6BC","non_qualified":null,"docomo":null,"au":"EB18","softbank":"E13A","google":"FEB35","image":"1f6bc.png","sheet_x":38,"sheet_y":17,"short_name":"baby_symbol","short_names":["baby_symbol"],"text":null,"texts":null,"category":"Symbols","subcategory":"transport-sign","sort_order":1419,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TOILET","unified":"1F6BD","non_qualified":null,"docomo":"E66E","au":"E4A5","softbank":"E140","google":"FE507","image":"1f6bd.png","sheet_x":38,"sheet_y":18,"short_name":"toilet","short_names":["toilet"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1385,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WATER CLOSET","unified":"1F6BE","non_qualified":null,"docomo":"E66E","au":"E4A5","softbank":"E309","google":"FE508","image":"1f6be.png","sheet_x":38,"sheet_y":19,"short_name":"wc","short_names":["wc"],"text":null,"texts":null,"category":"Symbols","subcategory":"transport-sign","sort_order":1420,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SHOWER","unified":"1F6BF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6bf.png","sheet_x":38,"sheet_y":20,"short_name":"shower","short_names":["shower"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1387,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BATH","unified":"1F6C0","non_qualified":null,"docomo":"E6F7","au":"E5D8","softbank":"E13F","google":"FE505","image":"1f6c0.png","sheet_x":38,"sheet_y":21,"short_name":"bath","short_names":["bath"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-resting","sort_order":505,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F6C0-1F3FB","non_qualified":null,"image":"1f6c0-1f3fb.png","sheet_x":38,"sheet_y":22,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F6C0-1F3FC","non_qualified":null,"image":"1f6c0-1f3fc.png","sheet_x":38,"sheet_y":23,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F6C0-1F3FD","non_qualified":null,"image":"1f6c0-1f3fd.png","sheet_x":38,"sheet_y":24,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F6C0-1F3FE","non_qualified":null,"image":"1f6c0-1f3fe.png","sheet_x":38,"sheet_y":25,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F6C0-1F3FF","non_qualified":null,"image":"1f6c0-1f3ff.png","sheet_x":38,"sheet_y":26,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"BATHTUB","unified":"1F6C1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6c1.png","sheet_x":38,"sheet_y":27,"short_name":"bathtub","short_names":["bathtub"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1388,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PASSPORT CONTROL","unified":"1F6C2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6c2.png","sheet_x":38,"sheet_y":28,"short_name":"passport_control","short_names":["passport_control"],"text":null,"texts":null,"category":"Symbols","subcategory":"transport-sign","sort_order":1421,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CUSTOMS","unified":"1F6C3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6c3.png","sheet_x":38,"sheet_y":29,"short_name":"customs","short_names":["customs"],"text":null,"texts":null,"category":"Symbols","subcategory":"transport-sign","sort_order":1422,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BAGGAGE CLAIM","unified":"1F6C4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6c4.png","sheet_x":38,"sheet_y":30,"short_name":"baggage_claim","short_names":["baggage_claim"],"text":null,"texts":null,"category":"Symbols","subcategory":"transport-sign","sort_order":1423,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LEFT LUGGAGE","unified":"1F6C5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6c5.png","sheet_x":38,"sheet_y":31,"short_name":"left_luggage","short_names":["left_luggage"],"text":null,"texts":null,"category":"Symbols","subcategory":"transport-sign","sort_order":1424,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"COUCH AND LAMP","unified":"1F6CB-FE0F","non_qualified":"1F6CB","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6cb-fe0f.png","sheet_x":38,"sheet_y":32,"short_name":"couch_and_lamp","short_names":["couch_and_lamp"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1383,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SLEEPING ACCOMMODATION","unified":"1F6CC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6cc.png","sheet_x":38,"sheet_y":33,"short_name":"sleeping_accommodation","short_names":["sleeping_accommodation"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-resting","sort_order":506,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F6CC-1F3FB","non_qualified":null,"image":"1f6cc-1f3fb.png","sheet_x":38,"sheet_y":34,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F6CC-1F3FC","non_qualified":null,"image":"1f6cc-1f3fc.png","sheet_x":38,"sheet_y":35,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F6CC-1F3FD","non_qualified":null,"image":"1f6cc-1f3fd.png","sheet_x":38,"sheet_y":36,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F6CC-1F3FE","non_qualified":null,"image":"1f6cc-1f3fe.png","sheet_x":38,"sheet_y":37,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F6CC-1F3FF","non_qualified":null,"image":"1f6cc-1f3ff.png","sheet_x":38,"sheet_y":38,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"SHOPPING BAGS","unified":"1F6CD-FE0F","non_qualified":"1F6CD","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6cd-fe0f.png","sheet_x":38,"sheet_y":39,"short_name":"shopping_bags","short_names":["shopping_bags"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1174,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BELLHOP BELL","unified":"1F6CE-FE0F","non_qualified":"1F6CE","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6ce-fe0f.png","sheet_x":38,"sheet_y":40,"short_name":"bellhop_bell","short_names":["bellhop_bell"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"hotel","sort_order":985,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BED","unified":"1F6CF-FE0F","non_qualified":"1F6CF","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6cf-fe0f.png","sheet_x":38,"sheet_y":41,"short_name":"bed","short_names":["bed"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1382,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PLACE OF WORSHIP","unified":"1F6D0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6d0.png","sheet_x":38,"sheet_y":42,"short_name":"place_of_worship","short_names":["place_of_worship"],"text":null,"texts":null,"category":"Symbols","subcategory":"religion","sort_order":1459,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"OCTAGONAL SIGN","unified":"1F6D1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6d1.png","sheet_x":38,"sheet_y":43,"short_name":"octagonal_sign","short_names":["octagonal_sign"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":961,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SHOPPING TROLLEY","unified":"1F6D2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6d2.png","sheet_x":38,"sheet_y":44,"short_name":"shopping_trolley","short_names":["shopping_trolley"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1402,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HINDU TEMPLE","unified":"1F6D5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6d5.png","sheet_x":38,"sheet_y":45,"short_name":"hindu_temple","short_names":["hindu_temple"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-religious","sort_order":892,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HUT","unified":"1F6D6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6d6.png","sheet_x":38,"sheet_y":46,"short_name":"hut","short_names":["hut"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":869,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ELEVATOR","unified":"1F6D7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6d7.png","sheet_x":38,"sheet_y":47,"short_name":"elevator","short_names":["elevator"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1379,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WIRELESS","unified":"1F6DC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6dc.png","sheet_x":38,"sheet_y":48,"short_name":"wireless","short_names":["wireless"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1507,"added_in":"15.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PLAYGROUND SLIDE","unified":"1F6DD","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6dd.png","sheet_x":38,"sheet_y":49,"short_name":"playground_slide","short_names":["playground_slide"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-other","sort_order":908,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WHEEL","unified":"1F6DE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6de.png","sheet_x":38,"sheet_y":50,"short_name":"wheel","short_names":["wheel"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":957,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RING BUOY","unified":"1F6DF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6df.png","sheet_x":38,"sheet_y":51,"short_name":"ring_buoy","short_names":["ring_buoy"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-water","sort_order":964,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HAMMER AND WRENCH","unified":"1F6E0-FE0F","non_qualified":"1F6E0","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6e0-fe0f.png","sheet_x":38,"sheet_y":52,"short_name":"hammer_and_wrench","short_names":["hammer_and_wrench"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1342,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SHIELD","unified":"1F6E1-FE0F","non_qualified":"1F6E1","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6e1-fe0f.png","sheet_x":38,"sheet_y":53,"short_name":"shield","short_names":["shield"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1348,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"OIL DRUM","unified":"1F6E2-FE0F","non_qualified":"1F6E2","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6e2-fe0f.png","sheet_x":38,"sheet_y":54,"short_name":"oil_drum","short_names":["oil_drum"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":955,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MOTORWAY","unified":"1F6E3-FE0F","non_qualified":"1F6E3","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6e3-fe0f.png","sheet_x":38,"sheet_y":55,"short_name":"motorway","short_names":["motorway"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":953,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RAILWAY TRACK","unified":"1F6E4-FE0F","non_qualified":"1F6E4","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6e4-fe0f.png","sheet_x":38,"sheet_y":56,"short_name":"railway_track","short_names":["railway_track"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":954,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MOTOR BOAT","unified":"1F6E5-FE0F","non_qualified":"1F6E5","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6e5-fe0f.png","sheet_x":38,"sheet_y":57,"short_name":"motor_boat","short_names":["motor_boat"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-water","sort_order":970,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SMALL AIRPLANE","unified":"1F6E9-FE0F","non_qualified":"1F6E9","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6e9-fe0f.png","sheet_x":38,"sheet_y":58,"short_name":"small_airplane","short_names":["small_airplane"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-air","sort_order":973,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"AIRPLANE DEPARTURE","unified":"1F6EB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6eb.png","sheet_x":38,"sheet_y":59,"short_name":"airplane_departure","short_names":["airplane_departure"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-air","sort_order":974,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"AIRPLANE ARRIVING","unified":"1F6EC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6ec.png","sheet_x":38,"sheet_y":60,"short_name":"airplane_arriving","short_names":["airplane_arriving"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-air","sort_order":975,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SATELLITE","unified":"1F6F0-FE0F","non_qualified":"1F6F0","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6f0-fe0f.png","sheet_x":38,"sheet_y":61,"short_name":"satellite","short_names":["satellite"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-air","sort_order":982,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PASSENGER SHIP","unified":"1F6F3-FE0F","non_qualified":"1F6F3","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6f3-fe0f.png","sheet_x":39,"sheet_y":0,"short_name":"passenger_ship","short_names":["passenger_ship"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-water","sort_order":968,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SCOOTER","unified":"1F6F4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6f4.png","sheet_x":39,"sheet_y":1,"short_name":"scooter","short_names":["scooter"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":949,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MOTOR SCOOTER","unified":"1F6F5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6f5.png","sheet_x":39,"sheet_y":2,"short_name":"motor_scooter","short_names":["motor_scooter"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":944,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CANOE","unified":"1F6F6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6f6.png","sheet_x":39,"sheet_y":3,"short_name":"canoe","short_names":["canoe"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-water","sort_order":966,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SLED","unified":"1F6F7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6f7.png","sheet_x":39,"sheet_y":4,"short_name":"sled","short_names":["sled"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1117,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FLYING SAUCER","unified":"1F6F8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6f8.png","sheet_x":39,"sheet_y":5,"short_name":"flying_saucer","short_names":["flying_saucer"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-air","sort_order":984,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SKATEBOARD","unified":"1F6F9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6f9.png","sheet_x":39,"sheet_y":6,"short_name":"skateboard","short_names":["skateboard"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":950,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"AUTO RICKSHAW","unified":"1F6FA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6fa.png","sheet_x":39,"sheet_y":7,"short_name":"auto_rickshaw","short_names":["auto_rickshaw"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":947,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PICKUP TRUCK","unified":"1F6FB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6fb.png","sheet_x":39,"sheet_y":8,"short_name":"pickup_truck","short_names":["pickup_truck"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":938,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ROLLER SKATE","unified":"1F6FC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f6fc.png","sheet_x":39,"sheet_y":9,"short_name":"roller_skate","short_names":["roller_skate"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":951,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LARGE ORANGE CIRCLE","unified":"1F7E0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f7e0.png","sheet_x":39,"sheet_y":10,"short_name":"large_orange_circle","short_names":["large_orange_circle"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1602,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LARGE YELLOW CIRCLE","unified":"1F7E1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f7e1.png","sheet_x":39,"sheet_y":11,"short_name":"large_yellow_circle","short_names":["large_yellow_circle"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1603,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LARGE GREEN CIRCLE","unified":"1F7E2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f7e2.png","sheet_x":39,"sheet_y":12,"short_name":"large_green_circle","short_names":["large_green_circle"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1604,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LARGE PURPLE CIRCLE","unified":"1F7E3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f7e3.png","sheet_x":39,"sheet_y":13,"short_name":"large_purple_circle","short_names":["large_purple_circle"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1606,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LARGE BROWN CIRCLE","unified":"1F7E4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f7e4.png","sheet_x":39,"sheet_y":14,"short_name":"large_brown_circle","short_names":["large_brown_circle"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1607,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LARGE RED SQUARE","unified":"1F7E5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f7e5.png","sheet_x":39,"sheet_y":15,"short_name":"large_red_square","short_names":["large_red_square"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1610,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LARGE BLUE SQUARE","unified":"1F7E6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f7e6.png","sheet_x":39,"sheet_y":16,"short_name":"large_blue_square","short_names":["large_blue_square"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1614,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LARGE ORANGE SQUARE","unified":"1F7E7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f7e7.png","sheet_x":39,"sheet_y":17,"short_name":"large_orange_square","short_names":["large_orange_square"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1611,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LARGE YELLOW SQUARE","unified":"1F7E8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f7e8.png","sheet_x":39,"sheet_y":18,"short_name":"large_yellow_square","short_names":["large_yellow_square"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1612,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LARGE GREEN SQUARE","unified":"1F7E9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f7e9.png","sheet_x":39,"sheet_y":19,"short_name":"large_green_square","short_names":["large_green_square"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1613,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LARGE PURPLE SQUARE","unified":"1F7EA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f7ea.png","sheet_x":39,"sheet_y":20,"short_name":"large_purple_square","short_names":["large_purple_square"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1615,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LARGE BROWN SQUARE","unified":"1F7EB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f7eb.png","sheet_x":39,"sheet_y":21,"short_name":"large_brown_square","short_names":["large_brown_square"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1616,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HEAVY EQUALS SIGN","unified":"1F7F0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f7f0.png","sheet_x":39,"sheet_y":22,"short_name":"heavy_equals_sign","short_names":["heavy_equals_sign"],"text":null,"texts":null,"category":"Symbols","subcategory":"math","sort_order":1517,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PINCHED FINGERS","unified":"1F90C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f90c.png","sheet_x":39,"sheet_y":23,"short_name":"pinched_fingers","short_names":["pinched_fingers"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-partial","sort_order":181,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F90C-1F3FB","non_qualified":null,"image":"1f90c-1f3fb.png","sheet_x":39,"sheet_y":24,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F90C-1F3FC","non_qualified":null,"image":"1f90c-1f3fc.png","sheet_x":39,"sheet_y":25,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F90C-1F3FD","non_qualified":null,"image":"1f90c-1f3fd.png","sheet_x":39,"sheet_y":26,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F90C-1F3FE","non_qualified":null,"image":"1f90c-1f3fe.png","sheet_x":39,"sheet_y":27,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F90C-1F3FF","non_qualified":null,"image":"1f90c-1f3ff.png","sheet_x":39,"sheet_y":28,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WHITE HEART","unified":"1F90D","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f90d.png","sheet_x":39,"sheet_y":29,"short_name":"white_heart","short_names":["white_heart"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"heart","sort_order":154,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BROWN HEART","unified":"1F90E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f90e.png","sheet_x":39,"sheet_y":30,"short_name":"brown_heart","short_names":["brown_heart"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"heart","sort_order":151,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PINCHING HAND","unified":"1F90F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f90f.png","sheet_x":39,"sheet_y":31,"short_name":"pinching_hand","short_names":["pinching_hand"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-partial","sort_order":182,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F90F-1F3FB","non_qualified":null,"image":"1f90f-1f3fb.png","sheet_x":39,"sheet_y":32,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F90F-1F3FC","non_qualified":null,"image":"1f90f-1f3fc.png","sheet_x":39,"sheet_y":33,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F90F-1F3FD","non_qualified":null,"image":"1f90f-1f3fd.png","sheet_x":39,"sheet_y":34,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F90F-1F3FE","non_qualified":null,"image":"1f90f-1f3fe.png","sheet_x":39,"sheet_y":35,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F90F-1F3FF","non_qualified":null,"image":"1f90f-1f3ff.png","sheet_x":39,"sheet_y":36,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"ZIPPER-MOUTH FACE","unified":"1F910","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f910.png","sheet_x":39,"sheet_y":37,"short_name":"zipper_mouth_face","short_names":["zipper_mouth_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-neutral-skeptical","sort_order":37,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MONEY-MOUTH FACE","unified":"1F911","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f911.png","sheet_x":39,"sheet_y":38,"short_name":"money_mouth_face","short_names":["money_mouth_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-tongue","sort_order":29,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE WITH THERMOMETER","unified":"1F912","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f912.png","sheet_x":39,"sheet_y":39,"short_name":"face_with_thermometer","short_names":["face_with_thermometer"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-unwell","sort_order":59,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NERD FACE","unified":"1F913","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f913.png","sheet_x":39,"sheet_y":40,"short_name":"nerd_face","short_names":["nerd_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-glasses","sort_order":74,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"THINKING FACE","unified":"1F914","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f914.png","sheet_x":39,"sheet_y":41,"short_name":"thinking_face","short_names":["thinking_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-hand","sort_order":35,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE WITH HEAD-BANDAGE","unified":"1F915","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f915.png","sheet_x":39,"sheet_y":42,"short_name":"face_with_head_bandage","short_names":["face_with_head_bandage"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-unwell","sort_order":60,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ROBOT FACE","unified":"1F916","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f916.png","sheet_x":39,"sheet_y":43,"short_name":"robot_face","short_names":["robot_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-costume","sort_order":117,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HUGGING FACE","unified":"1F917","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f917.png","sheet_x":39,"sheet_y":44,"short_name":"hugging_face","short_names":["hugging_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-hand","sort_order":30,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SIGN OF THE HORNS","unified":"1F918","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f918.png","sheet_x":39,"sheet_y":45,"short_name":"the_horns","short_names":["the_horns","sign_of_the_horns"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-partial","sort_order":187,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F918-1F3FB","non_qualified":null,"image":"1f918-1f3fb.png","sheet_x":39,"sheet_y":46,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F918-1F3FC","non_qualified":null,"image":"1f918-1f3fc.png","sheet_x":39,"sheet_y":47,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F918-1F3FD","non_qualified":null,"image":"1f918-1f3fd.png","sheet_x":39,"sheet_y":48,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F918-1F3FE","non_qualified":null,"image":"1f918-1f3fe.png","sheet_x":39,"sheet_y":49,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F918-1F3FF","non_qualified":null,"image":"1f918-1f3ff.png","sheet_x":39,"sheet_y":50,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"CALL ME HAND","unified":"1F919","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f919.png","sheet_x":39,"sheet_y":51,"short_name":"call_me_hand","short_names":["call_me_hand"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-partial","sort_order":188,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F919-1F3FB","non_qualified":null,"image":"1f919-1f3fb.png","sheet_x":39,"sheet_y":52,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F919-1F3FC","non_qualified":null,"image":"1f919-1f3fc.png","sheet_x":39,"sheet_y":53,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F919-1F3FD","non_qualified":null,"image":"1f919-1f3fd.png","sheet_x":39,"sheet_y":54,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F919-1F3FE","non_qualified":null,"image":"1f919-1f3fe.png","sheet_x":39,"sheet_y":55,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F919-1F3FF","non_qualified":null,"image":"1f919-1f3ff.png","sheet_x":39,"sheet_y":56,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"RAISED BACK OF HAND","unified":"1F91A","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f91a.png","sheet_x":39,"sheet_y":57,"short_name":"raised_back_of_hand","short_names":["raised_back_of_hand"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-open","sort_order":170,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F91A-1F3FB","non_qualified":null,"image":"1f91a-1f3fb.png","sheet_x":39,"sheet_y":58,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F91A-1F3FC","non_qualified":null,"image":"1f91a-1f3fc.png","sheet_x":39,"sheet_y":59,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F91A-1F3FD","non_qualified":null,"image":"1f91a-1f3fd.png","sheet_x":39,"sheet_y":60,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F91A-1F3FE","non_qualified":null,"image":"1f91a-1f3fe.png","sheet_x":39,"sheet_y":61,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F91A-1F3FF","non_qualified":null,"image":"1f91a-1f3ff.png","sheet_x":40,"sheet_y":0,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"LEFT-FACING FIST","unified":"1F91B","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f91b.png","sheet_x":40,"sheet_y":1,"short_name":"left-facing_fist","short_names":["left-facing_fist"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-closed","sort_order":200,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F91B-1F3FB","non_qualified":null,"image":"1f91b-1f3fb.png","sheet_x":40,"sheet_y":2,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F91B-1F3FC","non_qualified":null,"image":"1f91b-1f3fc.png","sheet_x":40,"sheet_y":3,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F91B-1F3FD","non_qualified":null,"image":"1f91b-1f3fd.png","sheet_x":40,"sheet_y":4,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F91B-1F3FE","non_qualified":null,"image":"1f91b-1f3fe.png","sheet_x":40,"sheet_y":5,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F91B-1F3FF","non_qualified":null,"image":"1f91b-1f3ff.png","sheet_x":40,"sheet_y":6,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"RIGHT-FACING FIST","unified":"1F91C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f91c.png","sheet_x":40,"sheet_y":7,"short_name":"right-facing_fist","short_names":["right-facing_fist"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-closed","sort_order":201,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F91C-1F3FB","non_qualified":null,"image":"1f91c-1f3fb.png","sheet_x":40,"sheet_y":8,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F91C-1F3FC","non_qualified":null,"image":"1f91c-1f3fc.png","sheet_x":40,"sheet_y":9,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F91C-1F3FD","non_qualified":null,"image":"1f91c-1f3fd.png","sheet_x":40,"sheet_y":10,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F91C-1F3FE","non_qualified":null,"image":"1f91c-1f3fe.png","sheet_x":40,"sheet_y":11,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F91C-1F3FF","non_qualified":null,"image":"1f91c-1f3ff.png","sheet_x":40,"sheet_y":12,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"HANDSHAKE","unified":"1F91D","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f91d.png","sheet_x":40,"sheet_y":13,"short_name":"handshake","short_names":["handshake"],"text":null,"texts":null,"category":"People & Body","subcategory":"hands","sort_order":207,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F91D-1F3FB","non_qualified":null,"image":"1f91d-1f3fb.png","sheet_x":40,"sheet_y":14,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F91D-1F3FC","non_qualified":null,"image":"1f91d-1f3fc.png","sheet_x":40,"sheet_y":15,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F91D-1F3FD","non_qualified":null,"image":"1f91d-1f3fd.png","sheet_x":40,"sheet_y":16,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F91D-1F3FE","non_qualified":null,"image":"1f91d-1f3fe.png","sheet_x":40,"sheet_y":17,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F91D-1F3FF","non_qualified":null,"image":"1f91d-1f3ff.png","sheet_x":40,"sheet_y":18,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FC":{"unified":"1FAF1-1F3FB-200D-1FAF2-1F3FC","non_qualified":null,"image":"1faf1-1f3fb-200d-1faf2-1f3fc.png","sheet_x":40,"sheet_y":19,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FD":{"unified":"1FAF1-1F3FB-200D-1FAF2-1F3FD","non_qualified":null,"image":"1faf1-1f3fb-200d-1faf2-1f3fd.png","sheet_x":40,"sheet_y":20,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FE":{"unified":"1FAF1-1F3FB-200D-1FAF2-1F3FE","non_qualified":null,"image":"1faf1-1f3fb-200d-1faf2-1f3fe.png","sheet_x":40,"sheet_y":21,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FF":{"unified":"1FAF1-1F3FB-200D-1FAF2-1F3FF","non_qualified":null,"image":"1faf1-1f3fb-200d-1faf2-1f3ff.png","sheet_x":40,"sheet_y":22,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FB":{"unified":"1FAF1-1F3FC-200D-1FAF2-1F3FB","non_qualified":null,"image":"1faf1-1f3fc-200d-1faf2-1f3fb.png","sheet_x":40,"sheet_y":23,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FD":{"unified":"1FAF1-1F3FC-200D-1FAF2-1F3FD","non_qualified":null,"image":"1faf1-1f3fc-200d-1faf2-1f3fd.png","sheet_x":40,"sheet_y":24,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FE":{"unified":"1FAF1-1F3FC-200D-1FAF2-1F3FE","non_qualified":null,"image":"1faf1-1f3fc-200d-1faf2-1f3fe.png","sheet_x":40,"sheet_y":25,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FF":{"unified":"1FAF1-1F3FC-200D-1FAF2-1F3FF","non_qualified":null,"image":"1faf1-1f3fc-200d-1faf2-1f3ff.png","sheet_x":40,"sheet_y":26,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FB":{"unified":"1FAF1-1F3FD-200D-1FAF2-1F3FB","non_qualified":null,"image":"1faf1-1f3fd-200d-1faf2-1f3fb.png","sheet_x":40,"sheet_y":27,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FC":{"unified":"1FAF1-1F3FD-200D-1FAF2-1F3FC","non_qualified":null,"image":"1faf1-1f3fd-200d-1faf2-1f3fc.png","sheet_x":40,"sheet_y":28,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FE":{"unified":"1FAF1-1F3FD-200D-1FAF2-1F3FE","non_qualified":null,"image":"1faf1-1f3fd-200d-1faf2-1f3fe.png","sheet_x":40,"sheet_y":29,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FF":{"unified":"1FAF1-1F3FD-200D-1FAF2-1F3FF","non_qualified":null,"image":"1faf1-1f3fd-200d-1faf2-1f3ff.png","sheet_x":40,"sheet_y":30,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FB":{"unified":"1FAF1-1F3FE-200D-1FAF2-1F3FB","non_qualified":null,"image":"1faf1-1f3fe-200d-1faf2-1f3fb.png","sheet_x":40,"sheet_y":31,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FC":{"unified":"1FAF1-1F3FE-200D-1FAF2-1F3FC","non_qualified":null,"image":"1faf1-1f3fe-200d-1faf2-1f3fc.png","sheet_x":40,"sheet_y":32,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FD":{"unified":"1FAF1-1F3FE-200D-1FAF2-1F3FD","non_qualified":null,"image":"1faf1-1f3fe-200d-1faf2-1f3fd.png","sheet_x":40,"sheet_y":33,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FF":{"unified":"1FAF1-1F3FE-200D-1FAF2-1F3FF","non_qualified":null,"image":"1faf1-1f3fe-200d-1faf2-1f3ff.png","sheet_x":40,"sheet_y":34,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FB":{"unified":"1FAF1-1F3FF-200D-1FAF2-1F3FB","non_qualified":null,"image":"1faf1-1f3ff-200d-1faf2-1f3fb.png","sheet_x":40,"sheet_y":35,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FC":{"unified":"1FAF1-1F3FF-200D-1FAF2-1F3FC","non_qualified":null,"image":"1faf1-1f3ff-200d-1faf2-1f3fc.png","sheet_x":40,"sheet_y":36,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FD":{"unified":"1FAF1-1F3FF-200D-1FAF2-1F3FD","non_qualified":null,"image":"1faf1-1f3ff-200d-1faf2-1f3fd.png","sheet_x":40,"sheet_y":37,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FE":{"unified":"1FAF1-1F3FF-200D-1FAF2-1F3FE","non_qualified":null,"image":"1faf1-1f3ff-200d-1faf2-1f3fe.png","sheet_x":40,"sheet_y":38,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"HAND WITH INDEX AND MIDDLE FINGERS CROSSED","unified":"1F91E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f91e.png","sheet_x":40,"sheet_y":39,"short_name":"crossed_fingers","short_names":["crossed_fingers","hand_with_index_and_middle_fingers_crossed"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-partial","sort_order":184,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F91E-1F3FB","non_qualified":null,"image":"1f91e-1f3fb.png","sheet_x":40,"sheet_y":40,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F91E-1F3FC","non_qualified":null,"image":"1f91e-1f3fc.png","sheet_x":40,"sheet_y":41,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F91E-1F3FD","non_qualified":null,"image":"1f91e-1f3fd.png","sheet_x":40,"sheet_y":42,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F91E-1F3FE","non_qualified":null,"image":"1f91e-1f3fe.png","sheet_x":40,"sheet_y":43,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F91E-1F3FF","non_qualified":null,"image":"1f91e-1f3ff.png","sheet_x":40,"sheet_y":44,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"I LOVE YOU HAND SIGN","unified":"1F91F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f91f.png","sheet_x":40,"sheet_y":45,"short_name":"i_love_you_hand_sign","short_names":["i_love_you_hand_sign"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-partial","sort_order":186,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F91F-1F3FB","non_qualified":null,"image":"1f91f-1f3fb.png","sheet_x":40,"sheet_y":46,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F91F-1F3FC","non_qualified":null,"image":"1f91f-1f3fc.png","sheet_x":40,"sheet_y":47,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F91F-1F3FD","non_qualified":null,"image":"1f91f-1f3fd.png","sheet_x":40,"sheet_y":48,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F91F-1F3FE","non_qualified":null,"image":"1f91f-1f3fe.png","sheet_x":40,"sheet_y":49,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F91F-1F3FF","non_qualified":null,"image":"1f91f-1f3ff.png","sheet_x":40,"sheet_y":50,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"FACE WITH COWBOY HAT","unified":"1F920","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f920.png","sheet_x":40,"sheet_y":51,"short_name":"face_with_cowboy_hat","short_names":["face_with_cowboy_hat"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-hat","sort_order":70,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOWN FACE","unified":"1F921","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f921.png","sheet_x":40,"sheet_y":52,"short_name":"clown_face","short_names":["clown_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-costume","sort_order":111,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NAUSEATED FACE","unified":"1F922","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f922.png","sheet_x":40,"sheet_y":53,"short_name":"nauseated_face","short_names":["nauseated_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-unwell","sort_order":61,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ROLLING ON THE FLOOR LAUGHING","unified":"1F923","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f923.png","sheet_x":40,"sheet_y":54,"short_name":"rolling_on_the_floor_laughing","short_names":["rolling_on_the_floor_laughing"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-smiling","sort_order":7,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DROOLING FACE","unified":"1F924","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f924.png","sheet_x":40,"sheet_y":55,"short_name":"drooling_face","short_names":["drooling_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-sleepy","sort_order":56,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LYING FACE","unified":"1F925","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f925.png","sheet_x":40,"sheet_y":56,"short_name":"lying_face","short_names":["lying_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-neutral-skeptical","sort_order":49,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WOMAN FACEPALMING","unified":"1F926-200D-2640-FE0F","non_qualified":"1F926-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f926-200d-2640-fe0f.png","sheet_x":40,"sheet_y":57,"short_name":"woman-facepalming","short_names":["woman-facepalming"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":284,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F926-1F3FB-200D-2640-FE0F","non_qualified":"1F926-1F3FB-200D-2640","image":"1f926-1f3fb-200d-2640-fe0f.png","sheet_x":40,"sheet_y":58,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F926-1F3FC-200D-2640-FE0F","non_qualified":"1F926-1F3FC-200D-2640","image":"1f926-1f3fc-200d-2640-fe0f.png","sheet_x":40,"sheet_y":59,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F926-1F3FD-200D-2640-FE0F","non_qualified":"1F926-1F3FD-200D-2640","image":"1f926-1f3fd-200d-2640-fe0f.png","sheet_x":40,"sheet_y":60,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F926-1F3FE-200D-2640-FE0F","non_qualified":"1F926-1F3FE-200D-2640","image":"1f926-1f3fe-200d-2640-fe0f.png","sheet_x":40,"sheet_y":61,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F926-1F3FF-200D-2640-FE0F","non_qualified":"1F926-1F3FF-200D-2640","image":"1f926-1f3ff-200d-2640-fe0f.png","sheet_x":41,"sheet_y":0,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN FACEPALMING","unified":"1F926-200D-2642-FE0F","non_qualified":"1F926-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f926-200d-2642-fe0f.png","sheet_x":41,"sheet_y":1,"short_name":"man-facepalming","short_names":["man-facepalming"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":283,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F926-1F3FB-200D-2642-FE0F","non_qualified":"1F926-1F3FB-200D-2642","image":"1f926-1f3fb-200d-2642-fe0f.png","sheet_x":41,"sheet_y":2,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F926-1F3FC-200D-2642-FE0F","non_qualified":"1F926-1F3FC-200D-2642","image":"1f926-1f3fc-200d-2642-fe0f.png","sheet_x":41,"sheet_y":3,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F926-1F3FD-200D-2642-FE0F","non_qualified":"1F926-1F3FD-200D-2642","image":"1f926-1f3fd-200d-2642-fe0f.png","sheet_x":41,"sheet_y":4,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F926-1F3FE-200D-2642-FE0F","non_qualified":"1F926-1F3FE-200D-2642","image":"1f926-1f3fe-200d-2642-fe0f.png","sheet_x":41,"sheet_y":5,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F926-1F3FF-200D-2642-FE0F","non_qualified":"1F926-1F3FF-200D-2642","image":"1f926-1f3ff-200d-2642-fe0f.png","sheet_x":41,"sheet_y":6,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"FACE PALM","unified":"1F926","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f926.png","sheet_x":41,"sheet_y":7,"short_name":"face_palm","short_names":["face_palm"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":282,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F926-1F3FB","non_qualified":null,"image":"1f926-1f3fb.png","sheet_x":41,"sheet_y":8,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F926-1F3FC","non_qualified":null,"image":"1f926-1f3fc.png","sheet_x":41,"sheet_y":9,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F926-1F3FD","non_qualified":null,"image":"1f926-1f3fd.png","sheet_x":41,"sheet_y":10,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F926-1F3FE","non_qualified":null,"image":"1f926-1f3fe.png","sheet_x":41,"sheet_y":11,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F926-1F3FF","non_qualified":null,"image":"1f926-1f3ff.png","sheet_x":41,"sheet_y":12,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"SNEEZING FACE","unified":"1F927","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f927.png","sheet_x":41,"sheet_y":13,"short_name":"sneezing_face","short_names":["sneezing_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-unwell","sort_order":63,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE WITH ONE EYEBROW RAISED","unified":"1F928","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f928.png","sheet_x":41,"sheet_y":14,"short_name":"face_with_raised_eyebrow","short_names":["face_with_raised_eyebrow","face_with_one_eyebrow_raised"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-neutral-skeptical","sort_order":38,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GRINNING FACE WITH STAR EYES","unified":"1F929","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f929.png","sheet_x":41,"sheet_y":15,"short_name":"star-struck","short_names":["star-struck","grinning_face_with_star_eyes"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-affection","sort_order":17,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GRINNING FACE WITH ONE LARGE AND ONE SMALL EYE","unified":"1F92A","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f92a.png","sheet_x":41,"sheet_y":16,"short_name":"zany_face","short_names":["zany_face","grinning_face_with_one_large_and_one_small_eye"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-tongue","sort_order":27,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE WITH FINGER COVERING CLOSED LIPS","unified":"1F92B","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f92b.png","sheet_x":41,"sheet_y":17,"short_name":"shushing_face","short_names":["shushing_face","face_with_finger_covering_closed_lips"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-hand","sort_order":34,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SERIOUS FACE WITH SYMBOLS COVERING MOUTH","unified":"1F92C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f92c.png","sheet_x":41,"sheet_y":18,"short_name":"face_with_symbols_on_mouth","short_names":["face_with_symbols_on_mouth","serious_face_with_symbols_covering_mouth"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-negative","sort_order":105,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SMILING FACE WITH SMILING EYES AND HAND COVERING MOUTH","unified":"1F92D","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f92d.png","sheet_x":41,"sheet_y":19,"short_name":"face_with_hand_over_mouth","short_names":["face_with_hand_over_mouth","smiling_face_with_smiling_eyes_and_hand_covering_mouth"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-hand","sort_order":31,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE WITH OPEN MOUTH VOMITING","unified":"1F92E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f92e.png","sheet_x":41,"sheet_y":20,"short_name":"face_vomiting","short_names":["face_vomiting","face_with_open_mouth_vomiting"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-unwell","sort_order":62,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SHOCKED FACE WITH EXPLODING HEAD","unified":"1F92F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f92f.png","sheet_x":41,"sheet_y":21,"short_name":"exploding_head","short_names":["exploding_head","shocked_face_with_exploding_head"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-unwell","sort_order":69,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PREGNANT WOMAN","unified":"1F930","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f930.png","sheet_x":41,"sheet_y":22,"short_name":"pregnant_woman","short_names":["pregnant_woman"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":363,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F930-1F3FB","non_qualified":null,"image":"1f930-1f3fb.png","sheet_x":41,"sheet_y":23,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F930-1F3FC","non_qualified":null,"image":"1f930-1f3fc.png","sheet_x":41,"sheet_y":24,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F930-1F3FD","non_qualified":null,"image":"1f930-1f3fd.png","sheet_x":41,"sheet_y":25,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F930-1F3FE","non_qualified":null,"image":"1f930-1f3fe.png","sheet_x":41,"sheet_y":26,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F930-1F3FF","non_qualified":null,"image":"1f930-1f3ff.png","sheet_x":41,"sheet_y":27,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"BREAST-FEEDING","unified":"1F931","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f931.png","sheet_x":41,"sheet_y":28,"short_name":"breast-feeding","short_names":["breast-feeding"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":366,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F931-1F3FB","non_qualified":null,"image":"1f931-1f3fb.png","sheet_x":41,"sheet_y":29,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F931-1F3FC","non_qualified":null,"image":"1f931-1f3fc.png","sheet_x":41,"sheet_y":30,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F931-1F3FD","non_qualified":null,"image":"1f931-1f3fd.png","sheet_x":41,"sheet_y":31,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F931-1F3FE","non_qualified":null,"image":"1f931-1f3fe.png","sheet_x":41,"sheet_y":32,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F931-1F3FF","non_qualified":null,"image":"1f931-1f3ff.png","sheet_x":41,"sheet_y":33,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"PALMS UP TOGETHER","unified":"1F932","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f932.png","sheet_x":41,"sheet_y":34,"short_name":"palms_up_together","short_names":["palms_up_together"],"text":null,"texts":null,"category":"People & Body","subcategory":"hands","sort_order":206,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F932-1F3FB","non_qualified":null,"image":"1f932-1f3fb.png","sheet_x":41,"sheet_y":35,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F932-1F3FC","non_qualified":null,"image":"1f932-1f3fc.png","sheet_x":41,"sheet_y":36,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F932-1F3FD","non_qualified":null,"image":"1f932-1f3fd.png","sheet_x":41,"sheet_y":37,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F932-1F3FE","non_qualified":null,"image":"1f932-1f3fe.png","sheet_x":41,"sheet_y":38,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F932-1F3FF","non_qualified":null,"image":"1f932-1f3ff.png","sheet_x":41,"sheet_y":39,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"SELFIE","unified":"1F933","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f933.png","sheet_x":41,"sheet_y":40,"short_name":"selfie","short_names":["selfie"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-prop","sort_order":211,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F933-1F3FB","non_qualified":null,"image":"1f933-1f3fb.png","sheet_x":41,"sheet_y":41,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F933-1F3FC","non_qualified":null,"image":"1f933-1f3fc.png","sheet_x":41,"sheet_y":42,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F933-1F3FD","non_qualified":null,"image":"1f933-1f3fd.png","sheet_x":41,"sheet_y":43,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F933-1F3FE","non_qualified":null,"image":"1f933-1f3fe.png","sheet_x":41,"sheet_y":44,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F933-1F3FF","non_qualified":null,"image":"1f933-1f3ff.png","sheet_x":41,"sheet_y":45,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"PRINCE","unified":"1F934","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f934.png","sheet_x":41,"sheet_y":46,"short_name":"prince","short_names":["prince"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":350,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F934-1F3FB","non_qualified":null,"image":"1f934-1f3fb.png","sheet_x":41,"sheet_y":47,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F934-1F3FC","non_qualified":null,"image":"1f934-1f3fc.png","sheet_x":41,"sheet_y":48,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F934-1F3FD","non_qualified":null,"image":"1f934-1f3fd.png","sheet_x":41,"sheet_y":49,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F934-1F3FE","non_qualified":null,"image":"1f934-1f3fe.png","sheet_x":41,"sheet_y":50,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F934-1F3FF","non_qualified":null,"image":"1f934-1f3ff.png","sheet_x":41,"sheet_y":51,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN IN TUXEDO","unified":"1F935-200D-2640-FE0F","non_qualified":"1F935-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f935-200d-2640-fe0f.png","sheet_x":41,"sheet_y":52,"short_name":"woman_in_tuxedo","short_names":["woman_in_tuxedo"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":359,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F935-1F3FB-200D-2640-FE0F","non_qualified":"1F935-1F3FB-200D-2640","image":"1f935-1f3fb-200d-2640-fe0f.png","sheet_x":41,"sheet_y":53,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F935-1F3FC-200D-2640-FE0F","non_qualified":"1F935-1F3FC-200D-2640","image":"1f935-1f3fc-200d-2640-fe0f.png","sheet_x":41,"sheet_y":54,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F935-1F3FD-200D-2640-FE0F","non_qualified":"1F935-1F3FD-200D-2640","image":"1f935-1f3fd-200d-2640-fe0f.png","sheet_x":41,"sheet_y":55,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F935-1F3FE-200D-2640-FE0F","non_qualified":"1F935-1F3FE-200D-2640","image":"1f935-1f3fe-200d-2640-fe0f.png","sheet_x":41,"sheet_y":56,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F935-1F3FF-200D-2640-FE0F","non_qualified":"1F935-1F3FF-200D-2640","image":"1f935-1f3ff-200d-2640-fe0f.png","sheet_x":41,"sheet_y":57,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN IN TUXEDO","unified":"1F935-200D-2642-FE0F","non_qualified":"1F935-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f935-200d-2642-fe0f.png","sheet_x":41,"sheet_y":58,"short_name":"man_in_tuxedo","short_names":["man_in_tuxedo"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":358,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F935-1F3FB-200D-2642-FE0F","non_qualified":"1F935-1F3FB-200D-2642","image":"1f935-1f3fb-200d-2642-fe0f.png","sheet_x":41,"sheet_y":59,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F935-1F3FC-200D-2642-FE0F","non_qualified":"1F935-1F3FC-200D-2642","image":"1f935-1f3fc-200d-2642-fe0f.png","sheet_x":41,"sheet_y":60,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F935-1F3FD-200D-2642-FE0F","non_qualified":"1F935-1F3FD-200D-2642","image":"1f935-1f3fd-200d-2642-fe0f.png","sheet_x":41,"sheet_y":61,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F935-1F3FE-200D-2642-FE0F","non_qualified":"1F935-1F3FE-200D-2642","image":"1f935-1f3fe-200d-2642-fe0f.png","sheet_x":42,"sheet_y":0,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F935-1F3FF-200D-2642-FE0F","non_qualified":"1F935-1F3FF-200D-2642","image":"1f935-1f3ff-200d-2642-fe0f.png","sheet_x":42,"sheet_y":1,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN IN TUXEDO","unified":"1F935","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f935.png","sheet_x":42,"sheet_y":2,"short_name":"person_in_tuxedo","short_names":["person_in_tuxedo"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":357,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F935-1F3FB","non_qualified":null,"image":"1f935-1f3fb.png","sheet_x":42,"sheet_y":3,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F935-1F3FC","non_qualified":null,"image":"1f935-1f3fc.png","sheet_x":42,"sheet_y":4,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F935-1F3FD","non_qualified":null,"image":"1f935-1f3fd.png","sheet_x":42,"sheet_y":5,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F935-1F3FE","non_qualified":null,"image":"1f935-1f3fe.png","sheet_x":42,"sheet_y":6,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F935-1F3FF","non_qualified":null,"image":"1f935-1f3ff.png","sheet_x":42,"sheet_y":7,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MOTHER CHRISTMAS","unified":"1F936","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f936.png","sheet_x":42,"sheet_y":8,"short_name":"mrs_claus","short_names":["mrs_claus","mother_christmas"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":372,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F936-1F3FB","non_qualified":null,"image":"1f936-1f3fb.png","sheet_x":42,"sheet_y":9,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F936-1F3FC","non_qualified":null,"image":"1f936-1f3fc.png","sheet_x":42,"sheet_y":10,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F936-1F3FD","non_qualified":null,"image":"1f936-1f3fd.png","sheet_x":42,"sheet_y":11,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F936-1F3FE","non_qualified":null,"image":"1f936-1f3fe.png","sheet_x":42,"sheet_y":12,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F936-1F3FF","non_qualified":null,"image":"1f936-1f3ff.png","sheet_x":42,"sheet_y":13,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN SHRUGGING","unified":"1F937-200D-2640-FE0F","non_qualified":"1F937-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f937-200d-2640-fe0f.png","sheet_x":42,"sheet_y":14,"short_name":"woman-shrugging","short_names":["woman-shrugging"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":287,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F937-1F3FB-200D-2640-FE0F","non_qualified":"1F937-1F3FB-200D-2640","image":"1f937-1f3fb-200d-2640-fe0f.png","sheet_x":42,"sheet_y":15,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F937-1F3FC-200D-2640-FE0F","non_qualified":"1F937-1F3FC-200D-2640","image":"1f937-1f3fc-200d-2640-fe0f.png","sheet_x":42,"sheet_y":16,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F937-1F3FD-200D-2640-FE0F","non_qualified":"1F937-1F3FD-200D-2640","image":"1f937-1f3fd-200d-2640-fe0f.png","sheet_x":42,"sheet_y":17,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F937-1F3FE-200D-2640-FE0F","non_qualified":"1F937-1F3FE-200D-2640","image":"1f937-1f3fe-200d-2640-fe0f.png","sheet_x":42,"sheet_y":18,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F937-1F3FF-200D-2640-FE0F","non_qualified":"1F937-1F3FF-200D-2640","image":"1f937-1f3ff-200d-2640-fe0f.png","sheet_x":42,"sheet_y":19,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN SHRUGGING","unified":"1F937-200D-2642-FE0F","non_qualified":"1F937-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f937-200d-2642-fe0f.png","sheet_x":42,"sheet_y":20,"short_name":"man-shrugging","short_names":["man-shrugging"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":286,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F937-1F3FB-200D-2642-FE0F","non_qualified":"1F937-1F3FB-200D-2642","image":"1f937-1f3fb-200d-2642-fe0f.png","sheet_x":42,"sheet_y":21,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F937-1F3FC-200D-2642-FE0F","non_qualified":"1F937-1F3FC-200D-2642","image":"1f937-1f3fc-200d-2642-fe0f.png","sheet_x":42,"sheet_y":22,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F937-1F3FD-200D-2642-FE0F","non_qualified":"1F937-1F3FD-200D-2642","image":"1f937-1f3fd-200d-2642-fe0f.png","sheet_x":42,"sheet_y":23,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F937-1F3FE-200D-2642-FE0F","non_qualified":"1F937-1F3FE-200D-2642","image":"1f937-1f3fe-200d-2642-fe0f.png","sheet_x":42,"sheet_y":24,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F937-1F3FF-200D-2642-FE0F","non_qualified":"1F937-1F3FF-200D-2642","image":"1f937-1f3ff-200d-2642-fe0f.png","sheet_x":42,"sheet_y":25,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"SHRUG","unified":"1F937","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f937.png","sheet_x":42,"sheet_y":26,"short_name":"shrug","short_names":["shrug"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":285,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F937-1F3FB","non_qualified":null,"image":"1f937-1f3fb.png","sheet_x":42,"sheet_y":27,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F937-1F3FC","non_qualified":null,"image":"1f937-1f3fc.png","sheet_x":42,"sheet_y":28,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F937-1F3FD","non_qualified":null,"image":"1f937-1f3fd.png","sheet_x":42,"sheet_y":29,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F937-1F3FE","non_qualified":null,"image":"1f937-1f3fe.png","sheet_x":42,"sheet_y":30,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F937-1F3FF","non_qualified":null,"image":"1f937-1f3ff.png","sheet_x":42,"sheet_y":31,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN CARTWHEELING","unified":"1F938-200D-2640-FE0F","non_qualified":"1F938-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f938-200d-2640-fe0f.png","sheet_x":42,"sheet_y":32,"short_name":"woman-cartwheeling","short_names":["woman-cartwheeling"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":489,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F938-1F3FB-200D-2640-FE0F","non_qualified":"1F938-1F3FB-200D-2640","image":"1f938-1f3fb-200d-2640-fe0f.png","sheet_x":42,"sheet_y":33,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F938-1F3FC-200D-2640-FE0F","non_qualified":"1F938-1F3FC-200D-2640","image":"1f938-1f3fc-200d-2640-fe0f.png","sheet_x":42,"sheet_y":34,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F938-1F3FD-200D-2640-FE0F","non_qualified":"1F938-1F3FD-200D-2640","image":"1f938-1f3fd-200d-2640-fe0f.png","sheet_x":42,"sheet_y":35,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F938-1F3FE-200D-2640-FE0F","non_qualified":"1F938-1F3FE-200D-2640","image":"1f938-1f3fe-200d-2640-fe0f.png","sheet_x":42,"sheet_y":36,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F938-1F3FF-200D-2640-FE0F","non_qualified":"1F938-1F3FF-200D-2640","image":"1f938-1f3ff-200d-2640-fe0f.png","sheet_x":42,"sheet_y":37,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN CARTWHEELING","unified":"1F938-200D-2642-FE0F","non_qualified":"1F938-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f938-200d-2642-fe0f.png","sheet_x":42,"sheet_y":38,"short_name":"man-cartwheeling","short_names":["man-cartwheeling"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":488,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F938-1F3FB-200D-2642-FE0F","non_qualified":"1F938-1F3FB-200D-2642","image":"1f938-1f3fb-200d-2642-fe0f.png","sheet_x":42,"sheet_y":39,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F938-1F3FC-200D-2642-FE0F","non_qualified":"1F938-1F3FC-200D-2642","image":"1f938-1f3fc-200d-2642-fe0f.png","sheet_x":42,"sheet_y":40,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F938-1F3FD-200D-2642-FE0F","non_qualified":"1F938-1F3FD-200D-2642","image":"1f938-1f3fd-200d-2642-fe0f.png","sheet_x":42,"sheet_y":41,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F938-1F3FE-200D-2642-FE0F","non_qualified":"1F938-1F3FE-200D-2642","image":"1f938-1f3fe-200d-2642-fe0f.png","sheet_x":42,"sheet_y":42,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F938-1F3FF-200D-2642-FE0F","non_qualified":"1F938-1F3FF-200D-2642","image":"1f938-1f3ff-200d-2642-fe0f.png","sheet_x":42,"sheet_y":43,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"PERSON DOING CARTWHEEL","unified":"1F938","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f938.png","sheet_x":42,"sheet_y":44,"short_name":"person_doing_cartwheel","short_names":["person_doing_cartwheel"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":487,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F938-1F3FB","non_qualified":null,"image":"1f938-1f3fb.png","sheet_x":42,"sheet_y":45,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F938-1F3FC","non_qualified":null,"image":"1f938-1f3fc.png","sheet_x":42,"sheet_y":46,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F938-1F3FD","non_qualified":null,"image":"1f938-1f3fd.png","sheet_x":42,"sheet_y":47,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F938-1F3FE","non_qualified":null,"image":"1f938-1f3fe.png","sheet_x":42,"sheet_y":48,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F938-1F3FF","non_qualified":null,"image":"1f938-1f3ff.png","sheet_x":42,"sheet_y":49,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN JUGGLING","unified":"1F939-200D-2640-FE0F","non_qualified":"1F939-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f939-200d-2640-fe0f.png","sheet_x":42,"sheet_y":50,"short_name":"woman-juggling","short_names":["woman-juggling"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":501,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F939-1F3FB-200D-2640-FE0F","non_qualified":"1F939-1F3FB-200D-2640","image":"1f939-1f3fb-200d-2640-fe0f.png","sheet_x":42,"sheet_y":51,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F939-1F3FC-200D-2640-FE0F","non_qualified":"1F939-1F3FC-200D-2640","image":"1f939-1f3fc-200d-2640-fe0f.png","sheet_x":42,"sheet_y":52,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F939-1F3FD-200D-2640-FE0F","non_qualified":"1F939-1F3FD-200D-2640","image":"1f939-1f3fd-200d-2640-fe0f.png","sheet_x":42,"sheet_y":53,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F939-1F3FE-200D-2640-FE0F","non_qualified":"1F939-1F3FE-200D-2640","image":"1f939-1f3fe-200d-2640-fe0f.png","sheet_x":42,"sheet_y":54,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F939-1F3FF-200D-2640-FE0F","non_qualified":"1F939-1F3FF-200D-2640","image":"1f939-1f3ff-200d-2640-fe0f.png","sheet_x":42,"sheet_y":55,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN JUGGLING","unified":"1F939-200D-2642-FE0F","non_qualified":"1F939-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f939-200d-2642-fe0f.png","sheet_x":42,"sheet_y":56,"short_name":"man-juggling","short_names":["man-juggling"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":500,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F939-1F3FB-200D-2642-FE0F","non_qualified":"1F939-1F3FB-200D-2642","image":"1f939-1f3fb-200d-2642-fe0f.png","sheet_x":42,"sheet_y":57,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F939-1F3FC-200D-2642-FE0F","non_qualified":"1F939-1F3FC-200D-2642","image":"1f939-1f3fc-200d-2642-fe0f.png","sheet_x":42,"sheet_y":58,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F939-1F3FD-200D-2642-FE0F","non_qualified":"1F939-1F3FD-200D-2642","image":"1f939-1f3fd-200d-2642-fe0f.png","sheet_x":42,"sheet_y":59,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F939-1F3FE-200D-2642-FE0F","non_qualified":"1F939-1F3FE-200D-2642","image":"1f939-1f3fe-200d-2642-fe0f.png","sheet_x":42,"sheet_y":60,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F939-1F3FF-200D-2642-FE0F","non_qualified":"1F939-1F3FF-200D-2642","image":"1f939-1f3ff-200d-2642-fe0f.png","sheet_x":42,"sheet_y":61,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"JUGGLING","unified":"1F939","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f939.png","sheet_x":43,"sheet_y":0,"short_name":"juggling","short_names":["juggling"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":499,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F939-1F3FB","non_qualified":null,"image":"1f939-1f3fb.png","sheet_x":43,"sheet_y":1,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F939-1F3FC","non_qualified":null,"image":"1f939-1f3fc.png","sheet_x":43,"sheet_y":2,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F939-1F3FD","non_qualified":null,"image":"1f939-1f3fd.png","sheet_x":43,"sheet_y":3,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F939-1F3FE","non_qualified":null,"image":"1f939-1f3fe.png","sheet_x":43,"sheet_y":4,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F939-1F3FF","non_qualified":null,"image":"1f939-1f3ff.png","sheet_x":43,"sheet_y":5,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"FENCER","unified":"1F93A","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f93a.png","sheet_x":43,"sheet_y":6,"short_name":"fencer","short_names":["fencer"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":459,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WOMEN WRESTLING","unified":"1F93C-200D-2640-FE0F","non_qualified":"1F93C-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f93c-200d-2640-fe0f.png","sheet_x":43,"sheet_y":7,"short_name":"woman-wrestling","short_names":["woman-wrestling"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":492,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MEN WRESTLING","unified":"1F93C-200D-2642-FE0F","non_qualified":"1F93C-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f93c-200d-2642-fe0f.png","sheet_x":43,"sheet_y":8,"short_name":"man-wrestling","short_names":["man-wrestling"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":491,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WRESTLERS","unified":"1F93C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f93c.png","sheet_x":43,"sheet_y":9,"short_name":"wrestlers","short_names":["wrestlers"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":490,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WOMAN PLAYING WATER POLO","unified":"1F93D-200D-2640-FE0F","non_qualified":"1F93D-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f93d-200d-2640-fe0f.png","sheet_x":43,"sheet_y":10,"short_name":"woman-playing-water-polo","short_names":["woman-playing-water-polo"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":495,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F93D-1F3FB-200D-2640-FE0F","non_qualified":"1F93D-1F3FB-200D-2640","image":"1f93d-1f3fb-200d-2640-fe0f.png","sheet_x":43,"sheet_y":11,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F93D-1F3FC-200D-2640-FE0F","non_qualified":"1F93D-1F3FC-200D-2640","image":"1f93d-1f3fc-200d-2640-fe0f.png","sheet_x":43,"sheet_y":12,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F93D-1F3FD-200D-2640-FE0F","non_qualified":"1F93D-1F3FD-200D-2640","image":"1f93d-1f3fd-200d-2640-fe0f.png","sheet_x":43,"sheet_y":13,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F93D-1F3FE-200D-2640-FE0F","non_qualified":"1F93D-1F3FE-200D-2640","image":"1f93d-1f3fe-200d-2640-fe0f.png","sheet_x":43,"sheet_y":14,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F93D-1F3FF-200D-2640-FE0F","non_qualified":"1F93D-1F3FF-200D-2640","image":"1f93d-1f3ff-200d-2640-fe0f.png","sheet_x":43,"sheet_y":15,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN PLAYING WATER POLO","unified":"1F93D-200D-2642-FE0F","non_qualified":"1F93D-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f93d-200d-2642-fe0f.png","sheet_x":43,"sheet_y":16,"short_name":"man-playing-water-polo","short_names":["man-playing-water-polo"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":494,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F93D-1F3FB-200D-2642-FE0F","non_qualified":"1F93D-1F3FB-200D-2642","image":"1f93d-1f3fb-200d-2642-fe0f.png","sheet_x":43,"sheet_y":17,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F93D-1F3FC-200D-2642-FE0F","non_qualified":"1F93D-1F3FC-200D-2642","image":"1f93d-1f3fc-200d-2642-fe0f.png","sheet_x":43,"sheet_y":18,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F93D-1F3FD-200D-2642-FE0F","non_qualified":"1F93D-1F3FD-200D-2642","image":"1f93d-1f3fd-200d-2642-fe0f.png","sheet_x":43,"sheet_y":19,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F93D-1F3FE-200D-2642-FE0F","non_qualified":"1F93D-1F3FE-200D-2642","image":"1f93d-1f3fe-200d-2642-fe0f.png","sheet_x":43,"sheet_y":20,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F93D-1F3FF-200D-2642-FE0F","non_qualified":"1F93D-1F3FF-200D-2642","image":"1f93d-1f3ff-200d-2642-fe0f.png","sheet_x":43,"sheet_y":21,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WATER POLO","unified":"1F93D","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f93d.png","sheet_x":43,"sheet_y":22,"short_name":"water_polo","short_names":["water_polo"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":493,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F93D-1F3FB","non_qualified":null,"image":"1f93d-1f3fb.png","sheet_x":43,"sheet_y":23,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F93D-1F3FC","non_qualified":null,"image":"1f93d-1f3fc.png","sheet_x":43,"sheet_y":24,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F93D-1F3FD","non_qualified":null,"image":"1f93d-1f3fd.png","sheet_x":43,"sheet_y":25,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F93D-1F3FE","non_qualified":null,"image":"1f93d-1f3fe.png","sheet_x":43,"sheet_y":26,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F93D-1F3FF","non_qualified":null,"image":"1f93d-1f3ff.png","sheet_x":43,"sheet_y":27,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN PLAYING HANDBALL","unified":"1F93E-200D-2640-FE0F","non_qualified":"1F93E-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f93e-200d-2640-fe0f.png","sheet_x":43,"sheet_y":28,"short_name":"woman-playing-handball","short_names":["woman-playing-handball"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":498,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F93E-1F3FB-200D-2640-FE0F","non_qualified":"1F93E-1F3FB-200D-2640","image":"1f93e-1f3fb-200d-2640-fe0f.png","sheet_x":43,"sheet_y":29,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F93E-1F3FC-200D-2640-FE0F","non_qualified":"1F93E-1F3FC-200D-2640","image":"1f93e-1f3fc-200d-2640-fe0f.png","sheet_x":43,"sheet_y":30,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F93E-1F3FD-200D-2640-FE0F","non_qualified":"1F93E-1F3FD-200D-2640","image":"1f93e-1f3fd-200d-2640-fe0f.png","sheet_x":43,"sheet_y":31,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F93E-1F3FE-200D-2640-FE0F","non_qualified":"1F93E-1F3FE-200D-2640","image":"1f93e-1f3fe-200d-2640-fe0f.png","sheet_x":43,"sheet_y":32,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F93E-1F3FF-200D-2640-FE0F","non_qualified":"1F93E-1F3FF-200D-2640","image":"1f93e-1f3ff-200d-2640-fe0f.png","sheet_x":43,"sheet_y":33,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN PLAYING HANDBALL","unified":"1F93E-200D-2642-FE0F","non_qualified":"1F93E-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f93e-200d-2642-fe0f.png","sheet_x":43,"sheet_y":34,"short_name":"man-playing-handball","short_names":["man-playing-handball"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":497,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F93E-1F3FB-200D-2642-FE0F","non_qualified":"1F93E-1F3FB-200D-2642","image":"1f93e-1f3fb-200d-2642-fe0f.png","sheet_x":43,"sheet_y":35,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F93E-1F3FC-200D-2642-FE0F","non_qualified":"1F93E-1F3FC-200D-2642","image":"1f93e-1f3fc-200d-2642-fe0f.png","sheet_x":43,"sheet_y":36,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F93E-1F3FD-200D-2642-FE0F","non_qualified":"1F93E-1F3FD-200D-2642","image":"1f93e-1f3fd-200d-2642-fe0f.png","sheet_x":43,"sheet_y":37,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F93E-1F3FE-200D-2642-FE0F","non_qualified":"1F93E-1F3FE-200D-2642","image":"1f93e-1f3fe-200d-2642-fe0f.png","sheet_x":43,"sheet_y":38,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F93E-1F3FF-200D-2642-FE0F","non_qualified":"1F93E-1F3FF-200D-2642","image":"1f93e-1f3ff-200d-2642-fe0f.png","sheet_x":43,"sheet_y":39,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"HANDBALL","unified":"1F93E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f93e.png","sheet_x":43,"sheet_y":40,"short_name":"handball","short_names":["handball"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":496,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F93E-1F3FB","non_qualified":null,"image":"1f93e-1f3fb.png","sheet_x":43,"sheet_y":41,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F93E-1F3FC","non_qualified":null,"image":"1f93e-1f3fc.png","sheet_x":43,"sheet_y":42,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F93E-1F3FD","non_qualified":null,"image":"1f93e-1f3fd.png","sheet_x":43,"sheet_y":43,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F93E-1F3FE","non_qualified":null,"image":"1f93e-1f3fe.png","sheet_x":43,"sheet_y":44,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F93E-1F3FF","non_qualified":null,"image":"1f93e-1f3ff.png","sheet_x":43,"sheet_y":45,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"DIVING MASK","unified":"1F93F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f93f.png","sheet_x":43,"sheet_y":46,"short_name":"diving_mask","short_names":["diving_mask"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1114,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WILTED FLOWER","unified":"1F940","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f940.png","sheet_x":43,"sheet_y":47,"short_name":"wilted_flower","short_names":["wilted_flower"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-flower","sort_order":690,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DRUM WITH DRUMSTICKS","unified":"1F941","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f941.png","sheet_x":43,"sheet_y":48,"short_name":"drum_with_drumsticks","short_names":["drum_with_drumsticks"],"text":null,"texts":null,"category":"Objects","subcategory":"musical-instrument","sort_order":1222,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLINKING GLASSES","unified":"1F942","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f942.png","sheet_x":43,"sheet_y":49,"short_name":"clinking_glasses","short_names":["clinking_glasses"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"drink","sort_order":832,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TUMBLER GLASS","unified":"1F943","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f943.png","sheet_x":43,"sheet_y":50,"short_name":"tumbler_glass","short_names":["tumbler_glass"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"drink","sort_order":833,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SPOON","unified":"1F944","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f944.png","sheet_x":43,"sheet_y":51,"short_name":"spoon","short_names":["spoon"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"dishware","sort_order":843,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GOAL NET","unified":"1F945","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f945.png","sheet_x":43,"sheet_y":52,"short_name":"goal_net","short_names":["goal_net"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1110,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FIRST PLACE MEDAL","unified":"1F947","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f947.png","sheet_x":43,"sheet_y":53,"short_name":"first_place_medal","short_names":["first_place_medal"],"text":null,"texts":null,"category":"Activities","subcategory":"award-medal","sort_order":1089,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SECOND PLACE MEDAL","unified":"1F948","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f948.png","sheet_x":43,"sheet_y":54,"short_name":"second_place_medal","short_names":["second_place_medal"],"text":null,"texts":null,"category":"Activities","subcategory":"award-medal","sort_order":1090,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"THIRD PLACE MEDAL","unified":"1F949","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f949.png","sheet_x":43,"sheet_y":55,"short_name":"third_place_medal","short_names":["third_place_medal"],"text":null,"texts":null,"category":"Activities","subcategory":"award-medal","sort_order":1091,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BOXING GLOVE","unified":"1F94A","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f94a.png","sheet_x":43,"sheet_y":56,"short_name":"boxing_glove","short_names":["boxing_glove"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1108,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MARTIAL ARTS UNIFORM","unified":"1F94B","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f94b.png","sheet_x":43,"sheet_y":57,"short_name":"martial_arts_uniform","short_names":["martial_arts_uniform"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1109,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CURLING STONE","unified":"1F94C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f94c.png","sheet_x":43,"sheet_y":58,"short_name":"curling_stone","short_names":["curling_stone"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1118,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LACROSSE STICK AND BALL","unified":"1F94D","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f94d.png","sheet_x":43,"sheet_y":59,"short_name":"lacrosse","short_names":["lacrosse"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1105,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SOFTBALL","unified":"1F94E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f94e.png","sheet_x":43,"sheet_y":60,"short_name":"softball","short_names":["softball"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1094,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FLYING DISC","unified":"1F94F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f94f.png","sheet_x":43,"sheet_y":61,"short_name":"flying_disc","short_names":["flying_disc"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1100,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CROISSANT","unified":"1F950","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f950.png","sheet_x":44,"sheet_y":0,"short_name":"croissant","short_names":["croissant"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":751,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"AVOCADO","unified":"1F951","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f951.png","sheet_x":44,"sheet_y":1,"short_name":"avocado","short_names":["avocado"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-vegetable","sort_order":732,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CUCUMBER","unified":"1F952","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f952.png","sheet_x":44,"sheet_y":2,"short_name":"cucumber","short_names":["cucumber"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-vegetable","sort_order":739,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BACON","unified":"1F953","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f953.png","sheet_x":44,"sheet_y":3,"short_name":"bacon","short_names":["bacon"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":762,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"POTATO","unified":"1F954","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f954.png","sheet_x":44,"sheet_y":4,"short_name":"potato","short_names":["potato"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-vegetable","sort_order":734,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CARROT","unified":"1F955","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f955.png","sheet_x":44,"sheet_y":5,"short_name":"carrot","short_names":["carrot"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-vegetable","sort_order":735,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BAGUETTE BREAD","unified":"1F956","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f956.png","sheet_x":44,"sheet_y":6,"short_name":"baguette_bread","short_names":["baguette_bread"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":752,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GREEN SALAD","unified":"1F957","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f957.png","sheet_x":44,"sheet_y":7,"short_name":"green_salad","short_names":["green_salad"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":779,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SHALLOW PAN OF FOOD","unified":"1F958","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f958.png","sheet_x":44,"sheet_y":8,"short_name":"shallow_pan_of_food","short_names":["shallow_pan_of_food"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":775,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"STUFFED FLATBREAD","unified":"1F959","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f959.png","sheet_x":44,"sheet_y":9,"short_name":"stuffed_flatbread","short_names":["stuffed_flatbread"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":771,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EGG","unified":"1F95A","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f95a.png","sheet_x":44,"sheet_y":10,"short_name":"egg","short_names":["egg"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":773,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GLASS OF MILK","unified":"1F95B","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f95b.png","sheet_x":44,"sheet_y":11,"short_name":"glass_of_milk","short_names":["glass_of_milk"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"drink","sort_order":821,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PEANUTS","unified":"1F95C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f95c.png","sheet_x":44,"sheet_y":12,"short_name":"peanuts","short_names":["peanuts"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-vegetable","sort_order":744,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"KIWIFRUIT","unified":"1F95D","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f95d.png","sheet_x":44,"sheet_y":13,"short_name":"kiwifruit","short_names":["kiwifruit"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-fruit","sort_order":728,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PANCAKES","unified":"1F95E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f95e.png","sheet_x":44,"sheet_y":14,"short_name":"pancakes","short_names":["pancakes"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":756,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DUMPLING","unified":"1F95F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f95f.png","sheet_x":44,"sheet_y":15,"short_name":"dumpling","short_names":["dumpling"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-asian","sort_order":798,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FORTUNE COOKIE","unified":"1F960","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f960.png","sheet_x":44,"sheet_y":16,"short_name":"fortune_cookie","short_names":["fortune_cookie"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-asian","sort_order":799,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TAKEOUT BOX","unified":"1F961","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f961.png","sheet_x":44,"sheet_y":17,"short_name":"takeout_box","short_names":["takeout_box"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-asian","sort_order":800,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CHOPSTICKS","unified":"1F962","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f962.png","sheet_x":44,"sheet_y":18,"short_name":"chopsticks","short_names":["chopsticks"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"dishware","sort_order":840,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BOWL WITH SPOON","unified":"1F963","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f963.png","sheet_x":44,"sheet_y":19,"short_name":"bowl_with_spoon","short_names":["bowl_with_spoon"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":778,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CUP WITH STRAW","unified":"1F964","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f964.png","sheet_x":44,"sheet_y":20,"short_name":"cup_with_straw","short_names":["cup_with_straw"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"drink","sort_order":835,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"COCONUT","unified":"1F965","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f965.png","sheet_x":44,"sheet_y":21,"short_name":"coconut","short_names":["coconut"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-fruit","sort_order":731,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BROCCOLI","unified":"1F966","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f966.png","sheet_x":44,"sheet_y":22,"short_name":"broccoli","short_names":["broccoli"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-vegetable","sort_order":741,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PIE","unified":"1F967","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f967.png","sheet_x":44,"sheet_y":23,"short_name":"pie","short_names":["pie"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-sweet","sort_order":814,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PRETZEL","unified":"1F968","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f968.png","sheet_x":44,"sheet_y":24,"short_name":"pretzel","short_names":["pretzel"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":754,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CUT OF MEAT","unified":"1F969","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f969.png","sheet_x":44,"sheet_y":25,"short_name":"cut_of_meat","short_names":["cut_of_meat"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":761,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SANDWICH","unified":"1F96A","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f96a.png","sheet_x":44,"sheet_y":26,"short_name":"sandwich","short_names":["sandwich"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":767,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CANNED FOOD","unified":"1F96B","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f96b.png","sheet_x":44,"sheet_y":27,"short_name":"canned_food","short_names":["canned_food"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":783,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LEAFY GREEN","unified":"1F96C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f96c.png","sheet_x":44,"sheet_y":28,"short_name":"leafy_green","short_names":["leafy_green"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-vegetable","sort_order":740,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MANGO","unified":"1F96D","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f96d.png","sheet_x":44,"sheet_y":29,"short_name":"mango","short_names":["mango"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-fruit","sort_order":720,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MOON CAKE","unified":"1F96E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f96e.png","sheet_x":44,"sheet_y":30,"short_name":"moon_cake","short_names":["moon_cake"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-asian","sort_order":796,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BAGEL","unified":"1F96F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f96f.png","sheet_x":44,"sheet_y":31,"short_name":"bagel","short_names":["bagel"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":755,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SMILING FACE WITH SMILING EYES AND THREE HEARTS","unified":"1F970","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f970.png","sheet_x":44,"sheet_y":32,"short_name":"smiling_face_with_3_hearts","short_names":["smiling_face_with_3_hearts"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-affection","sort_order":15,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"YAWNING FACE","unified":"1F971","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f971.png","sheet_x":44,"sheet_y":33,"short_name":"yawning_face","short_names":["yawning_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":101,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SMILING FACE WITH TEAR","unified":"1F972","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f972.png","sheet_x":44,"sheet_y":34,"short_name":"smiling_face_with_tear","short_names":["smiling_face_with_tear"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-affection","sort_order":23,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE WITH PARTY HORN AND PARTY HAT","unified":"1F973","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f973.png","sheet_x":44,"sheet_y":35,"short_name":"partying_face","short_names":["partying_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-hat","sort_order":71,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE WITH UNEVEN EYES AND WAVY MOUTH","unified":"1F974","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f974.png","sheet_x":44,"sheet_y":36,"short_name":"woozy_face","short_names":["woozy_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-unwell","sort_order":66,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"OVERHEATED FACE","unified":"1F975","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f975.png","sheet_x":44,"sheet_y":37,"short_name":"hot_face","short_names":["hot_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-unwell","sort_order":64,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FREEZING FACE","unified":"1F976","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f976.png","sheet_x":44,"sheet_y":38,"short_name":"cold_face","short_names":["cold_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-unwell","sort_order":65,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NINJA","unified":"1F977","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f977.png","sheet_x":44,"sheet_y":39,"short_name":"ninja","short_names":["ninja"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":345,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F977-1F3FB","non_qualified":null,"image":"1f977-1f3fb.png","sheet_x":44,"sheet_y":40,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F977-1F3FC","non_qualified":null,"image":"1f977-1f3fc.png","sheet_x":44,"sheet_y":41,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F977-1F3FD","non_qualified":null,"image":"1f977-1f3fd.png","sheet_x":44,"sheet_y":42,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F977-1F3FE","non_qualified":null,"image":"1f977-1f3fe.png","sheet_x":44,"sheet_y":43,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F977-1F3FF","non_qualified":null,"image":"1f977-1f3ff.png","sheet_x":44,"sheet_y":44,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"DISGUISED FACE","unified":"1F978","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f978.png","sheet_x":44,"sheet_y":45,"short_name":"disguised_face","short_names":["disguised_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-hat","sort_order":72,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE HOLDING BACK TEARS","unified":"1F979","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f979.png","sheet_x":44,"sheet_y":46,"short_name":"face_holding_back_tears","short_names":["face_holding_back_tears"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":86,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE WITH PLEADING EYES","unified":"1F97A","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f97a.png","sheet_x":44,"sheet_y":47,"short_name":"pleading_face","short_names":["pleading_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":85,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SARI","unified":"1F97B","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f97b.png","sheet_x":44,"sheet_y":48,"short_name":"sari","short_names":["sari"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1164,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LAB COAT","unified":"1F97C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f97c.png","sheet_x":44,"sheet_y":49,"short_name":"lab_coat","short_names":["lab_coat"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1153,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GOGGLES","unified":"1F97D","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f97d.png","sheet_x":44,"sheet_y":50,"short_name":"goggles","short_names":["goggles"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1152,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HIKING BOOT","unified":"1F97E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f97e.png","sheet_x":44,"sheet_y":51,"short_name":"hiking_boot","short_names":["hiking_boot"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1179,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FLAT SHOE","unified":"1F97F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f97f.png","sheet_x":44,"sheet_y":52,"short_name":"womans_flat_shoe","short_names":["womans_flat_shoe"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1180,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CRAB","unified":"1F980","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f980.png","sheet_x":44,"sheet_y":53,"short_name":"crab","short_names":["crab"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-marine","sort_order":801,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LION FACE","unified":"1F981","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f981.png","sheet_x":44,"sheet_y":54,"short_name":"lion_face","short_names":["lion_face"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":574,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SCORPION","unified":"1F982","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f982.png","sheet_x":44,"sheet_y":55,"short_name":"scorpion","short_names":["scorpion"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bug","sort_order":679,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TURKEY","unified":"1F983","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f983.png","sheet_x":44,"sheet_y":56,"short_name":"turkey","short_names":["turkey"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bird","sort_order":625,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"UNICORN FACE","unified":"1F984","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f984.png","sheet_x":44,"sheet_y":57,"short_name":"unicorn_face","short_names":["unicorn_face"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":582,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EAGLE","unified":"1F985","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f985.png","sheet_x":44,"sheet_y":58,"short_name":"eagle","short_names":["eagle"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bird","sort_order":634,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DUCK","unified":"1F986","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f986.png","sheet_x":44,"sheet_y":59,"short_name":"duck","short_names":["duck"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bird","sort_order":635,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BAT","unified":"1F987","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f987.png","sheet_x":44,"sheet_y":60,"short_name":"bat","short_names":["bat"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":614,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SHARK","unified":"1F988","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f988.png","sheet_x":44,"sheet_y":61,"short_name":"shark","short_names":["shark"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-marine","sort_order":663,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"OWL","unified":"1F989","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f989.png","sheet_x":45,"sheet_y":0,"short_name":"owl","short_names":["owl"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bird","sort_order":637,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FOX FACE","unified":"1F98A","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f98a.png","sheet_x":45,"sheet_y":1,"short_name":"fox_face","short_names":["fox_face"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":569,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BUTTERFLY","unified":"1F98B","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f98b.png","sheet_x":45,"sheet_y":2,"short_name":"butterfly","short_names":["butterfly"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bug","sort_order":669,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DEER","unified":"1F98C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f98c.png","sheet_x":45,"sheet_y":3,"short_name":"deer","short_names":["deer"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":584,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GORILLA","unified":"1F98D","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f98d.png","sheet_x":45,"sheet_y":4,"short_name":"gorilla","short_names":["gorilla"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":561,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LIZARD","unified":"1F98E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f98e.png","sheet_x":45,"sheet_y":5,"short_name":"lizard","short_names":["lizard"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-reptile","sort_order":650,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RHINOCEROS","unified":"1F98F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f98f.png","sheet_x":45,"sheet_y":6,"short_name":"rhinoceros","short_names":["rhinoceros"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":603,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SHRIMP","unified":"1F990","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f990.png","sheet_x":45,"sheet_y":7,"short_name":"shrimp","short_names":["shrimp"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-marine","sort_order":803,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SQUID","unified":"1F991","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f991.png","sheet_x":45,"sheet_y":8,"short_name":"squid","short_names":["squid"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-marine","sort_order":804,"added_in":"3.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GIRAFFE FACE","unified":"1F992","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f992.png","sheet_x":45,"sheet_y":9,"short_name":"giraffe_face","short_names":["giraffe_face"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":600,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ZEBRA FACE","unified":"1F993","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f993.png","sheet_x":45,"sheet_y":10,"short_name":"zebra_face","short_names":["zebra_face"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":583,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HEDGEHOG","unified":"1F994","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f994.png","sheet_x":45,"sheet_y":11,"short_name":"hedgehog","short_names":["hedgehog"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":613,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SAUROPOD","unified":"1F995","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f995.png","sheet_x":45,"sheet_y":12,"short_name":"sauropod","short_names":["sauropod"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-reptile","sort_order":654,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"T-REX","unified":"1F996","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f996.png","sheet_x":45,"sheet_y":13,"short_name":"t-rex","short_names":["t-rex"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-reptile","sort_order":655,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CRICKET","unified":"1F997","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f997.png","sheet_x":45,"sheet_y":14,"short_name":"cricket","short_names":["cricket"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bug","sort_order":675,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"KANGAROO","unified":"1F998","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f998.png","sheet_x":45,"sheet_y":15,"short_name":"kangaroo","short_names":["kangaroo"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":622,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LLAMA","unified":"1F999","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f999.png","sheet_x":45,"sheet_y":16,"short_name":"llama","short_names":["llama"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":599,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PEACOCK","unified":"1F99A","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f99a.png","sheet_x":45,"sheet_y":17,"short_name":"peacock","short_names":["peacock"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bird","sort_order":641,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HIPPOPOTAMUS","unified":"1F99B","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f99b.png","sheet_x":45,"sheet_y":18,"short_name":"hippopotamus","short_names":["hippopotamus"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":604,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PARROT","unified":"1F99C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f99c.png","sheet_x":45,"sheet_y":19,"short_name":"parrot","short_names":["parrot"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bird","sort_order":642,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RACCOON","unified":"1F99D","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f99d.png","sheet_x":45,"sheet_y":20,"short_name":"raccoon","short_names":["raccoon"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":570,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LOBSTER","unified":"1F99E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f99e.png","sheet_x":45,"sheet_y":21,"short_name":"lobster","short_names":["lobster"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-marine","sort_order":802,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MOSQUITO","unified":"1F99F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f99f.png","sheet_x":45,"sheet_y":22,"short_name":"mosquito","short_names":["mosquito"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bug","sort_order":680,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MICROBE","unified":"1F9A0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9a0.png","sheet_x":45,"sheet_y":23,"short_name":"microbe","short_names":["microbe"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bug","sort_order":683,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BADGER","unified":"1F9A1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9a1.png","sheet_x":45,"sheet_y":24,"short_name":"badger","short_names":["badger"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":623,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SWAN","unified":"1F9A2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9a2.png","sheet_x":45,"sheet_y":25,"short_name":"swan","short_names":["swan"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bird","sort_order":636,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MAMMOTH","unified":"1F9A3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9a3.png","sheet_x":45,"sheet_y":26,"short_name":"mammoth","short_names":["mammoth"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":602,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DODO","unified":"1F9A4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9a4.png","sheet_x":45,"sheet_y":27,"short_name":"dodo","short_names":["dodo"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bird","sort_order":638,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SLOTH","unified":"1F9A5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9a5.png","sheet_x":45,"sheet_y":28,"short_name":"sloth","short_names":["sloth"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":619,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"OTTER","unified":"1F9A6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9a6.png","sheet_x":45,"sheet_y":29,"short_name":"otter","short_names":["otter"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":620,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ORANGUTAN","unified":"1F9A7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9a7.png","sheet_x":45,"sheet_y":30,"short_name":"orangutan","short_names":["orangutan"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":562,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SKUNK","unified":"1F9A8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9a8.png","sheet_x":45,"sheet_y":31,"short_name":"skunk","short_names":["skunk"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":621,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FLAMINGO","unified":"1F9A9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9a9.png","sheet_x":45,"sheet_y":32,"short_name":"flamingo","short_names":["flamingo"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bird","sort_order":640,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"OYSTER","unified":"1F9AA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9aa.png","sheet_x":45,"sheet_y":33,"short_name":"oyster","short_names":["oyster"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-marine","sort_order":805,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BEAVER","unified":"1F9AB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9ab.png","sheet_x":45,"sheet_y":34,"short_name":"beaver","short_names":["beaver"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":612,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BISON","unified":"1F9AC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9ac.png","sheet_x":45,"sheet_y":35,"short_name":"bison","short_names":["bison"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":585,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SEAL","unified":"1F9AD","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9ad.png","sheet_x":45,"sheet_y":36,"short_name":"seal","short_names":["seal"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-marine","sort_order":659,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GUIDE DOG","unified":"1F9AE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9ae.png","sheet_x":45,"sheet_y":37,"short_name":"guide_dog","short_names":["guide_dog"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":565,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PROBING CANE","unified":"1F9AF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9af.png","sheet_x":45,"sheet_y":38,"short_name":"probing_cane","short_names":["probing_cane"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1356,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BONE","unified":"1F9B4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9b4.png","sheet_x":45,"sheet_y":39,"short_name":"bone","short_names":["bone"],"text":null,"texts":null,"category":"People & Body","subcategory":"body-parts","sort_order":224,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LEG","unified":"1F9B5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9b5.png","sheet_x":45,"sheet_y":40,"short_name":"leg","short_names":["leg"],"text":null,"texts":null,"category":"People & Body","subcategory":"body-parts","sort_order":215,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9B5-1F3FB","non_qualified":null,"image":"1f9b5-1f3fb.png","sheet_x":45,"sheet_y":41,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9B5-1F3FC","non_qualified":null,"image":"1f9b5-1f3fc.png","sheet_x":45,"sheet_y":42,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9B5-1F3FD","non_qualified":null,"image":"1f9b5-1f3fd.png","sheet_x":45,"sheet_y":43,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9B5-1F3FE","non_qualified":null,"image":"1f9b5-1f3fe.png","sheet_x":45,"sheet_y":44,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9B5-1F3FF","non_qualified":null,"image":"1f9b5-1f3ff.png","sheet_x":45,"sheet_y":45,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"FOOT","unified":"1F9B6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9b6.png","sheet_x":45,"sheet_y":46,"short_name":"foot","short_names":["foot"],"text":null,"texts":null,"category":"People & Body","subcategory":"body-parts","sort_order":216,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9B6-1F3FB","non_qualified":null,"image":"1f9b6-1f3fb.png","sheet_x":45,"sheet_y":47,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9B6-1F3FC","non_qualified":null,"image":"1f9b6-1f3fc.png","sheet_x":45,"sheet_y":48,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9B6-1F3FD","non_qualified":null,"image":"1f9b6-1f3fd.png","sheet_x":45,"sheet_y":49,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9B6-1F3FE","non_qualified":null,"image":"1f9b6-1f3fe.png","sheet_x":45,"sheet_y":50,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9B6-1F3FF","non_qualified":null,"image":"1f9b6-1f3ff.png","sheet_x":45,"sheet_y":51,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"TOOTH","unified":"1F9B7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9b7.png","sheet_x":45,"sheet_y":52,"short_name":"tooth","short_names":["tooth"],"text":null,"texts":null,"category":"People & Body","subcategory":"body-parts","sort_order":223,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WOMAN SUPERHERO","unified":"1F9B8-200D-2640-FE0F","non_qualified":"1F9B8-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9b8-200d-2640-fe0f.png","sheet_x":45,"sheet_y":53,"short_name":"female_superhero","short_names":["female_superhero"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":376,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9B8-1F3FB-200D-2640-FE0F","non_qualified":"1F9B8-1F3FB-200D-2640","image":"1f9b8-1f3fb-200d-2640-fe0f.png","sheet_x":45,"sheet_y":54,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9B8-1F3FC-200D-2640-FE0F","non_qualified":"1F9B8-1F3FC-200D-2640","image":"1f9b8-1f3fc-200d-2640-fe0f.png","sheet_x":45,"sheet_y":55,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9B8-1F3FD-200D-2640-FE0F","non_qualified":"1F9B8-1F3FD-200D-2640","image":"1f9b8-1f3fd-200d-2640-fe0f.png","sheet_x":45,"sheet_y":56,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9B8-1F3FE-200D-2640-FE0F","non_qualified":"1F9B8-1F3FE-200D-2640","image":"1f9b8-1f3fe-200d-2640-fe0f.png","sheet_x":45,"sheet_y":57,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9B8-1F3FF-200D-2640-FE0F","non_qualified":"1F9B8-1F3FF-200D-2640","image":"1f9b8-1f3ff-200d-2640-fe0f.png","sheet_x":45,"sheet_y":58,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN SUPERHERO","unified":"1F9B8-200D-2642-FE0F","non_qualified":"1F9B8-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9b8-200d-2642-fe0f.png","sheet_x":45,"sheet_y":59,"short_name":"male_superhero","short_names":["male_superhero"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":375,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9B8-1F3FB-200D-2642-FE0F","non_qualified":"1F9B8-1F3FB-200D-2642","image":"1f9b8-1f3fb-200d-2642-fe0f.png","sheet_x":45,"sheet_y":60,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9B8-1F3FC-200D-2642-FE0F","non_qualified":"1F9B8-1F3FC-200D-2642","image":"1f9b8-1f3fc-200d-2642-fe0f.png","sheet_x":45,"sheet_y":61,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9B8-1F3FD-200D-2642-FE0F","non_qualified":"1F9B8-1F3FD-200D-2642","image":"1f9b8-1f3fd-200d-2642-fe0f.png","sheet_x":46,"sheet_y":0,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9B8-1F3FE-200D-2642-FE0F","non_qualified":"1F9B8-1F3FE-200D-2642","image":"1f9b8-1f3fe-200d-2642-fe0f.png","sheet_x":46,"sheet_y":1,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9B8-1F3FF-200D-2642-FE0F","non_qualified":"1F9B8-1F3FF-200D-2642","image":"1f9b8-1f3ff-200d-2642-fe0f.png","sheet_x":46,"sheet_y":2,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"SUPERHERO","unified":"1F9B8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9b8.png","sheet_x":46,"sheet_y":3,"short_name":"superhero","short_names":["superhero"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":374,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9B8-1F3FB","non_qualified":null,"image":"1f9b8-1f3fb.png","sheet_x":46,"sheet_y":4,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9B8-1F3FC","non_qualified":null,"image":"1f9b8-1f3fc.png","sheet_x":46,"sheet_y":5,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9B8-1F3FD","non_qualified":null,"image":"1f9b8-1f3fd.png","sheet_x":46,"sheet_y":6,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9B8-1F3FE","non_qualified":null,"image":"1f9b8-1f3fe.png","sheet_x":46,"sheet_y":7,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9B8-1F3FF","non_qualified":null,"image":"1f9b8-1f3ff.png","sheet_x":46,"sheet_y":8,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN SUPERVILLAIN","unified":"1F9B9-200D-2640-FE0F","non_qualified":"1F9B9-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9b9-200d-2640-fe0f.png","sheet_x":46,"sheet_y":9,"short_name":"female_supervillain","short_names":["female_supervillain"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":379,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9B9-1F3FB-200D-2640-FE0F","non_qualified":"1F9B9-1F3FB-200D-2640","image":"1f9b9-1f3fb-200d-2640-fe0f.png","sheet_x":46,"sheet_y":10,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9B9-1F3FC-200D-2640-FE0F","non_qualified":"1F9B9-1F3FC-200D-2640","image":"1f9b9-1f3fc-200d-2640-fe0f.png","sheet_x":46,"sheet_y":11,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9B9-1F3FD-200D-2640-FE0F","non_qualified":"1F9B9-1F3FD-200D-2640","image":"1f9b9-1f3fd-200d-2640-fe0f.png","sheet_x":46,"sheet_y":12,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9B9-1F3FE-200D-2640-FE0F","non_qualified":"1F9B9-1F3FE-200D-2640","image":"1f9b9-1f3fe-200d-2640-fe0f.png","sheet_x":46,"sheet_y":13,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9B9-1F3FF-200D-2640-FE0F","non_qualified":"1F9B9-1F3FF-200D-2640","image":"1f9b9-1f3ff-200d-2640-fe0f.png","sheet_x":46,"sheet_y":14,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN SUPERVILLAIN","unified":"1F9B9-200D-2642-FE0F","non_qualified":"1F9B9-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9b9-200d-2642-fe0f.png","sheet_x":46,"sheet_y":15,"short_name":"male_supervillain","short_names":["male_supervillain"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":378,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9B9-1F3FB-200D-2642-FE0F","non_qualified":"1F9B9-1F3FB-200D-2642","image":"1f9b9-1f3fb-200d-2642-fe0f.png","sheet_x":46,"sheet_y":16,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9B9-1F3FC-200D-2642-FE0F","non_qualified":"1F9B9-1F3FC-200D-2642","image":"1f9b9-1f3fc-200d-2642-fe0f.png","sheet_x":46,"sheet_y":17,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9B9-1F3FD-200D-2642-FE0F","non_qualified":"1F9B9-1F3FD-200D-2642","image":"1f9b9-1f3fd-200d-2642-fe0f.png","sheet_x":46,"sheet_y":18,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9B9-1F3FE-200D-2642-FE0F","non_qualified":"1F9B9-1F3FE-200D-2642","image":"1f9b9-1f3fe-200d-2642-fe0f.png","sheet_x":46,"sheet_y":19,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9B9-1F3FF-200D-2642-FE0F","non_qualified":"1F9B9-1F3FF-200D-2642","image":"1f9b9-1f3ff-200d-2642-fe0f.png","sheet_x":46,"sheet_y":20,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"SUPERVILLAIN","unified":"1F9B9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9b9.png","sheet_x":46,"sheet_y":21,"short_name":"supervillain","short_names":["supervillain"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":377,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9B9-1F3FB","non_qualified":null,"image":"1f9b9-1f3fb.png","sheet_x":46,"sheet_y":22,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9B9-1F3FC","non_qualified":null,"image":"1f9b9-1f3fc.png","sheet_x":46,"sheet_y":23,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9B9-1F3FD","non_qualified":null,"image":"1f9b9-1f3fd.png","sheet_x":46,"sheet_y":24,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9B9-1F3FE","non_qualified":null,"image":"1f9b9-1f3fe.png","sheet_x":46,"sheet_y":25,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9B9-1F3FF","non_qualified":null,"image":"1f9b9-1f3ff.png","sheet_x":46,"sheet_y":26,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"SAFETY VEST","unified":"1F9BA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9ba.png","sheet_x":46,"sheet_y":27,"short_name":"safety_vest","short_names":["safety_vest"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1154,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EAR WITH HEARING AID","unified":"1F9BB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9bb.png","sheet_x":46,"sheet_y":28,"short_name":"ear_with_hearing_aid","short_names":["ear_with_hearing_aid"],"text":null,"texts":null,"category":"People & Body","subcategory":"body-parts","sort_order":218,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9BB-1F3FB","non_qualified":null,"image":"1f9bb-1f3fb.png","sheet_x":46,"sheet_y":29,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9BB-1F3FC","non_qualified":null,"image":"1f9bb-1f3fc.png","sheet_x":46,"sheet_y":30,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9BB-1F3FD","non_qualified":null,"image":"1f9bb-1f3fd.png","sheet_x":46,"sheet_y":31,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9BB-1F3FE","non_qualified":null,"image":"1f9bb-1f3fe.png","sheet_x":46,"sheet_y":32,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9BB-1F3FF","non_qualified":null,"image":"1f9bb-1f3ff.png","sheet_x":46,"sheet_y":33,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MOTORIZED WHEELCHAIR","unified":"1F9BC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9bc.png","sheet_x":46,"sheet_y":34,"short_name":"motorized_wheelchair","short_names":["motorized_wheelchair"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":946,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MANUAL WHEELCHAIR","unified":"1F9BD","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9bd.png","sheet_x":46,"sheet_y":35,"short_name":"manual_wheelchair","short_names":["manual_wheelchair"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":945,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MECHANICAL ARM","unified":"1F9BE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9be.png","sheet_x":46,"sheet_y":36,"short_name":"mechanical_arm","short_names":["mechanical_arm"],"text":null,"texts":null,"category":"People & Body","subcategory":"body-parts","sort_order":213,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MECHANICAL LEG","unified":"1F9BF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9bf.png","sheet_x":46,"sheet_y":37,"short_name":"mechanical_leg","short_names":["mechanical_leg"],"text":null,"texts":null,"category":"People & Body","subcategory":"body-parts","sort_order":214,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CHEESE WEDGE","unified":"1F9C0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9c0.png","sheet_x":46,"sheet_y":38,"short_name":"cheese_wedge","short_names":["cheese_wedge"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":758,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CUPCAKE","unified":"1F9C1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9c1.png","sheet_x":46,"sheet_y":39,"short_name":"cupcake","short_names":["cupcake"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-sweet","sort_order":813,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SALT SHAKER","unified":"1F9C2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9c2.png","sheet_x":46,"sheet_y":40,"short_name":"salt","short_names":["salt"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":782,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BEVERAGE BOX","unified":"1F9C3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9c3.png","sheet_x":46,"sheet_y":41,"short_name":"beverage_box","short_names":["beverage_box"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"drink","sort_order":837,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GARLIC","unified":"1F9C4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9c4.png","sheet_x":46,"sheet_y":42,"short_name":"garlic","short_names":["garlic"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-vegetable","sort_order":742,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ONION","unified":"1F9C5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9c5.png","sheet_x":46,"sheet_y":43,"short_name":"onion","short_names":["onion"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-vegetable","sort_order":743,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FALAFEL","unified":"1F9C6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9c6.png","sheet_x":46,"sheet_y":44,"short_name":"falafel","short_names":["falafel"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":772,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WAFFLE","unified":"1F9C7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9c7.png","sheet_x":46,"sheet_y":45,"short_name":"waffle","short_names":["waffle"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":757,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BUTTER","unified":"1F9C8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9c8.png","sheet_x":46,"sheet_y":46,"short_name":"butter","short_names":["butter"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":781,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MATE DRINK","unified":"1F9C9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9c9.png","sheet_x":46,"sheet_y":47,"short_name":"mate_drink","short_names":["mate_drink"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"drink","sort_order":838,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ICE CUBE","unified":"1F9CA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9ca.png","sheet_x":46,"sheet_y":48,"short_name":"ice_cube","short_names":["ice_cube"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"drink","sort_order":839,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BUBBLE TEA","unified":"1F9CB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9cb.png","sheet_x":46,"sheet_y":49,"short_name":"bubble_tea","short_names":["bubble_tea"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"drink","sort_order":836,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TROLL","unified":"1F9CC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9cc.png","sheet_x":46,"sheet_y":50,"short_name":"troll","short_names":["troll"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":401,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WOMAN STANDING","unified":"1F9CD-200D-2640-FE0F","non_qualified":"1F9CD-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9cd-200d-2640-fe0f.png","sheet_x":46,"sheet_y":51,"short_name":"woman_standing","short_names":["woman_standing"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":416,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9CD-1F3FB-200D-2640-FE0F","non_qualified":"1F9CD-1F3FB-200D-2640","image":"1f9cd-1f3fb-200d-2640-fe0f.png","sheet_x":46,"sheet_y":52,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9CD-1F3FC-200D-2640-FE0F","non_qualified":"1F9CD-1F3FC-200D-2640","image":"1f9cd-1f3fc-200d-2640-fe0f.png","sheet_x":46,"sheet_y":53,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9CD-1F3FD-200D-2640-FE0F","non_qualified":"1F9CD-1F3FD-200D-2640","image":"1f9cd-1f3fd-200d-2640-fe0f.png","sheet_x":46,"sheet_y":54,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9CD-1F3FE-200D-2640-FE0F","non_qualified":"1F9CD-1F3FE-200D-2640","image":"1f9cd-1f3fe-200d-2640-fe0f.png","sheet_x":46,"sheet_y":55,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9CD-1F3FF-200D-2640-FE0F","non_qualified":"1F9CD-1F3FF-200D-2640","image":"1f9cd-1f3ff-200d-2640-fe0f.png","sheet_x":46,"sheet_y":56,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN STANDING","unified":"1F9CD-200D-2642-FE0F","non_qualified":"1F9CD-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9cd-200d-2642-fe0f.png","sheet_x":46,"sheet_y":57,"short_name":"man_standing","short_names":["man_standing"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":415,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9CD-1F3FB-200D-2642-FE0F","non_qualified":"1F9CD-1F3FB-200D-2642","image":"1f9cd-1f3fb-200d-2642-fe0f.png","sheet_x":46,"sheet_y":58,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9CD-1F3FC-200D-2642-FE0F","non_qualified":"1F9CD-1F3FC-200D-2642","image":"1f9cd-1f3fc-200d-2642-fe0f.png","sheet_x":46,"sheet_y":59,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9CD-1F3FD-200D-2642-FE0F","non_qualified":"1F9CD-1F3FD-200D-2642","image":"1f9cd-1f3fd-200d-2642-fe0f.png","sheet_x":46,"sheet_y":60,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9CD-1F3FE-200D-2642-FE0F","non_qualified":"1F9CD-1F3FE-200D-2642","image":"1f9cd-1f3fe-200d-2642-fe0f.png","sheet_x":46,"sheet_y":61,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9CD-1F3FF-200D-2642-FE0F","non_qualified":"1F9CD-1F3FF-200D-2642","image":"1f9cd-1f3ff-200d-2642-fe0f.png","sheet_x":47,"sheet_y":0,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"STANDING PERSON","unified":"1F9CD","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9cd.png","sheet_x":47,"sheet_y":1,"short_name":"standing_person","short_names":["standing_person"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":414,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9CD-1F3FB","non_qualified":null,"image":"1f9cd-1f3fb.png","sheet_x":47,"sheet_y":2,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9CD-1F3FC","non_qualified":null,"image":"1f9cd-1f3fc.png","sheet_x":47,"sheet_y":3,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9CD-1F3FD","non_qualified":null,"image":"1f9cd-1f3fd.png","sheet_x":47,"sheet_y":4,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9CD-1F3FE","non_qualified":null,"image":"1f9cd-1f3fe.png","sheet_x":47,"sheet_y":5,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9CD-1F3FF","non_qualified":null,"image":"1f9cd-1f3ff.png","sheet_x":47,"sheet_y":6,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN KNEELING","unified":"1F9CE-200D-2640-FE0F","non_qualified":"1F9CE-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9ce-200d-2640-fe0f.png","sheet_x":47,"sheet_y":7,"short_name":"woman_kneeling","short_names":["woman_kneeling"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":419,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9CE-1F3FB-200D-2640-FE0F","non_qualified":"1F9CE-1F3FB-200D-2640","image":"1f9ce-1f3fb-200d-2640-fe0f.png","sheet_x":47,"sheet_y":8,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9CE-1F3FC-200D-2640-FE0F","non_qualified":"1F9CE-1F3FC-200D-2640","image":"1f9ce-1f3fc-200d-2640-fe0f.png","sheet_x":47,"sheet_y":9,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9CE-1F3FD-200D-2640-FE0F","non_qualified":"1F9CE-1F3FD-200D-2640","image":"1f9ce-1f3fd-200d-2640-fe0f.png","sheet_x":47,"sheet_y":10,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9CE-1F3FE-200D-2640-FE0F","non_qualified":"1F9CE-1F3FE-200D-2640","image":"1f9ce-1f3fe-200d-2640-fe0f.png","sheet_x":47,"sheet_y":11,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9CE-1F3FF-200D-2640-FE0F","non_qualified":"1F9CE-1F3FF-200D-2640","image":"1f9ce-1f3ff-200d-2640-fe0f.png","sheet_x":47,"sheet_y":12,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN KNEELING FACING RIGHT","unified":"1F9CE-200D-2640-FE0F-200D-27A1-FE0F","non_qualified":"1F9CE-200D-2640-200D-27A1","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9ce-200d-2640-fe0f-200d-27a1-fe0f.png","sheet_x":47,"sheet_y":13,"short_name":"woman_kneeling_facing_right","short_names":["woman_kneeling_facing_right"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":421,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"1F9CE-1F3FB-200D-2640-FE0F-200D-27A1-FE0F","non_qualified":"1F9CE-1F3FB-200D-2640-200D-27A1","image":"1f9ce-1f3fb-200d-2640-fe0f-200d-27a1-fe0f.png","sheet_x":47,"sheet_y":14,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FC":{"unified":"1F9CE-1F3FC-200D-2640-FE0F-200D-27A1-FE0F","non_qualified":"1F9CE-1F3FC-200D-2640-200D-27A1","image":"1f9ce-1f3fc-200d-2640-fe0f-200d-27a1-fe0f.png","sheet_x":47,"sheet_y":15,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FD":{"unified":"1F9CE-1F3FD-200D-2640-FE0F-200D-27A1-FE0F","non_qualified":"1F9CE-1F3FD-200D-2640-200D-27A1","image":"1f9ce-1f3fd-200d-2640-fe0f-200d-27a1-fe0f.png","sheet_x":47,"sheet_y":16,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FE":{"unified":"1F9CE-1F3FE-200D-2640-FE0F-200D-27A1-FE0F","non_qualified":"1F9CE-1F3FE-200D-2640-200D-27A1","image":"1f9ce-1f3fe-200d-2640-fe0f-200d-27a1-fe0f.png","sheet_x":47,"sheet_y":17,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FF":{"unified":"1F9CE-1F3FF-200D-2640-FE0F-200D-27A1-FE0F","non_qualified":"1F9CE-1F3FF-200D-2640-200D-27A1","image":"1f9ce-1f3ff-200d-2640-fe0f-200d-27a1-fe0f.png","sheet_x":47,"sheet_y":18,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false}}},{"name":"MAN KNEELING","unified":"1F9CE-200D-2642-FE0F","non_qualified":"1F9CE-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9ce-200d-2642-fe0f.png","sheet_x":47,"sheet_y":19,"short_name":"man_kneeling","short_names":["man_kneeling"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":418,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9CE-1F3FB-200D-2642-FE0F","non_qualified":"1F9CE-1F3FB-200D-2642","image":"1f9ce-1f3fb-200d-2642-fe0f.png","sheet_x":47,"sheet_y":20,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9CE-1F3FC-200D-2642-FE0F","non_qualified":"1F9CE-1F3FC-200D-2642","image":"1f9ce-1f3fc-200d-2642-fe0f.png","sheet_x":47,"sheet_y":21,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9CE-1F3FD-200D-2642-FE0F","non_qualified":"1F9CE-1F3FD-200D-2642","image":"1f9ce-1f3fd-200d-2642-fe0f.png","sheet_x":47,"sheet_y":22,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9CE-1F3FE-200D-2642-FE0F","non_qualified":"1F9CE-1F3FE-200D-2642","image":"1f9ce-1f3fe-200d-2642-fe0f.png","sheet_x":47,"sheet_y":23,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9CE-1F3FF-200D-2642-FE0F","non_qualified":"1F9CE-1F3FF-200D-2642","image":"1f9ce-1f3ff-200d-2642-fe0f.png","sheet_x":47,"sheet_y":24,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN KNEELING FACING RIGHT","unified":"1F9CE-200D-2642-FE0F-200D-27A1-FE0F","non_qualified":"1F9CE-200D-2642-200D-27A1","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9ce-200d-2642-fe0f-200d-27a1-fe0f.png","sheet_x":47,"sheet_y":25,"short_name":"man_kneeling_facing_right","short_names":["man_kneeling_facing_right"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":422,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"1F9CE-1F3FB-200D-2642-FE0F-200D-27A1-FE0F","non_qualified":"1F9CE-1F3FB-200D-2642-200D-27A1","image":"1f9ce-1f3fb-200d-2642-fe0f-200d-27a1-fe0f.png","sheet_x":47,"sheet_y":26,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FC":{"unified":"1F9CE-1F3FC-200D-2642-FE0F-200D-27A1-FE0F","non_qualified":"1F9CE-1F3FC-200D-2642-200D-27A1","image":"1f9ce-1f3fc-200d-2642-fe0f-200d-27a1-fe0f.png","sheet_x":47,"sheet_y":27,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FD":{"unified":"1F9CE-1F3FD-200D-2642-FE0F-200D-27A1-FE0F","non_qualified":"1F9CE-1F3FD-200D-2642-200D-27A1","image":"1f9ce-1f3fd-200d-2642-fe0f-200d-27a1-fe0f.png","sheet_x":47,"sheet_y":28,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FE":{"unified":"1F9CE-1F3FE-200D-2642-FE0F-200D-27A1-FE0F","non_qualified":"1F9CE-1F3FE-200D-2642-200D-27A1","image":"1f9ce-1f3fe-200d-2642-fe0f-200d-27a1-fe0f.png","sheet_x":47,"sheet_y":29,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FF":{"unified":"1F9CE-1F3FF-200D-2642-FE0F-200D-27A1-FE0F","non_qualified":"1F9CE-1F3FF-200D-2642-200D-27A1","image":"1f9ce-1f3ff-200d-2642-fe0f-200d-27a1-fe0f.png","sheet_x":47,"sheet_y":30,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false}}},{"name":"PERSON KNEELING FACING RIGHT","unified":"1F9CE-200D-27A1-FE0F","non_qualified":"1F9CE-200D-27A1","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9ce-200d-27a1-fe0f.png","sheet_x":47,"sheet_y":31,"short_name":"person_kneeling_facing_right","short_names":["person_kneeling_facing_right"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":420,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"1F9CE-1F3FB-200D-27A1-FE0F","non_qualified":"1F9CE-1F3FB-200D-27A1","image":"1f9ce-1f3fb-200d-27a1-fe0f.png","sheet_x":47,"sheet_y":32,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FC":{"unified":"1F9CE-1F3FC-200D-27A1-FE0F","non_qualified":"1F9CE-1F3FC-200D-27A1","image":"1f9ce-1f3fc-200d-27a1-fe0f.png","sheet_x":47,"sheet_y":33,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FD":{"unified":"1F9CE-1F3FD-200D-27A1-FE0F","non_qualified":"1F9CE-1F3FD-200D-27A1","image":"1f9ce-1f3fd-200d-27a1-fe0f.png","sheet_x":47,"sheet_y":34,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FE":{"unified":"1F9CE-1F3FE-200D-27A1-FE0F","non_qualified":"1F9CE-1F3FE-200D-27A1","image":"1f9ce-1f3fe-200d-27a1-fe0f.png","sheet_x":47,"sheet_y":35,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FF":{"unified":"1F9CE-1F3FF-200D-27A1-FE0F","non_qualified":"1F9CE-1F3FF-200D-27A1","image":"1f9ce-1f3ff-200d-27a1-fe0f.png","sheet_x":47,"sheet_y":36,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false}}},{"name":"KNEELING PERSON","unified":"1F9CE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9ce.png","sheet_x":47,"sheet_y":37,"short_name":"kneeling_person","short_names":["kneeling_person"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":417,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9CE-1F3FB","non_qualified":null,"image":"1f9ce-1f3fb.png","sheet_x":47,"sheet_y":38,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9CE-1F3FC","non_qualified":null,"image":"1f9ce-1f3fc.png","sheet_x":47,"sheet_y":39,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9CE-1F3FD","non_qualified":null,"image":"1f9ce-1f3fd.png","sheet_x":47,"sheet_y":40,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9CE-1F3FE","non_qualified":null,"image":"1f9ce-1f3fe.png","sheet_x":47,"sheet_y":41,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9CE-1F3FF","non_qualified":null,"image":"1f9ce-1f3ff.png","sheet_x":47,"sheet_y":42,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"DEAF WOMAN","unified":"1F9CF-200D-2640-FE0F","non_qualified":"1F9CF-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9cf-200d-2640-fe0f.png","sheet_x":47,"sheet_y":43,"short_name":"deaf_woman","short_names":["deaf_woman"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":278,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9CF-1F3FB-200D-2640-FE0F","non_qualified":"1F9CF-1F3FB-200D-2640","image":"1f9cf-1f3fb-200d-2640-fe0f.png","sheet_x":47,"sheet_y":44,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9CF-1F3FC-200D-2640-FE0F","non_qualified":"1F9CF-1F3FC-200D-2640","image":"1f9cf-1f3fc-200d-2640-fe0f.png","sheet_x":47,"sheet_y":45,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9CF-1F3FD-200D-2640-FE0F","non_qualified":"1F9CF-1F3FD-200D-2640","image":"1f9cf-1f3fd-200d-2640-fe0f.png","sheet_x":47,"sheet_y":46,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9CF-1F3FE-200D-2640-FE0F","non_qualified":"1F9CF-1F3FE-200D-2640","image":"1f9cf-1f3fe-200d-2640-fe0f.png","sheet_x":47,"sheet_y":47,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9CF-1F3FF-200D-2640-FE0F","non_qualified":"1F9CF-1F3FF-200D-2640","image":"1f9cf-1f3ff-200d-2640-fe0f.png","sheet_x":47,"sheet_y":48,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"DEAF MAN","unified":"1F9CF-200D-2642-FE0F","non_qualified":"1F9CF-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9cf-200d-2642-fe0f.png","sheet_x":47,"sheet_y":49,"short_name":"deaf_man","short_names":["deaf_man"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":277,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9CF-1F3FB-200D-2642-FE0F","non_qualified":"1F9CF-1F3FB-200D-2642","image":"1f9cf-1f3fb-200d-2642-fe0f.png","sheet_x":47,"sheet_y":50,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9CF-1F3FC-200D-2642-FE0F","non_qualified":"1F9CF-1F3FC-200D-2642","image":"1f9cf-1f3fc-200d-2642-fe0f.png","sheet_x":47,"sheet_y":51,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9CF-1F3FD-200D-2642-FE0F","non_qualified":"1F9CF-1F3FD-200D-2642","image":"1f9cf-1f3fd-200d-2642-fe0f.png","sheet_x":47,"sheet_y":52,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9CF-1F3FE-200D-2642-FE0F","non_qualified":"1F9CF-1F3FE-200D-2642","image":"1f9cf-1f3fe-200d-2642-fe0f.png","sheet_x":47,"sheet_y":53,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9CF-1F3FF-200D-2642-FE0F","non_qualified":"1F9CF-1F3FF-200D-2642","image":"1f9cf-1f3ff-200d-2642-fe0f.png","sheet_x":47,"sheet_y":54,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"DEAF PERSON","unified":"1F9CF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9cf.png","sheet_x":47,"sheet_y":55,"short_name":"deaf_person","short_names":["deaf_person"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-gesture","sort_order":276,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9CF-1F3FB","non_qualified":null,"image":"1f9cf-1f3fb.png","sheet_x":47,"sheet_y":56,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9CF-1F3FC","non_qualified":null,"image":"1f9cf-1f3fc.png","sheet_x":47,"sheet_y":57,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9CF-1F3FD","non_qualified":null,"image":"1f9cf-1f3fd.png","sheet_x":47,"sheet_y":58,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9CF-1F3FE","non_qualified":null,"image":"1f9cf-1f3fe.png","sheet_x":47,"sheet_y":59,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9CF-1F3FF","non_qualified":null,"image":"1f9cf-1f3ff.png","sheet_x":47,"sheet_y":60,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"FACE WITH MONOCLE","unified":"1F9D0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d0.png","sheet_x":47,"sheet_y":61,"short_name":"face_with_monocle","short_names":["face_with_monocle"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-glasses","sort_order":75,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FARMER","unified":"1F9D1-200D-1F33E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f33e.png","sheet_x":48,"sheet_y":0,"short_name":"farmer","short_names":["farmer"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":300,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F33E","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f33e.png","sheet_x":48,"sheet_y":1,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F33E","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f33e.png","sheet_x":48,"sheet_y":2,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F33E","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f33e.png","sheet_x":48,"sheet_y":3,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F33E","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f33e.png","sheet_x":48,"sheet_y":4,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F33E","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f33e.png","sheet_x":48,"sheet_y":5,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"COOK","unified":"1F9D1-200D-1F373","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f373.png","sheet_x":48,"sheet_y":6,"short_name":"cook","short_names":["cook"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":303,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F373","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f373.png","sheet_x":48,"sheet_y":7,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F373","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f373.png","sheet_x":48,"sheet_y":8,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F373","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f373.png","sheet_x":48,"sheet_y":9,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F373","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f373.png","sheet_x":48,"sheet_y":10,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F373","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f373.png","sheet_x":48,"sheet_y":11,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"PERSON FEEDING BABY","unified":"1F9D1-200D-1F37C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f37c.png","sheet_x":48,"sheet_y":12,"short_name":"person_feeding_baby","short_names":["person_feeding_baby"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":369,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F37C","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f37c.png","sheet_x":48,"sheet_y":13,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F37C","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f37c.png","sheet_x":48,"sheet_y":14,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F37C","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f37c.png","sheet_x":48,"sheet_y":15,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F37C","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f37c.png","sheet_x":48,"sheet_y":16,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F37C","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f37c.png","sheet_x":48,"sheet_y":17,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MX CLAUS","unified":"1F9D1-200D-1F384","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f384.png","sheet_x":48,"sheet_y":18,"short_name":"mx_claus","short_names":["mx_claus"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":373,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F384","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f384.png","sheet_x":48,"sheet_y":19,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F384","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f384.png","sheet_x":48,"sheet_y":20,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F384","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f384.png","sheet_x":48,"sheet_y":21,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F384","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f384.png","sheet_x":48,"sheet_y":22,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F384","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f384.png","sheet_x":48,"sheet_y":23,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"STUDENT","unified":"1F9D1-200D-1F393","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f393.png","sheet_x":48,"sheet_y":24,"short_name":"student","short_names":["student"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":291,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F393","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f393.png","sheet_x":48,"sheet_y":25,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F393","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f393.png","sheet_x":48,"sheet_y":26,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F393","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f393.png","sheet_x":48,"sheet_y":27,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F393","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f393.png","sheet_x":48,"sheet_y":28,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F393","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f393.png","sheet_x":48,"sheet_y":29,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"SINGER","unified":"1F9D1-200D-1F3A4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f3a4.png","sheet_x":48,"sheet_y":30,"short_name":"singer","short_names":["singer"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":321,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F3A4","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f3a4.png","sheet_x":48,"sheet_y":31,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F3A4","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f3a4.png","sheet_x":48,"sheet_y":32,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F3A4","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f3a4.png","sheet_x":48,"sheet_y":33,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F3A4","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f3a4.png","sheet_x":48,"sheet_y":34,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F3A4","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f3a4.png","sheet_x":48,"sheet_y":35,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"ARTIST","unified":"1F9D1-200D-1F3A8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f3a8.png","sheet_x":48,"sheet_y":36,"short_name":"artist","short_names":["artist"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":324,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F3A8","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f3a8.png","sheet_x":48,"sheet_y":37,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F3A8","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f3a8.png","sheet_x":48,"sheet_y":38,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F3A8","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f3a8.png","sheet_x":48,"sheet_y":39,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F3A8","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f3a8.png","sheet_x":48,"sheet_y":40,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F3A8","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f3a8.png","sheet_x":48,"sheet_y":41,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"TEACHER","unified":"1F9D1-200D-1F3EB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f3eb.png","sheet_x":48,"sheet_y":42,"short_name":"teacher","short_names":["teacher"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":294,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F3EB","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f3eb.png","sheet_x":48,"sheet_y":43,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F3EB","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f3eb.png","sheet_x":48,"sheet_y":44,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F3EB","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f3eb.png","sheet_x":48,"sheet_y":45,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F3EB","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f3eb.png","sheet_x":48,"sheet_y":46,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F3EB","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f3eb.png","sheet_x":48,"sheet_y":47,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"FACTORY WORKER","unified":"1F9D1-200D-1F3ED","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f3ed.png","sheet_x":48,"sheet_y":48,"short_name":"factory_worker","short_names":["factory_worker"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":309,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F3ED","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f3ed.png","sheet_x":48,"sheet_y":49,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F3ED","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f3ed.png","sheet_x":48,"sheet_y":50,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F3ED","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f3ed.png","sheet_x":48,"sheet_y":51,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F3ED","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f3ed.png","sheet_x":48,"sheet_y":52,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F3ED","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f3ed.png","sheet_x":48,"sheet_y":53,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"TECHNOLOGIST","unified":"1F9D1-200D-1F4BB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f4bb.png","sheet_x":48,"sheet_y":54,"short_name":"technologist","short_names":["technologist"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":318,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F4BB","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f4bb.png","sheet_x":48,"sheet_y":55,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F4BB","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f4bb.png","sheet_x":48,"sheet_y":56,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F4BB","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f4bb.png","sheet_x":48,"sheet_y":57,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F4BB","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f4bb.png","sheet_x":48,"sheet_y":58,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F4BB","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f4bb.png","sheet_x":48,"sheet_y":59,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"OFFICE WORKER","unified":"1F9D1-200D-1F4BC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f4bc.png","sheet_x":48,"sheet_y":60,"short_name":"office_worker","short_names":["office_worker"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":312,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F4BC","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f4bc.png","sheet_x":48,"sheet_y":61,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F4BC","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f4bc.png","sheet_x":49,"sheet_y":0,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F4BC","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f4bc.png","sheet_x":49,"sheet_y":1,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F4BC","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f4bc.png","sheet_x":49,"sheet_y":2,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F4BC","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f4bc.png","sheet_x":49,"sheet_y":3,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MECHANIC","unified":"1F9D1-200D-1F527","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f527.png","sheet_x":49,"sheet_y":4,"short_name":"mechanic","short_names":["mechanic"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":306,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F527","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f527.png","sheet_x":49,"sheet_y":5,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F527","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f527.png","sheet_x":49,"sheet_y":6,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F527","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f527.png","sheet_x":49,"sheet_y":7,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F527","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f527.png","sheet_x":49,"sheet_y":8,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F527","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f527.png","sheet_x":49,"sheet_y":9,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"SCIENTIST","unified":"1F9D1-200D-1F52C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f52c.png","sheet_x":49,"sheet_y":10,"short_name":"scientist","short_names":["scientist"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":315,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F52C","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f52c.png","sheet_x":49,"sheet_y":11,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F52C","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f52c.png","sheet_x":49,"sheet_y":12,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F52C","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f52c.png","sheet_x":49,"sheet_y":13,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F52C","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f52c.png","sheet_x":49,"sheet_y":14,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F52C","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f52c.png","sheet_x":49,"sheet_y":15,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"ASTRONAUT","unified":"1F9D1-200D-1F680","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f680.png","sheet_x":49,"sheet_y":16,"short_name":"astronaut","short_names":["astronaut"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":330,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F680","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f680.png","sheet_x":49,"sheet_y":17,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F680","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f680.png","sheet_x":49,"sheet_y":18,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F680","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f680.png","sheet_x":49,"sheet_y":19,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F680","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f680.png","sheet_x":49,"sheet_y":20,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F680","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f680.png","sheet_x":49,"sheet_y":21,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"FIREFIGHTER","unified":"1F9D1-200D-1F692","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f692.png","sheet_x":49,"sheet_y":22,"short_name":"firefighter","short_names":["firefighter"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":333,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F692","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f692.png","sheet_x":49,"sheet_y":23,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F692","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f692.png","sheet_x":49,"sheet_y":24,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F692","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f692.png","sheet_x":49,"sheet_y":25,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F692","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f692.png","sheet_x":49,"sheet_y":26,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F692","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f692.png","sheet_x":49,"sheet_y":27,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"PEOPLE HOLDING HANDS","unified":"1F9D1-200D-1F91D-200D-1F9D1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f91d-200d-1f9d1.png","sheet_x":49,"sheet_y":28,"short_name":"people_holding_hands","short_names":["people_holding_hands"],"text":null,"texts":null,"category":"People & Body","subcategory":"family","sort_order":507,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB-1F3FB":{"unified":"1F9D1-1F3FB-200D-1F91D-200D-1F9D1-1F3FB","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f91d-200d-1f9d1-1f3fb.png","sheet_x":49,"sheet_y":29,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FC":{"unified":"1F9D1-1F3FB-200D-1F91D-200D-1F9D1-1F3FC","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f91d-200d-1f9d1-1f3fc.png","sheet_x":49,"sheet_y":30,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FD":{"unified":"1F9D1-1F3FB-200D-1F91D-200D-1F9D1-1F3FD","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f91d-200d-1f9d1-1f3fd.png","sheet_x":49,"sheet_y":31,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FE":{"unified":"1F9D1-1F3FB-200D-1F91D-200D-1F9D1-1F3FE","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f91d-200d-1f9d1-1f3fe.png","sheet_x":49,"sheet_y":32,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FB-1F3FF":{"unified":"1F9D1-1F3FB-200D-1F91D-200D-1F9D1-1F3FF","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f91d-200d-1f9d1-1f3ff.png","sheet_x":49,"sheet_y":33,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FB":{"unified":"1F9D1-1F3FC-200D-1F91D-200D-1F9D1-1F3FB","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f91d-200d-1f9d1-1f3fb.png","sheet_x":49,"sheet_y":34,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FC":{"unified":"1F9D1-1F3FC-200D-1F91D-200D-1F9D1-1F3FC","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f91d-200d-1f9d1-1f3fc.png","sheet_x":49,"sheet_y":35,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FD":{"unified":"1F9D1-1F3FC-200D-1F91D-200D-1F9D1-1F3FD","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f91d-200d-1f9d1-1f3fd.png","sheet_x":49,"sheet_y":36,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FE":{"unified":"1F9D1-1F3FC-200D-1F91D-200D-1F9D1-1F3FE","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f91d-200d-1f9d1-1f3fe.png","sheet_x":49,"sheet_y":37,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC-1F3FF":{"unified":"1F9D1-1F3FC-200D-1F91D-200D-1F9D1-1F3FF","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f91d-200d-1f9d1-1f3ff.png","sheet_x":49,"sheet_y":38,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FB":{"unified":"1F9D1-1F3FD-200D-1F91D-200D-1F9D1-1F3FB","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f91d-200d-1f9d1-1f3fb.png","sheet_x":49,"sheet_y":39,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FC":{"unified":"1F9D1-1F3FD-200D-1F91D-200D-1F9D1-1F3FC","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f91d-200d-1f9d1-1f3fc.png","sheet_x":49,"sheet_y":40,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FD":{"unified":"1F9D1-1F3FD-200D-1F91D-200D-1F9D1-1F3FD","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f91d-200d-1f9d1-1f3fd.png","sheet_x":49,"sheet_y":41,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FE":{"unified":"1F9D1-1F3FD-200D-1F91D-200D-1F9D1-1F3FE","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f91d-200d-1f9d1-1f3fe.png","sheet_x":49,"sheet_y":42,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD-1F3FF":{"unified":"1F9D1-1F3FD-200D-1F91D-200D-1F9D1-1F3FF","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f91d-200d-1f9d1-1f3ff.png","sheet_x":49,"sheet_y":43,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FB":{"unified":"1F9D1-1F3FE-200D-1F91D-200D-1F9D1-1F3FB","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f91d-200d-1f9d1-1f3fb.png","sheet_x":49,"sheet_y":44,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FC":{"unified":"1F9D1-1F3FE-200D-1F91D-200D-1F9D1-1F3FC","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f91d-200d-1f9d1-1f3fc.png","sheet_x":49,"sheet_y":45,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FD":{"unified":"1F9D1-1F3FE-200D-1F91D-200D-1F9D1-1F3FD","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f91d-200d-1f9d1-1f3fd.png","sheet_x":49,"sheet_y":46,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FE":{"unified":"1F9D1-1F3FE-200D-1F91D-200D-1F9D1-1F3FE","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f91d-200d-1f9d1-1f3fe.png","sheet_x":49,"sheet_y":47,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE-1F3FF":{"unified":"1F9D1-1F3FE-200D-1F91D-200D-1F9D1-1F3FF","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f91d-200d-1f9d1-1f3ff.png","sheet_x":49,"sheet_y":48,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FB":{"unified":"1F9D1-1F3FF-200D-1F91D-200D-1F9D1-1F3FB","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f91d-200d-1f9d1-1f3fb.png","sheet_x":49,"sheet_y":49,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FC":{"unified":"1F9D1-1F3FF-200D-1F91D-200D-1F9D1-1F3FC","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f91d-200d-1f9d1-1f3fc.png","sheet_x":49,"sheet_y":50,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FD":{"unified":"1F9D1-1F3FF-200D-1F91D-200D-1F9D1-1F3FD","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f91d-200d-1f9d1-1f3fd.png","sheet_x":49,"sheet_y":51,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FE":{"unified":"1F9D1-1F3FF-200D-1F91D-200D-1F9D1-1F3FE","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f91d-200d-1f9d1-1f3fe.png","sheet_x":49,"sheet_y":52,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF-1F3FF":{"unified":"1F9D1-1F3FF-200D-1F91D-200D-1F9D1-1F3FF","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f91d-200d-1f9d1-1f3ff.png","sheet_x":49,"sheet_y":53,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"PERSON WITH WHITE CANE FACING RIGHT","unified":"1F9D1-200D-1F9AF-200D-27A1-FE0F","non_qualified":"1F9D1-200D-1F9AF-200D-27A1","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f9af-200d-27a1-fe0f.png","sheet_x":49,"sheet_y":54,"short_name":"person_with_white_cane_facing_right","short_names":["person_with_white_cane_facing_right"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":424,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F9AF-200D-27A1-FE0F","non_qualified":"1F9D1-1F3FB-200D-1F9AF-200D-27A1","image":"1f9d1-1f3fb-200d-1f9af-200d-27a1-fe0f.png","sheet_x":49,"sheet_y":55,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F9AF-200D-27A1-FE0F","non_qualified":"1F9D1-1F3FC-200D-1F9AF-200D-27A1","image":"1f9d1-1f3fc-200d-1f9af-200d-27a1-fe0f.png","sheet_x":49,"sheet_y":56,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F9AF-200D-27A1-FE0F","non_qualified":"1F9D1-1F3FD-200D-1F9AF-200D-27A1","image":"1f9d1-1f3fd-200d-1f9af-200d-27a1-fe0f.png","sheet_x":49,"sheet_y":57,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F9AF-200D-27A1-FE0F","non_qualified":"1F9D1-1F3FE-200D-1F9AF-200D-27A1","image":"1f9d1-1f3fe-200d-1f9af-200d-27a1-fe0f.png","sheet_x":49,"sheet_y":58,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F9AF-200D-27A1-FE0F","non_qualified":"1F9D1-1F3FF-200D-1F9AF-200D-27A1","image":"1f9d1-1f3ff-200d-1f9af-200d-27a1-fe0f.png","sheet_x":49,"sheet_y":59,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false}}},{"name":"PERSON WITH WHITE CANE","unified":"1F9D1-200D-1F9AF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f9af.png","sheet_x":49,"sheet_y":60,"short_name":"person_with_probing_cane","short_names":["person_with_probing_cane"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":423,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F9AF","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f9af.png","sheet_x":49,"sheet_y":61,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F9AF","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f9af.png","sheet_x":50,"sheet_y":0,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F9AF","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f9af.png","sheet_x":50,"sheet_y":1,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F9AF","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f9af.png","sheet_x":50,"sheet_y":2,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F9AF","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f9af.png","sheet_x":50,"sheet_y":3,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"PERSON: RED HAIR","unified":"1F9D1-200D-1F9B0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f9b0.png","sheet_x":50,"sheet_y":4,"short_name":"red_haired_person","short_names":["red_haired_person"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":246,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F9B0","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f9b0.png","sheet_x":50,"sheet_y":5,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F9B0","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f9b0.png","sheet_x":50,"sheet_y":6,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F9B0","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f9b0.png","sheet_x":50,"sheet_y":7,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F9B0","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f9b0.png","sheet_x":50,"sheet_y":8,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F9B0","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f9b0.png","sheet_x":50,"sheet_y":9,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"PERSON: CURLY HAIR","unified":"1F9D1-200D-1F9B1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f9b1.png","sheet_x":50,"sheet_y":10,"short_name":"curly_haired_person","short_names":["curly_haired_person"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":248,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F9B1","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f9b1.png","sheet_x":50,"sheet_y":11,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F9B1","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f9b1.png","sheet_x":50,"sheet_y":12,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F9B1","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f9b1.png","sheet_x":50,"sheet_y":13,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F9B1","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f9b1.png","sheet_x":50,"sheet_y":14,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F9B1","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f9b1.png","sheet_x":50,"sheet_y":15,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"PERSON: BALD","unified":"1F9D1-200D-1F9B2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f9b2.png","sheet_x":50,"sheet_y":16,"short_name":"bald_person","short_names":["bald_person"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":252,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F9B2","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f9b2.png","sheet_x":50,"sheet_y":17,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F9B2","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f9b2.png","sheet_x":50,"sheet_y":18,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F9B2","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f9b2.png","sheet_x":50,"sheet_y":19,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F9B2","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f9b2.png","sheet_x":50,"sheet_y":20,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F9B2","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f9b2.png","sheet_x":50,"sheet_y":21,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"PERSON: WHITE HAIR","unified":"1F9D1-200D-1F9B3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f9b3.png","sheet_x":50,"sheet_y":22,"short_name":"white_haired_person","short_names":["white_haired_person"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":250,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F9B3","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f9b3.png","sheet_x":50,"sheet_y":23,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F9B3","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f9b3.png","sheet_x":50,"sheet_y":24,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F9B3","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f9b3.png","sheet_x":50,"sheet_y":25,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F9B3","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f9b3.png","sheet_x":50,"sheet_y":26,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F9B3","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f9b3.png","sheet_x":50,"sheet_y":27,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"PERSON IN MOTORIZED WHEELCHAIR FACING RIGHT","unified":"1F9D1-200D-1F9BC-200D-27A1-FE0F","non_qualified":"1F9D1-200D-1F9BC-200D-27A1","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f9bc-200d-27a1-fe0f.png","sheet_x":50,"sheet_y":28,"short_name":"person_in_motorized_wheelchair_facing_right","short_names":["person_in_motorized_wheelchair_facing_right"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":430,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F9BC-200D-27A1-FE0F","non_qualified":"1F9D1-1F3FB-200D-1F9BC-200D-27A1","image":"1f9d1-1f3fb-200d-1f9bc-200d-27a1-fe0f.png","sheet_x":50,"sheet_y":29,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F9BC-200D-27A1-FE0F","non_qualified":"1F9D1-1F3FC-200D-1F9BC-200D-27A1","image":"1f9d1-1f3fc-200d-1f9bc-200d-27a1-fe0f.png","sheet_x":50,"sheet_y":30,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F9BC-200D-27A1-FE0F","non_qualified":"1F9D1-1F3FD-200D-1F9BC-200D-27A1","image":"1f9d1-1f3fd-200d-1f9bc-200d-27a1-fe0f.png","sheet_x":50,"sheet_y":31,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F9BC-200D-27A1-FE0F","non_qualified":"1F9D1-1F3FE-200D-1F9BC-200D-27A1","image":"1f9d1-1f3fe-200d-1f9bc-200d-27a1-fe0f.png","sheet_x":50,"sheet_y":32,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F9BC-200D-27A1-FE0F","non_qualified":"1F9D1-1F3FF-200D-1F9BC-200D-27A1","image":"1f9d1-1f3ff-200d-1f9bc-200d-27a1-fe0f.png","sheet_x":50,"sheet_y":33,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false}}},{"name":"PERSON IN MOTORIZED WHEELCHAIR","unified":"1F9D1-200D-1F9BC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f9bc.png","sheet_x":50,"sheet_y":34,"short_name":"person_in_motorized_wheelchair","short_names":["person_in_motorized_wheelchair"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":429,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F9BC","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f9bc.png","sheet_x":50,"sheet_y":35,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F9BC","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f9bc.png","sheet_x":50,"sheet_y":36,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F9BC","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f9bc.png","sheet_x":50,"sheet_y":37,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F9BC","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f9bc.png","sheet_x":50,"sheet_y":38,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F9BC","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f9bc.png","sheet_x":50,"sheet_y":39,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"PERSON IN MANUAL WHEELCHAIR FACING RIGHT","unified":"1F9D1-200D-1F9BD-200D-27A1-FE0F","non_qualified":"1F9D1-200D-1F9BD-200D-27A1","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f9bd-200d-27a1-fe0f.png","sheet_x":50,"sheet_y":40,"short_name":"person_in_manual_wheelchair_facing_right","short_names":["person_in_manual_wheelchair_facing_right"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":436,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F9BD-200D-27A1-FE0F","non_qualified":"1F9D1-1F3FB-200D-1F9BD-200D-27A1","image":"1f9d1-1f3fb-200d-1f9bd-200d-27a1-fe0f.png","sheet_x":50,"sheet_y":41,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F9BD-200D-27A1-FE0F","non_qualified":"1F9D1-1F3FC-200D-1F9BD-200D-27A1","image":"1f9d1-1f3fc-200d-1f9bd-200d-27a1-fe0f.png","sheet_x":50,"sheet_y":42,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F9BD-200D-27A1-FE0F","non_qualified":"1F9D1-1F3FD-200D-1F9BD-200D-27A1","image":"1f9d1-1f3fd-200d-1f9bd-200d-27a1-fe0f.png","sheet_x":50,"sheet_y":43,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F9BD-200D-27A1-FE0F","non_qualified":"1F9D1-1F3FE-200D-1F9BD-200D-27A1","image":"1f9d1-1f3fe-200d-1f9bd-200d-27a1-fe0f.png","sheet_x":50,"sheet_y":44,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F9BD-200D-27A1-FE0F","non_qualified":"1F9D1-1F3FF-200D-1F9BD-200D-27A1","image":"1f9d1-1f3ff-200d-1f9bd-200d-27a1-fe0f.png","sheet_x":50,"sheet_y":45,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false}}},{"name":"PERSON IN MANUAL WHEELCHAIR","unified":"1F9D1-200D-1F9BD","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f9bd.png","sheet_x":50,"sheet_y":46,"short_name":"person_in_manual_wheelchair","short_names":["person_in_manual_wheelchair"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":435,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-1F9BD","non_qualified":null,"image":"1f9d1-1f3fb-200d-1f9bd.png","sheet_x":50,"sheet_y":47,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-1F9BD","non_qualified":null,"image":"1f9d1-1f3fc-200d-1f9bd.png","sheet_x":50,"sheet_y":48,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-1F9BD","non_qualified":null,"image":"1f9d1-1f3fd-200d-1f9bd.png","sheet_x":50,"sheet_y":49,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-1F9BD","non_qualified":null,"image":"1f9d1-1f3fe-200d-1f9bd.png","sheet_x":50,"sheet_y":50,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-1F9BD","non_qualified":null,"image":"1f9d1-1f3ff-200d-1f9bd.png","sheet_x":50,"sheet_y":51,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"FAMILY: ADULT, ADULT, CHILD","unified":"1F9D1-200D-1F9D1-200D-1F9D2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f9d1-200d-1f9d2.png","sheet_x":50,"sheet_y":52,"short_name":"family_adult_adult_child","short_names":["family_adult_adult_child"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-symbol","sort_order":549,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},{"name":"FAMILY: ADULT, ADULT, CHILD, CHILD","unified":"1F9D1-200D-1F9D1-200D-1F9D2-200D-1F9D2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f9d1-200d-1f9d2-200d-1f9d2.png","sheet_x":50,"sheet_y":53,"short_name":"family_adult_adult_child_child","short_names":["family_adult_adult_child_child"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-symbol","sort_order":550,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},{"name":"FAMILY: ADULT, CHILD, CHILD","unified":"1F9D1-200D-1F9D2-200D-1F9D2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f9d2-200d-1f9d2.png","sheet_x":50,"sheet_y":54,"short_name":"family_adult_child_child","short_names":["family_adult_child_child"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-symbol","sort_order":552,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},{"name":"FAMILY: ADULT, CHILD","unified":"1F9D1-200D-1F9D2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-1f9d2.png","sheet_x":50,"sheet_y":55,"short_name":"family_adult_child","short_names":["family_adult_child"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-symbol","sort_order":551,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},{"name":"HEALTH WORKER","unified":"1F9D1-200D-2695-FE0F","non_qualified":"1F9D1-200D-2695","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-2695-fe0f.png","sheet_x":50,"sheet_y":56,"short_name":"health_worker","short_names":["health_worker"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":288,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-2695-FE0F","non_qualified":"1F9D1-1F3FB-200D-2695","image":"1f9d1-1f3fb-200d-2695-fe0f.png","sheet_x":50,"sheet_y":57,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-2695-FE0F","non_qualified":"1F9D1-1F3FC-200D-2695","image":"1f9d1-1f3fc-200d-2695-fe0f.png","sheet_x":50,"sheet_y":58,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-2695-FE0F","non_qualified":"1F9D1-1F3FD-200D-2695","image":"1f9d1-1f3fd-200d-2695-fe0f.png","sheet_x":50,"sheet_y":59,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-2695-FE0F","non_qualified":"1F9D1-1F3FE-200D-2695","image":"1f9d1-1f3fe-200d-2695-fe0f.png","sheet_x":50,"sheet_y":60,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-2695-FE0F","non_qualified":"1F9D1-1F3FF-200D-2695","image":"1f9d1-1f3ff-200d-2695-fe0f.png","sheet_x":50,"sheet_y":61,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"JUDGE","unified":"1F9D1-200D-2696-FE0F","non_qualified":"1F9D1-200D-2696","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-2696-fe0f.png","sheet_x":51,"sheet_y":0,"short_name":"judge","short_names":["judge"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":297,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-2696-FE0F","non_qualified":"1F9D1-1F3FB-200D-2696","image":"1f9d1-1f3fb-200d-2696-fe0f.png","sheet_x":51,"sheet_y":1,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-2696-FE0F","non_qualified":"1F9D1-1F3FC-200D-2696","image":"1f9d1-1f3fc-200d-2696-fe0f.png","sheet_x":51,"sheet_y":2,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-2696-FE0F","non_qualified":"1F9D1-1F3FD-200D-2696","image":"1f9d1-1f3fd-200d-2696-fe0f.png","sheet_x":51,"sheet_y":3,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-2696-FE0F","non_qualified":"1F9D1-1F3FE-200D-2696","image":"1f9d1-1f3fe-200d-2696-fe0f.png","sheet_x":51,"sheet_y":4,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-2696-FE0F","non_qualified":"1F9D1-1F3FF-200D-2696","image":"1f9d1-1f3ff-200d-2696-fe0f.png","sheet_x":51,"sheet_y":5,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"PILOT","unified":"1F9D1-200D-2708-FE0F","non_qualified":"1F9D1-200D-2708","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1-200d-2708-fe0f.png","sheet_x":51,"sheet_y":6,"short_name":"pilot","short_names":["pilot"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":327,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB-200D-2708-FE0F","non_qualified":"1F9D1-1F3FB-200D-2708","image":"1f9d1-1f3fb-200d-2708-fe0f.png","sheet_x":51,"sheet_y":7,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC-200D-2708-FE0F","non_qualified":"1F9D1-1F3FC-200D-2708","image":"1f9d1-1f3fc-200d-2708-fe0f.png","sheet_x":51,"sheet_y":8,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD-200D-2708-FE0F","non_qualified":"1F9D1-1F3FD-200D-2708","image":"1f9d1-1f3fd-200d-2708-fe0f.png","sheet_x":51,"sheet_y":9,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE-200D-2708-FE0F","non_qualified":"1F9D1-1F3FE-200D-2708","image":"1f9d1-1f3fe-200d-2708-fe0f.png","sheet_x":51,"sheet_y":10,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF-200D-2708-FE0F","non_qualified":"1F9D1-1F3FF-200D-2708","image":"1f9d1-1f3ff-200d-2708-fe0f.png","sheet_x":51,"sheet_y":11,"added_in":"12.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"ADULT","unified":"1F9D1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d1.png","sheet_x":51,"sheet_y":12,"short_name":"adult","short_names":["adult"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":234,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D1-1F3FB","non_qualified":null,"image":"1f9d1-1f3fb.png","sheet_x":51,"sheet_y":13,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D1-1F3FC","non_qualified":null,"image":"1f9d1-1f3fc.png","sheet_x":51,"sheet_y":14,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D1-1F3FD","non_qualified":null,"image":"1f9d1-1f3fd.png","sheet_x":51,"sheet_y":15,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D1-1F3FE","non_qualified":null,"image":"1f9d1-1f3fe.png","sheet_x":51,"sheet_y":16,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D1-1F3FF","non_qualified":null,"image":"1f9d1-1f3ff.png","sheet_x":51,"sheet_y":17,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"CHILD","unified":"1F9D2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d2.png","sheet_x":51,"sheet_y":18,"short_name":"child","short_names":["child"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":231,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D2-1F3FB","non_qualified":null,"image":"1f9d2-1f3fb.png","sheet_x":51,"sheet_y":19,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D2-1F3FC","non_qualified":null,"image":"1f9d2-1f3fc.png","sheet_x":51,"sheet_y":20,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D2-1F3FD","non_qualified":null,"image":"1f9d2-1f3fd.png","sheet_x":51,"sheet_y":21,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D2-1F3FE","non_qualified":null,"image":"1f9d2-1f3fe.png","sheet_x":51,"sheet_y":22,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D2-1F3FF","non_qualified":null,"image":"1f9d2-1f3ff.png","sheet_x":51,"sheet_y":23,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"OLDER ADULT","unified":"1F9D3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d3.png","sheet_x":51,"sheet_y":24,"short_name":"older_adult","short_names":["older_adult"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":255,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D3-1F3FB","non_qualified":null,"image":"1f9d3-1f3fb.png","sheet_x":51,"sheet_y":25,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D3-1F3FC","non_qualified":null,"image":"1f9d3-1f3fc.png","sheet_x":51,"sheet_y":26,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D3-1F3FD","non_qualified":null,"image":"1f9d3-1f3fd.png","sheet_x":51,"sheet_y":27,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D3-1F3FE","non_qualified":null,"image":"1f9d3-1f3fe.png","sheet_x":51,"sheet_y":28,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D3-1F3FF","non_qualified":null,"image":"1f9d3-1f3ff.png","sheet_x":51,"sheet_y":29,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN: BEARD","unified":"1F9D4-200D-2640-FE0F","non_qualified":"1F9D4-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d4-200d-2640-fe0f.png","sheet_x":51,"sheet_y":30,"short_name":"woman_with_beard","short_names":["woman_with_beard"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":239,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D4-1F3FB-200D-2640-FE0F","non_qualified":"1F9D4-1F3FB-200D-2640","image":"1f9d4-1f3fb-200d-2640-fe0f.png","sheet_x":51,"sheet_y":31,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D4-1F3FC-200D-2640-FE0F","non_qualified":"1F9D4-1F3FC-200D-2640","image":"1f9d4-1f3fc-200d-2640-fe0f.png","sheet_x":51,"sheet_y":32,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D4-1F3FD-200D-2640-FE0F","non_qualified":"1F9D4-1F3FD-200D-2640","image":"1f9d4-1f3fd-200d-2640-fe0f.png","sheet_x":51,"sheet_y":33,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D4-1F3FE-200D-2640-FE0F","non_qualified":"1F9D4-1F3FE-200D-2640","image":"1f9d4-1f3fe-200d-2640-fe0f.png","sheet_x":51,"sheet_y":34,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D4-1F3FF-200D-2640-FE0F","non_qualified":"1F9D4-1F3FF-200D-2640","image":"1f9d4-1f3ff-200d-2640-fe0f.png","sheet_x":51,"sheet_y":35,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN: BEARD","unified":"1F9D4-200D-2642-FE0F","non_qualified":"1F9D4-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d4-200d-2642-fe0f.png","sheet_x":51,"sheet_y":36,"short_name":"man_with_beard","short_names":["man_with_beard"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":238,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D4-1F3FB-200D-2642-FE0F","non_qualified":"1F9D4-1F3FB-200D-2642","image":"1f9d4-1f3fb-200d-2642-fe0f.png","sheet_x":51,"sheet_y":37,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D4-1F3FC-200D-2642-FE0F","non_qualified":"1F9D4-1F3FC-200D-2642","image":"1f9d4-1f3fc-200d-2642-fe0f.png","sheet_x":51,"sheet_y":38,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D4-1F3FD-200D-2642-FE0F","non_qualified":"1F9D4-1F3FD-200D-2642","image":"1f9d4-1f3fd-200d-2642-fe0f.png","sheet_x":51,"sheet_y":39,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D4-1F3FE-200D-2642-FE0F","non_qualified":"1F9D4-1F3FE-200D-2642","image":"1f9d4-1f3fe-200d-2642-fe0f.png","sheet_x":51,"sheet_y":40,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D4-1F3FF-200D-2642-FE0F","non_qualified":"1F9D4-1F3FF-200D-2642","image":"1f9d4-1f3ff-200d-2642-fe0f.png","sheet_x":51,"sheet_y":41,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"BEARDED PERSON","unified":"1F9D4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d4.png","sheet_x":51,"sheet_y":42,"short_name":"bearded_person","short_names":["bearded_person"],"text":null,"texts":null,"category":"People & Body","subcategory":"person","sort_order":237,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D4-1F3FB","non_qualified":null,"image":"1f9d4-1f3fb.png","sheet_x":51,"sheet_y":43,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D4-1F3FC","non_qualified":null,"image":"1f9d4-1f3fc.png","sheet_x":51,"sheet_y":44,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D4-1F3FD","non_qualified":null,"image":"1f9d4-1f3fd.png","sheet_x":51,"sheet_y":45,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D4-1F3FE","non_qualified":null,"image":"1f9d4-1f3fe.png","sheet_x":51,"sheet_y":46,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D4-1F3FF","non_qualified":null,"image":"1f9d4-1f3ff.png","sheet_x":51,"sheet_y":47,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"PERSON WITH HEADSCARF","unified":"1F9D5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d5.png","sheet_x":51,"sheet_y":48,"short_name":"person_with_headscarf","short_names":["person_with_headscarf"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":356,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D5-1F3FB","non_qualified":null,"image":"1f9d5-1f3fb.png","sheet_x":51,"sheet_y":49,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D5-1F3FC","non_qualified":null,"image":"1f9d5-1f3fc.png","sheet_x":51,"sheet_y":50,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D5-1F3FD","non_qualified":null,"image":"1f9d5-1f3fd.png","sheet_x":51,"sheet_y":51,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D5-1F3FE","non_qualified":null,"image":"1f9d5-1f3fe.png","sheet_x":51,"sheet_y":52,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D5-1F3FF","non_qualified":null,"image":"1f9d5-1f3ff.png","sheet_x":51,"sheet_y":53,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WOMAN IN STEAMY ROOM","unified":"1F9D6-200D-2640-FE0F","non_qualified":"1F9D6-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d6-200d-2640-fe0f.png","sheet_x":51,"sheet_y":54,"short_name":"woman_in_steamy_room","short_names":["woman_in_steamy_room"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":455,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D6-1F3FB-200D-2640-FE0F","non_qualified":"1F9D6-1F3FB-200D-2640","image":"1f9d6-1f3fb-200d-2640-fe0f.png","sheet_x":51,"sheet_y":55,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D6-1F3FC-200D-2640-FE0F","non_qualified":"1F9D6-1F3FC-200D-2640","image":"1f9d6-1f3fc-200d-2640-fe0f.png","sheet_x":51,"sheet_y":56,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D6-1F3FD-200D-2640-FE0F","non_qualified":"1F9D6-1F3FD-200D-2640","image":"1f9d6-1f3fd-200d-2640-fe0f.png","sheet_x":51,"sheet_y":57,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D6-1F3FE-200D-2640-FE0F","non_qualified":"1F9D6-1F3FE-200D-2640","image":"1f9d6-1f3fe-200d-2640-fe0f.png","sheet_x":51,"sheet_y":58,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D6-1F3FF-200D-2640-FE0F","non_qualified":"1F9D6-1F3FF-200D-2640","image":"1f9d6-1f3ff-200d-2640-fe0f.png","sheet_x":51,"sheet_y":59,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN IN STEAMY ROOM","unified":"1F9D6-200D-2642-FE0F","non_qualified":"1F9D6-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d6-200d-2642-fe0f.png","sheet_x":51,"sheet_y":60,"short_name":"man_in_steamy_room","short_names":["man_in_steamy_room"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":454,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D6-1F3FB-200D-2642-FE0F","non_qualified":"1F9D6-1F3FB-200D-2642","image":"1f9d6-1f3fb-200d-2642-fe0f.png","sheet_x":51,"sheet_y":61,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9D6-1F3FB"},"1F3FC":{"unified":"1F9D6-1F3FC-200D-2642-FE0F","non_qualified":"1F9D6-1F3FC-200D-2642","image":"1f9d6-1f3fc-200d-2642-fe0f.png","sheet_x":52,"sheet_y":0,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9D6-1F3FC"},"1F3FD":{"unified":"1F9D6-1F3FD-200D-2642-FE0F","non_qualified":"1F9D6-1F3FD-200D-2642","image":"1f9d6-1f3fd-200d-2642-fe0f.png","sheet_x":52,"sheet_y":1,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9D6-1F3FD"},"1F3FE":{"unified":"1F9D6-1F3FE-200D-2642-FE0F","non_qualified":"1F9D6-1F3FE-200D-2642","image":"1f9d6-1f3fe-200d-2642-fe0f.png","sheet_x":52,"sheet_y":2,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9D6-1F3FE"},"1F3FF":{"unified":"1F9D6-1F3FF-200D-2642-FE0F","non_qualified":"1F9D6-1F3FF-200D-2642","image":"1f9d6-1f3ff-200d-2642-fe0f.png","sheet_x":52,"sheet_y":3,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9D6-1F3FF"}},"obsoletes":"1F9D6"},{"name":"PERSON IN STEAMY ROOM","unified":"1F9D6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d6.png","sheet_x":52,"sheet_y":4,"short_name":"person_in_steamy_room","short_names":["person_in_steamy_room"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":453,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D6-1F3FB","non_qualified":null,"image":"1f9d6-1f3fb.png","sheet_x":52,"sheet_y":5,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9D6-1F3FB-200D-2642-FE0F"},"1F3FC":{"unified":"1F9D6-1F3FC","non_qualified":null,"image":"1f9d6-1f3fc.png","sheet_x":52,"sheet_y":6,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9D6-1F3FC-200D-2642-FE0F"},"1F3FD":{"unified":"1F9D6-1F3FD","non_qualified":null,"image":"1f9d6-1f3fd.png","sheet_x":52,"sheet_y":7,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9D6-1F3FD-200D-2642-FE0F"},"1F3FE":{"unified":"1F9D6-1F3FE","non_qualified":null,"image":"1f9d6-1f3fe.png","sheet_x":52,"sheet_y":8,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9D6-1F3FE-200D-2642-FE0F"},"1F3FF":{"unified":"1F9D6-1F3FF","non_qualified":null,"image":"1f9d6-1f3ff.png","sheet_x":52,"sheet_y":9,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9D6-1F3FF-200D-2642-FE0F"}},"obsoleted_by":"1F9D6-200D-2642-FE0F"},{"name":"WOMAN CLIMBING","unified":"1F9D7-200D-2640-FE0F","non_qualified":"1F9D7-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d7-200d-2640-fe0f.png","sheet_x":52,"sheet_y":10,"short_name":"woman_climbing","short_names":["woman_climbing"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":458,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D7-1F3FB-200D-2640-FE0F","non_qualified":"1F9D7-1F3FB-200D-2640","image":"1f9d7-1f3fb-200d-2640-fe0f.png","sheet_x":52,"sheet_y":11,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9D7-1F3FB"},"1F3FC":{"unified":"1F9D7-1F3FC-200D-2640-FE0F","non_qualified":"1F9D7-1F3FC-200D-2640","image":"1f9d7-1f3fc-200d-2640-fe0f.png","sheet_x":52,"sheet_y":12,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9D7-1F3FC"},"1F3FD":{"unified":"1F9D7-1F3FD-200D-2640-FE0F","non_qualified":"1F9D7-1F3FD-200D-2640","image":"1f9d7-1f3fd-200d-2640-fe0f.png","sheet_x":52,"sheet_y":13,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9D7-1F3FD"},"1F3FE":{"unified":"1F9D7-1F3FE-200D-2640-FE0F","non_qualified":"1F9D7-1F3FE-200D-2640","image":"1f9d7-1f3fe-200d-2640-fe0f.png","sheet_x":52,"sheet_y":14,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9D7-1F3FE"},"1F3FF":{"unified":"1F9D7-1F3FF-200D-2640-FE0F","non_qualified":"1F9D7-1F3FF-200D-2640","image":"1f9d7-1f3ff-200d-2640-fe0f.png","sheet_x":52,"sheet_y":15,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9D7-1F3FF"}},"obsoletes":"1F9D7"},{"name":"MAN CLIMBING","unified":"1F9D7-200D-2642-FE0F","non_qualified":"1F9D7-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d7-200d-2642-fe0f.png","sheet_x":52,"sheet_y":16,"short_name":"man_climbing","short_names":["man_climbing"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":457,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D7-1F3FB-200D-2642-FE0F","non_qualified":"1F9D7-1F3FB-200D-2642","image":"1f9d7-1f3fb-200d-2642-fe0f.png","sheet_x":52,"sheet_y":17,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D7-1F3FC-200D-2642-FE0F","non_qualified":"1F9D7-1F3FC-200D-2642","image":"1f9d7-1f3fc-200d-2642-fe0f.png","sheet_x":52,"sheet_y":18,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D7-1F3FD-200D-2642-FE0F","non_qualified":"1F9D7-1F3FD-200D-2642","image":"1f9d7-1f3fd-200d-2642-fe0f.png","sheet_x":52,"sheet_y":19,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D7-1F3FE-200D-2642-FE0F","non_qualified":"1F9D7-1F3FE-200D-2642","image":"1f9d7-1f3fe-200d-2642-fe0f.png","sheet_x":52,"sheet_y":20,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D7-1F3FF-200D-2642-FE0F","non_qualified":"1F9D7-1F3FF-200D-2642","image":"1f9d7-1f3ff-200d-2642-fe0f.png","sheet_x":52,"sheet_y":21,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"PERSON CLIMBING","unified":"1F9D7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d7.png","sheet_x":52,"sheet_y":22,"short_name":"person_climbing","short_names":["person_climbing"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-activity","sort_order":456,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D7-1F3FB","non_qualified":null,"image":"1f9d7-1f3fb.png","sheet_x":52,"sheet_y":23,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9D7-1F3FB-200D-2640-FE0F"},"1F3FC":{"unified":"1F9D7-1F3FC","non_qualified":null,"image":"1f9d7-1f3fc.png","sheet_x":52,"sheet_y":24,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9D7-1F3FC-200D-2640-FE0F"},"1F3FD":{"unified":"1F9D7-1F3FD","non_qualified":null,"image":"1f9d7-1f3fd.png","sheet_x":52,"sheet_y":25,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9D7-1F3FD-200D-2640-FE0F"},"1F3FE":{"unified":"1F9D7-1F3FE","non_qualified":null,"image":"1f9d7-1f3fe.png","sheet_x":52,"sheet_y":26,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9D7-1F3FE-200D-2640-FE0F"},"1F3FF":{"unified":"1F9D7-1F3FF","non_qualified":null,"image":"1f9d7-1f3ff.png","sheet_x":52,"sheet_y":27,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9D7-1F3FF-200D-2640-FE0F"}},"obsoleted_by":"1F9D7-200D-2640-FE0F"},{"name":"WOMAN IN LOTUS POSITION","unified":"1F9D8-200D-2640-FE0F","non_qualified":"1F9D8-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d8-200d-2640-fe0f.png","sheet_x":52,"sheet_y":28,"short_name":"woman_in_lotus_position","short_names":["woman_in_lotus_position"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-resting","sort_order":504,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D8-1F3FB-200D-2640-FE0F","non_qualified":"1F9D8-1F3FB-200D-2640","image":"1f9d8-1f3fb-200d-2640-fe0f.png","sheet_x":52,"sheet_y":29,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9D8-1F3FB"},"1F3FC":{"unified":"1F9D8-1F3FC-200D-2640-FE0F","non_qualified":"1F9D8-1F3FC-200D-2640","image":"1f9d8-1f3fc-200d-2640-fe0f.png","sheet_x":52,"sheet_y":30,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9D8-1F3FC"},"1F3FD":{"unified":"1F9D8-1F3FD-200D-2640-FE0F","non_qualified":"1F9D8-1F3FD-200D-2640","image":"1f9d8-1f3fd-200d-2640-fe0f.png","sheet_x":52,"sheet_y":31,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9D8-1F3FD"},"1F3FE":{"unified":"1F9D8-1F3FE-200D-2640-FE0F","non_qualified":"1F9D8-1F3FE-200D-2640","image":"1f9d8-1f3fe-200d-2640-fe0f.png","sheet_x":52,"sheet_y":32,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9D8-1F3FE"},"1F3FF":{"unified":"1F9D8-1F3FF-200D-2640-FE0F","non_qualified":"1F9D8-1F3FF-200D-2640","image":"1f9d8-1f3ff-200d-2640-fe0f.png","sheet_x":52,"sheet_y":33,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9D8-1F3FF"}},"obsoletes":"1F9D8"},{"name":"MAN IN LOTUS POSITION","unified":"1F9D8-200D-2642-FE0F","non_qualified":"1F9D8-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d8-200d-2642-fe0f.png","sheet_x":52,"sheet_y":34,"short_name":"man_in_lotus_position","short_names":["man_in_lotus_position"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-resting","sort_order":503,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D8-1F3FB-200D-2642-FE0F","non_qualified":"1F9D8-1F3FB-200D-2642","image":"1f9d8-1f3fb-200d-2642-fe0f.png","sheet_x":52,"sheet_y":35,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D8-1F3FC-200D-2642-FE0F","non_qualified":"1F9D8-1F3FC-200D-2642","image":"1f9d8-1f3fc-200d-2642-fe0f.png","sheet_x":52,"sheet_y":36,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D8-1F3FD-200D-2642-FE0F","non_qualified":"1F9D8-1F3FD-200D-2642","image":"1f9d8-1f3fd-200d-2642-fe0f.png","sheet_x":52,"sheet_y":37,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D8-1F3FE-200D-2642-FE0F","non_qualified":"1F9D8-1F3FE-200D-2642","image":"1f9d8-1f3fe-200d-2642-fe0f.png","sheet_x":52,"sheet_y":38,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D8-1F3FF-200D-2642-FE0F","non_qualified":"1F9D8-1F3FF-200D-2642","image":"1f9d8-1f3ff-200d-2642-fe0f.png","sheet_x":52,"sheet_y":39,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"PERSON IN LOTUS POSITION","unified":"1F9D8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d8.png","sheet_x":52,"sheet_y":40,"short_name":"person_in_lotus_position","short_names":["person_in_lotus_position"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-resting","sort_order":502,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D8-1F3FB","non_qualified":null,"image":"1f9d8-1f3fb.png","sheet_x":52,"sheet_y":41,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9D8-1F3FB-200D-2640-FE0F"},"1F3FC":{"unified":"1F9D8-1F3FC","non_qualified":null,"image":"1f9d8-1f3fc.png","sheet_x":52,"sheet_y":42,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9D8-1F3FC-200D-2640-FE0F"},"1F3FD":{"unified":"1F9D8-1F3FD","non_qualified":null,"image":"1f9d8-1f3fd.png","sheet_x":52,"sheet_y":43,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9D8-1F3FD-200D-2640-FE0F"},"1F3FE":{"unified":"1F9D8-1F3FE","non_qualified":null,"image":"1f9d8-1f3fe.png","sheet_x":52,"sheet_y":44,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9D8-1F3FE-200D-2640-FE0F"},"1F3FF":{"unified":"1F9D8-1F3FF","non_qualified":null,"image":"1f9d8-1f3ff.png","sheet_x":52,"sheet_y":45,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9D8-1F3FF-200D-2640-FE0F"}},"obsoleted_by":"1F9D8-200D-2640-FE0F"},{"name":"WOMAN MAGE","unified":"1F9D9-200D-2640-FE0F","non_qualified":"1F9D9-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d9-200d-2640-fe0f.png","sheet_x":52,"sheet_y":46,"short_name":"female_mage","short_names":["female_mage"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":382,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D9-1F3FB-200D-2640-FE0F","non_qualified":"1F9D9-1F3FB-200D-2640","image":"1f9d9-1f3fb-200d-2640-fe0f.png","sheet_x":52,"sheet_y":47,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9D9-1F3FB"},"1F3FC":{"unified":"1F9D9-1F3FC-200D-2640-FE0F","non_qualified":"1F9D9-1F3FC-200D-2640","image":"1f9d9-1f3fc-200d-2640-fe0f.png","sheet_x":52,"sheet_y":48,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9D9-1F3FC"},"1F3FD":{"unified":"1F9D9-1F3FD-200D-2640-FE0F","non_qualified":"1F9D9-1F3FD-200D-2640","image":"1f9d9-1f3fd-200d-2640-fe0f.png","sheet_x":52,"sheet_y":49,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9D9-1F3FD"},"1F3FE":{"unified":"1F9D9-1F3FE-200D-2640-FE0F","non_qualified":"1F9D9-1F3FE-200D-2640","image":"1f9d9-1f3fe-200d-2640-fe0f.png","sheet_x":52,"sheet_y":50,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9D9-1F3FE"},"1F3FF":{"unified":"1F9D9-1F3FF-200D-2640-FE0F","non_qualified":"1F9D9-1F3FF-200D-2640","image":"1f9d9-1f3ff-200d-2640-fe0f.png","sheet_x":52,"sheet_y":51,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9D9-1F3FF"}},"obsoletes":"1F9D9"},{"name":"MAN MAGE","unified":"1F9D9-200D-2642-FE0F","non_qualified":"1F9D9-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d9-200d-2642-fe0f.png","sheet_x":52,"sheet_y":52,"short_name":"male_mage","short_names":["male_mage"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":381,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D9-1F3FB-200D-2642-FE0F","non_qualified":"1F9D9-1F3FB-200D-2642","image":"1f9d9-1f3fb-200d-2642-fe0f.png","sheet_x":52,"sheet_y":53,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9D9-1F3FC-200D-2642-FE0F","non_qualified":"1F9D9-1F3FC-200D-2642","image":"1f9d9-1f3fc-200d-2642-fe0f.png","sheet_x":52,"sheet_y":54,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9D9-1F3FD-200D-2642-FE0F","non_qualified":"1F9D9-1F3FD-200D-2642","image":"1f9d9-1f3fd-200d-2642-fe0f.png","sheet_x":52,"sheet_y":55,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9D9-1F3FE-200D-2642-FE0F","non_qualified":"1F9D9-1F3FE-200D-2642","image":"1f9d9-1f3fe-200d-2642-fe0f.png","sheet_x":52,"sheet_y":56,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9D9-1F3FF-200D-2642-FE0F","non_qualified":"1F9D9-1F3FF-200D-2642","image":"1f9d9-1f3ff-200d-2642-fe0f.png","sheet_x":52,"sheet_y":57,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAGE","unified":"1F9D9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9d9.png","sheet_x":52,"sheet_y":58,"short_name":"mage","short_names":["mage"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":380,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9D9-1F3FB","non_qualified":null,"image":"1f9d9-1f3fb.png","sheet_x":52,"sheet_y":59,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9D9-1F3FB-200D-2640-FE0F"},"1F3FC":{"unified":"1F9D9-1F3FC","non_qualified":null,"image":"1f9d9-1f3fc.png","sheet_x":52,"sheet_y":60,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9D9-1F3FC-200D-2640-FE0F"},"1F3FD":{"unified":"1F9D9-1F3FD","non_qualified":null,"image":"1f9d9-1f3fd.png","sheet_x":52,"sheet_y":61,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9D9-1F3FD-200D-2640-FE0F"},"1F3FE":{"unified":"1F9D9-1F3FE","non_qualified":null,"image":"1f9d9-1f3fe.png","sheet_x":53,"sheet_y":0,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9D9-1F3FE-200D-2640-FE0F"},"1F3FF":{"unified":"1F9D9-1F3FF","non_qualified":null,"image":"1f9d9-1f3ff.png","sheet_x":53,"sheet_y":1,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9D9-1F3FF-200D-2640-FE0F"}},"obsoleted_by":"1F9D9-200D-2640-FE0F"},{"name":"WOMAN FAIRY","unified":"1F9DA-200D-2640-FE0F","non_qualified":"1F9DA-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9da-200d-2640-fe0f.png","sheet_x":53,"sheet_y":2,"short_name":"female_fairy","short_names":["female_fairy"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":385,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9DA-1F3FB-200D-2640-FE0F","non_qualified":"1F9DA-1F3FB-200D-2640","image":"1f9da-1f3fb-200d-2640-fe0f.png","sheet_x":53,"sheet_y":3,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9DA-1F3FB"},"1F3FC":{"unified":"1F9DA-1F3FC-200D-2640-FE0F","non_qualified":"1F9DA-1F3FC-200D-2640","image":"1f9da-1f3fc-200d-2640-fe0f.png","sheet_x":53,"sheet_y":4,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9DA-1F3FC"},"1F3FD":{"unified":"1F9DA-1F3FD-200D-2640-FE0F","non_qualified":"1F9DA-1F3FD-200D-2640","image":"1f9da-1f3fd-200d-2640-fe0f.png","sheet_x":53,"sheet_y":5,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9DA-1F3FD"},"1F3FE":{"unified":"1F9DA-1F3FE-200D-2640-FE0F","non_qualified":"1F9DA-1F3FE-200D-2640","image":"1f9da-1f3fe-200d-2640-fe0f.png","sheet_x":53,"sheet_y":6,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9DA-1F3FE"},"1F3FF":{"unified":"1F9DA-1F3FF-200D-2640-FE0F","non_qualified":"1F9DA-1F3FF-200D-2640","image":"1f9da-1f3ff-200d-2640-fe0f.png","sheet_x":53,"sheet_y":7,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9DA-1F3FF"}},"obsoletes":"1F9DA"},{"name":"MAN FAIRY","unified":"1F9DA-200D-2642-FE0F","non_qualified":"1F9DA-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9da-200d-2642-fe0f.png","sheet_x":53,"sheet_y":8,"short_name":"male_fairy","short_names":["male_fairy"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":384,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9DA-1F3FB-200D-2642-FE0F","non_qualified":"1F9DA-1F3FB-200D-2642","image":"1f9da-1f3fb-200d-2642-fe0f.png","sheet_x":53,"sheet_y":9,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9DA-1F3FC-200D-2642-FE0F","non_qualified":"1F9DA-1F3FC-200D-2642","image":"1f9da-1f3fc-200d-2642-fe0f.png","sheet_x":53,"sheet_y":10,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9DA-1F3FD-200D-2642-FE0F","non_qualified":"1F9DA-1F3FD-200D-2642","image":"1f9da-1f3fd-200d-2642-fe0f.png","sheet_x":53,"sheet_y":11,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9DA-1F3FE-200D-2642-FE0F","non_qualified":"1F9DA-1F3FE-200D-2642","image":"1f9da-1f3fe-200d-2642-fe0f.png","sheet_x":53,"sheet_y":12,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9DA-1F3FF-200D-2642-FE0F","non_qualified":"1F9DA-1F3FF-200D-2642","image":"1f9da-1f3ff-200d-2642-fe0f.png","sheet_x":53,"sheet_y":13,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"FAIRY","unified":"1F9DA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9da.png","sheet_x":53,"sheet_y":14,"short_name":"fairy","short_names":["fairy"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":383,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9DA-1F3FB","non_qualified":null,"image":"1f9da-1f3fb.png","sheet_x":53,"sheet_y":15,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9DA-1F3FB-200D-2640-FE0F"},"1F3FC":{"unified":"1F9DA-1F3FC","non_qualified":null,"image":"1f9da-1f3fc.png","sheet_x":53,"sheet_y":16,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9DA-1F3FC-200D-2640-FE0F"},"1F3FD":{"unified":"1F9DA-1F3FD","non_qualified":null,"image":"1f9da-1f3fd.png","sheet_x":53,"sheet_y":17,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9DA-1F3FD-200D-2640-FE0F"},"1F3FE":{"unified":"1F9DA-1F3FE","non_qualified":null,"image":"1f9da-1f3fe.png","sheet_x":53,"sheet_y":18,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9DA-1F3FE-200D-2640-FE0F"},"1F3FF":{"unified":"1F9DA-1F3FF","non_qualified":null,"image":"1f9da-1f3ff.png","sheet_x":53,"sheet_y":19,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9DA-1F3FF-200D-2640-FE0F"}},"obsoleted_by":"1F9DA-200D-2640-FE0F"},{"name":"WOMAN VAMPIRE","unified":"1F9DB-200D-2640-FE0F","non_qualified":"1F9DB-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9db-200d-2640-fe0f.png","sheet_x":53,"sheet_y":20,"short_name":"female_vampire","short_names":["female_vampire"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":388,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9DB-1F3FB-200D-2640-FE0F","non_qualified":"1F9DB-1F3FB-200D-2640","image":"1f9db-1f3fb-200d-2640-fe0f.png","sheet_x":53,"sheet_y":21,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9DB-1F3FB"},"1F3FC":{"unified":"1F9DB-1F3FC-200D-2640-FE0F","non_qualified":"1F9DB-1F3FC-200D-2640","image":"1f9db-1f3fc-200d-2640-fe0f.png","sheet_x":53,"sheet_y":22,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9DB-1F3FC"},"1F3FD":{"unified":"1F9DB-1F3FD-200D-2640-FE0F","non_qualified":"1F9DB-1F3FD-200D-2640","image":"1f9db-1f3fd-200d-2640-fe0f.png","sheet_x":53,"sheet_y":23,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9DB-1F3FD"},"1F3FE":{"unified":"1F9DB-1F3FE-200D-2640-FE0F","non_qualified":"1F9DB-1F3FE-200D-2640","image":"1f9db-1f3fe-200d-2640-fe0f.png","sheet_x":53,"sheet_y":24,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9DB-1F3FE"},"1F3FF":{"unified":"1F9DB-1F3FF-200D-2640-FE0F","non_qualified":"1F9DB-1F3FF-200D-2640","image":"1f9db-1f3ff-200d-2640-fe0f.png","sheet_x":53,"sheet_y":25,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9DB-1F3FF"}},"obsoletes":"1F9DB"},{"name":"MAN VAMPIRE","unified":"1F9DB-200D-2642-FE0F","non_qualified":"1F9DB-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9db-200d-2642-fe0f.png","sheet_x":53,"sheet_y":26,"short_name":"male_vampire","short_names":["male_vampire"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":387,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9DB-1F3FB-200D-2642-FE0F","non_qualified":"1F9DB-1F3FB-200D-2642","image":"1f9db-1f3fb-200d-2642-fe0f.png","sheet_x":53,"sheet_y":27,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9DB-1F3FC-200D-2642-FE0F","non_qualified":"1F9DB-1F3FC-200D-2642","image":"1f9db-1f3fc-200d-2642-fe0f.png","sheet_x":53,"sheet_y":28,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9DB-1F3FD-200D-2642-FE0F","non_qualified":"1F9DB-1F3FD-200D-2642","image":"1f9db-1f3fd-200d-2642-fe0f.png","sheet_x":53,"sheet_y":29,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9DB-1F3FE-200D-2642-FE0F","non_qualified":"1F9DB-1F3FE-200D-2642","image":"1f9db-1f3fe-200d-2642-fe0f.png","sheet_x":53,"sheet_y":30,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9DB-1F3FF-200D-2642-FE0F","non_qualified":"1F9DB-1F3FF-200D-2642","image":"1f9db-1f3ff-200d-2642-fe0f.png","sheet_x":53,"sheet_y":31,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"VAMPIRE","unified":"1F9DB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9db.png","sheet_x":53,"sheet_y":32,"short_name":"vampire","short_names":["vampire"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":386,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9DB-1F3FB","non_qualified":null,"image":"1f9db-1f3fb.png","sheet_x":53,"sheet_y":33,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9DB-1F3FB-200D-2640-FE0F"},"1F3FC":{"unified":"1F9DB-1F3FC","non_qualified":null,"image":"1f9db-1f3fc.png","sheet_x":53,"sheet_y":34,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9DB-1F3FC-200D-2640-FE0F"},"1F3FD":{"unified":"1F9DB-1F3FD","non_qualified":null,"image":"1f9db-1f3fd.png","sheet_x":53,"sheet_y":35,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9DB-1F3FD-200D-2640-FE0F"},"1F3FE":{"unified":"1F9DB-1F3FE","non_qualified":null,"image":"1f9db-1f3fe.png","sheet_x":53,"sheet_y":36,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9DB-1F3FE-200D-2640-FE0F"},"1F3FF":{"unified":"1F9DB-1F3FF","non_qualified":null,"image":"1f9db-1f3ff.png","sheet_x":53,"sheet_y":37,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9DB-1F3FF-200D-2640-FE0F"}},"obsoleted_by":"1F9DB-200D-2640-FE0F"},{"name":"MERMAID","unified":"1F9DC-200D-2640-FE0F","non_qualified":"1F9DC-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9dc-200d-2640-fe0f.png","sheet_x":53,"sheet_y":38,"short_name":"mermaid","short_names":["mermaid"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":391,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9DC-1F3FB-200D-2640-FE0F","non_qualified":"1F9DC-1F3FB-200D-2640","image":"1f9dc-1f3fb-200d-2640-fe0f.png","sheet_x":53,"sheet_y":39,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9DC-1F3FC-200D-2640-FE0F","non_qualified":"1F9DC-1F3FC-200D-2640","image":"1f9dc-1f3fc-200d-2640-fe0f.png","sheet_x":53,"sheet_y":40,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9DC-1F3FD-200D-2640-FE0F","non_qualified":"1F9DC-1F3FD-200D-2640","image":"1f9dc-1f3fd-200d-2640-fe0f.png","sheet_x":53,"sheet_y":41,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9DC-1F3FE-200D-2640-FE0F","non_qualified":"1F9DC-1F3FE-200D-2640","image":"1f9dc-1f3fe-200d-2640-fe0f.png","sheet_x":53,"sheet_y":42,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9DC-1F3FF-200D-2640-FE0F","non_qualified":"1F9DC-1F3FF-200D-2640","image":"1f9dc-1f3ff-200d-2640-fe0f.png","sheet_x":53,"sheet_y":43,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MERMAN","unified":"1F9DC-200D-2642-FE0F","non_qualified":"1F9DC-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9dc-200d-2642-fe0f.png","sheet_x":53,"sheet_y":44,"short_name":"merman","short_names":["merman"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":390,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9DC-1F3FB-200D-2642-FE0F","non_qualified":"1F9DC-1F3FB-200D-2642","image":"1f9dc-1f3fb-200d-2642-fe0f.png","sheet_x":53,"sheet_y":45,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9DC-1F3FB"},"1F3FC":{"unified":"1F9DC-1F3FC-200D-2642-FE0F","non_qualified":"1F9DC-1F3FC-200D-2642","image":"1f9dc-1f3fc-200d-2642-fe0f.png","sheet_x":53,"sheet_y":46,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9DC-1F3FC"},"1F3FD":{"unified":"1F9DC-1F3FD-200D-2642-FE0F","non_qualified":"1F9DC-1F3FD-200D-2642","image":"1f9dc-1f3fd-200d-2642-fe0f.png","sheet_x":53,"sheet_y":47,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9DC-1F3FD"},"1F3FE":{"unified":"1F9DC-1F3FE-200D-2642-FE0F","non_qualified":"1F9DC-1F3FE-200D-2642","image":"1f9dc-1f3fe-200d-2642-fe0f.png","sheet_x":53,"sheet_y":48,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9DC-1F3FE"},"1F3FF":{"unified":"1F9DC-1F3FF-200D-2642-FE0F","non_qualified":"1F9DC-1F3FF-200D-2642","image":"1f9dc-1f3ff-200d-2642-fe0f.png","sheet_x":53,"sheet_y":49,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9DC-1F3FF"}},"obsoletes":"1F9DC"},{"name":"MERPERSON","unified":"1F9DC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9dc.png","sheet_x":53,"sheet_y":50,"short_name":"merperson","short_names":["merperson"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":389,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9DC-1F3FB","non_qualified":null,"image":"1f9dc-1f3fb.png","sheet_x":53,"sheet_y":51,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9DC-1F3FB-200D-2642-FE0F"},"1F3FC":{"unified":"1F9DC-1F3FC","non_qualified":null,"image":"1f9dc-1f3fc.png","sheet_x":53,"sheet_y":52,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9DC-1F3FC-200D-2642-FE0F"},"1F3FD":{"unified":"1F9DC-1F3FD","non_qualified":null,"image":"1f9dc-1f3fd.png","sheet_x":53,"sheet_y":53,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9DC-1F3FD-200D-2642-FE0F"},"1F3FE":{"unified":"1F9DC-1F3FE","non_qualified":null,"image":"1f9dc-1f3fe.png","sheet_x":53,"sheet_y":54,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9DC-1F3FE-200D-2642-FE0F"},"1F3FF":{"unified":"1F9DC-1F3FF","non_qualified":null,"image":"1f9dc-1f3ff.png","sheet_x":53,"sheet_y":55,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9DC-1F3FF-200D-2642-FE0F"}},"obsoleted_by":"1F9DC-200D-2642-FE0F"},{"name":"WOMAN ELF","unified":"1F9DD-200D-2640-FE0F","non_qualified":"1F9DD-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9dd-200d-2640-fe0f.png","sheet_x":53,"sheet_y":56,"short_name":"female_elf","short_names":["female_elf"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":394,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9DD-1F3FB-200D-2640-FE0F","non_qualified":"1F9DD-1F3FB-200D-2640","image":"1f9dd-1f3fb-200d-2640-fe0f.png","sheet_x":53,"sheet_y":57,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1F9DD-1F3FC-200D-2640-FE0F","non_qualified":"1F9DD-1F3FC-200D-2640","image":"1f9dd-1f3fc-200d-2640-fe0f.png","sheet_x":53,"sheet_y":58,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1F9DD-1F3FD-200D-2640-FE0F","non_qualified":"1F9DD-1F3FD-200D-2640","image":"1f9dd-1f3fd-200d-2640-fe0f.png","sheet_x":53,"sheet_y":59,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1F9DD-1F3FE-200D-2640-FE0F","non_qualified":"1F9DD-1F3FE-200D-2640","image":"1f9dd-1f3fe-200d-2640-fe0f.png","sheet_x":53,"sheet_y":60,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1F9DD-1F3FF-200D-2640-FE0F","non_qualified":"1F9DD-1F3FF-200D-2640","image":"1f9dd-1f3ff-200d-2640-fe0f.png","sheet_x":53,"sheet_y":61,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN ELF","unified":"1F9DD-200D-2642-FE0F","non_qualified":"1F9DD-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9dd-200d-2642-fe0f.png","sheet_x":54,"sheet_y":0,"short_name":"male_elf","short_names":["male_elf"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":393,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9DD-1F3FB-200D-2642-FE0F","non_qualified":"1F9DD-1F3FB-200D-2642","image":"1f9dd-1f3fb-200d-2642-fe0f.png","sheet_x":54,"sheet_y":1,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9DD-1F3FB"},"1F3FC":{"unified":"1F9DD-1F3FC-200D-2642-FE0F","non_qualified":"1F9DD-1F3FC-200D-2642","image":"1f9dd-1f3fc-200d-2642-fe0f.png","sheet_x":54,"sheet_y":2,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9DD-1F3FC"},"1F3FD":{"unified":"1F9DD-1F3FD-200D-2642-FE0F","non_qualified":"1F9DD-1F3FD-200D-2642","image":"1f9dd-1f3fd-200d-2642-fe0f.png","sheet_x":54,"sheet_y":3,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9DD-1F3FD"},"1F3FE":{"unified":"1F9DD-1F3FE-200D-2642-FE0F","non_qualified":"1F9DD-1F3FE-200D-2642","image":"1f9dd-1f3fe-200d-2642-fe0f.png","sheet_x":54,"sheet_y":4,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9DD-1F3FE"},"1F3FF":{"unified":"1F9DD-1F3FF-200D-2642-FE0F","non_qualified":"1F9DD-1F3FF-200D-2642","image":"1f9dd-1f3ff-200d-2642-fe0f.png","sheet_x":54,"sheet_y":5,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9DD-1F3FF"}},"obsoletes":"1F9DD"},{"name":"ELF","unified":"1F9DD","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9dd.png","sheet_x":54,"sheet_y":6,"short_name":"elf","short_names":["elf"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":392,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1F9DD-1F3FB","non_qualified":null,"image":"1f9dd-1f3fb.png","sheet_x":54,"sheet_y":7,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9DD-1F3FB-200D-2642-FE0F"},"1F3FC":{"unified":"1F9DD-1F3FC","non_qualified":null,"image":"1f9dd-1f3fc.png","sheet_x":54,"sheet_y":8,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9DD-1F3FC-200D-2642-FE0F"},"1F3FD":{"unified":"1F9DD-1F3FD","non_qualified":null,"image":"1f9dd-1f3fd.png","sheet_x":54,"sheet_y":9,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9DD-1F3FD-200D-2642-FE0F"},"1F3FE":{"unified":"1F9DD-1F3FE","non_qualified":null,"image":"1f9dd-1f3fe.png","sheet_x":54,"sheet_y":10,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9DD-1F3FE-200D-2642-FE0F"},"1F3FF":{"unified":"1F9DD-1F3FF","non_qualified":null,"image":"1f9dd-1f3ff.png","sheet_x":54,"sheet_y":11,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9DD-1F3FF-200D-2642-FE0F"}},"obsoleted_by":"1F9DD-200D-2642-FE0F"},{"name":"WOMAN GENIE","unified":"1F9DE-200D-2640-FE0F","non_qualified":"1F9DE-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9de-200d-2640-fe0f.png","sheet_x":54,"sheet_y":12,"short_name":"female_genie","short_names":["female_genie"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":397,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MAN GENIE","unified":"1F9DE-200D-2642-FE0F","non_qualified":"1F9DE-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9de-200d-2642-fe0f.png","sheet_x":54,"sheet_y":13,"short_name":"male_genie","short_names":["male_genie"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":396,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9DE"},{"name":"GENIE","unified":"1F9DE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9de.png","sheet_x":54,"sheet_y":14,"short_name":"genie","short_names":["genie"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":395,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9DE-200D-2642-FE0F"},{"name":"WOMAN ZOMBIE","unified":"1F9DF-200D-2640-FE0F","non_qualified":"1F9DF-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9df-200d-2640-fe0f.png","sheet_x":54,"sheet_y":15,"short_name":"female_zombie","short_names":["female_zombie"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":400,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MAN ZOMBIE","unified":"1F9DF-200D-2642-FE0F","non_qualified":"1F9DF-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9df-200d-2642-fe0f.png","sheet_x":54,"sheet_y":16,"short_name":"male_zombie","short_names":["male_zombie"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":399,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoletes":"1F9DF"},{"name":"ZOMBIE","unified":"1F9DF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9df.png","sheet_x":54,"sheet_y":17,"short_name":"zombie","short_names":["zombie"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-fantasy","sort_order":398,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"obsoleted_by":"1F9DF-200D-2642-FE0F"},{"name":"BRAIN","unified":"1F9E0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9e0.png","sheet_x":54,"sheet_y":18,"short_name":"brain","short_names":["brain"],"text":null,"texts":null,"category":"People & Body","subcategory":"body-parts","sort_order":220,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ORANGE HEART","unified":"1F9E1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9e1.png","sheet_x":54,"sheet_y":19,"short_name":"orange_heart","short_names":["orange_heart"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"heart","sort_order":145,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BILLED CAP","unified":"1F9E2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9e2.png","sheet_x":54,"sheet_y":20,"short_name":"billed_cap","short_names":["billed_cap"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1190,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SCARF","unified":"1F9E3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9e3.png","sheet_x":54,"sheet_y":21,"short_name":"scarf","short_names":["scarf"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1158,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GLOVES","unified":"1F9E4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9e4.png","sheet_x":54,"sheet_y":22,"short_name":"gloves","short_names":["gloves"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1159,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"COAT","unified":"1F9E5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9e5.png","sheet_x":54,"sheet_y":23,"short_name":"coat","short_names":["coat"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1160,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SOCKS","unified":"1F9E6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9e6.png","sheet_x":54,"sheet_y":24,"short_name":"socks","short_names":["socks"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1161,"added_in":"5.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RED GIFT ENVELOPE","unified":"1F9E7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9e7.png","sheet_x":54,"sheet_y":25,"short_name":"red_envelope","short_names":["red_envelope"],"text":null,"texts":null,"category":"Activities","subcategory":"event","sort_order":1080,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FIRECRACKER","unified":"1F9E8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9e8.png","sheet_x":54,"sheet_y":26,"short_name":"firecracker","short_names":["firecracker"],"text":null,"texts":null,"category":"Activities","subcategory":"event","sort_order":1069,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"JIGSAW PUZZLE PIECE","unified":"1F9E9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9e9.png","sheet_x":54,"sheet_y":27,"short_name":"jigsaw","short_names":["jigsaw"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1130,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TEST TUBE","unified":"1F9EA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9ea.png","sheet_x":54,"sheet_y":28,"short_name":"test_tube","short_names":["test_tube"],"text":null,"texts":null,"category":"Objects","subcategory":"science","sort_order":1365,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PETRI DISH","unified":"1F9EB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9eb.png","sheet_x":54,"sheet_y":29,"short_name":"petri_dish","short_names":["petri_dish"],"text":null,"texts":null,"category":"Objects","subcategory":"science","sort_order":1366,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DNA DOUBLE HELIX","unified":"1F9EC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9ec.png","sheet_x":54,"sheet_y":30,"short_name":"dna","short_names":["dna"],"text":null,"texts":null,"category":"Objects","subcategory":"science","sort_order":1367,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"COMPASS","unified":"1F9ED","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9ed.png","sheet_x":54,"sheet_y":31,"short_name":"compass","short_names":["compass"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-map","sort_order":853,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ABACUS","unified":"1F9EE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9ee.png","sheet_x":54,"sheet_y":32,"short_name":"abacus","short_names":["abacus"],"text":null,"texts":null,"category":"Objects","subcategory":"computer","sort_order":1245,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FIRE EXTINGUISHER","unified":"1F9EF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9ef.png","sheet_x":54,"sheet_y":33,"short_name":"fire_extinguisher","short_names":["fire_extinguisher"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1401,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TOOLBOX","unified":"1F9F0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9f0.png","sheet_x":54,"sheet_y":34,"short_name":"toolbox","short_names":["toolbox"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1361,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BRICK","unified":"1F9F1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9f1.png","sheet_x":54,"sheet_y":35,"short_name":"bricks","short_names":["bricks"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":866,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MAGNET","unified":"1F9F2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9f2.png","sheet_x":54,"sheet_y":36,"short_name":"magnet","short_names":["magnet"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1362,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LUGGAGE","unified":"1F9F3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9f3.png","sheet_x":54,"sheet_y":37,"short_name":"luggage","short_names":["luggage"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"hotel","sort_order":986,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LOTION BOTTLE","unified":"1F9F4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9f4.png","sheet_x":54,"sheet_y":38,"short_name":"lotion_bottle","short_names":["lotion_bottle"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1391,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SPOOL OF THREAD","unified":"1F9F5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9f5.png","sheet_x":54,"sheet_y":39,"short_name":"thread","short_names":["thread"],"text":null,"texts":null,"category":"Activities","subcategory":"arts & crafts","sort_order":1146,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BALL OF YARN","unified":"1F9F6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9f6.png","sheet_x":54,"sheet_y":40,"short_name":"yarn","short_names":["yarn"],"text":null,"texts":null,"category":"Activities","subcategory":"arts & crafts","sort_order":1148,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SAFETY PIN","unified":"1F9F7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9f7.png","sheet_x":54,"sheet_y":41,"short_name":"safety_pin","short_names":["safety_pin"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1392,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TEDDY BEAR","unified":"1F9F8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9f8.png","sheet_x":54,"sheet_y":42,"short_name":"teddy_bear","short_names":["teddy_bear"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1131,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BROOM","unified":"1F9F9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9f9.png","sheet_x":54,"sheet_y":43,"short_name":"broom","short_names":["broom"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1393,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BASKET","unified":"1F9FA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9fa.png","sheet_x":54,"sheet_y":44,"short_name":"basket","short_names":["basket"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1394,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ROLL OF PAPER","unified":"1F9FB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9fb.png","sheet_x":54,"sheet_y":45,"short_name":"roll_of_paper","short_names":["roll_of_paper"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1395,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BAR OF SOAP","unified":"1F9FC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9fc.png","sheet_x":54,"sheet_y":46,"short_name":"soap","short_names":["soap"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1397,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SPONGE","unified":"1F9FD","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9fd.png","sheet_x":54,"sheet_y":47,"short_name":"sponge","short_names":["sponge"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1400,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RECEIPT","unified":"1F9FE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9fe.png","sheet_x":54,"sheet_y":48,"short_name":"receipt","short_names":["receipt"],"text":null,"texts":null,"category":"Objects","subcategory":"money","sort_order":1287,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NAZAR AMULET","unified":"1F9FF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1f9ff.png","sheet_x":54,"sheet_y":49,"short_name":"nazar_amulet","short_names":["nazar_amulet"],"text":null,"texts":null,"category":"Objects","subcategory":"other-object","sort_order":1407,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BALLET SHOES","unified":"1FA70","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa70.png","sheet_x":54,"sheet_y":50,"short_name":"ballet_shoes","short_names":["ballet_shoes"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1183,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ONE-PIECE SWIMSUIT","unified":"1FA71","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa71.png","sheet_x":54,"sheet_y":51,"short_name":"one-piece_swimsuit","short_names":["one-piece_swimsuit"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1165,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BRIEFS","unified":"1FA72","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa72.png","sheet_x":54,"sheet_y":52,"short_name":"briefs","short_names":["briefs"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1166,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SHORTS","unified":"1FA73","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa73.png","sheet_x":54,"sheet_y":53,"short_name":"shorts","short_names":["shorts"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1167,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"THONG SANDAL","unified":"1FA74","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa74.png","sheet_x":54,"sheet_y":54,"short_name":"thong_sandal","short_names":["thong_sandal"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1176,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LIGHT BLUE HEART","unified":"1FA75","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa75.png","sheet_x":54,"sheet_y":55,"short_name":"light_blue_heart","short_names":["light_blue_heart"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"heart","sort_order":149,"added_in":"15.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GREY HEART","unified":"1FA76","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa76.png","sheet_x":54,"sheet_y":56,"short_name":"grey_heart","short_names":["grey_heart"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"heart","sort_order":153,"added_in":"15.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PINK HEART","unified":"1FA77","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa77.png","sheet_x":54,"sheet_y":57,"short_name":"pink_heart","short_names":["pink_heart"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"heart","sort_order":144,"added_in":"15.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DROP OF BLOOD","unified":"1FA78","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa78.png","sheet_x":54,"sheet_y":58,"short_name":"drop_of_blood","short_names":["drop_of_blood"],"text":null,"texts":null,"category":"Objects","subcategory":"medical","sort_order":1372,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ADHESIVE BANDAGE","unified":"1FA79","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa79.png","sheet_x":54,"sheet_y":59,"short_name":"adhesive_bandage","short_names":["adhesive_bandage"],"text":null,"texts":null,"category":"Objects","subcategory":"medical","sort_order":1374,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"STETHOSCOPE","unified":"1FA7A","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa7a.png","sheet_x":54,"sheet_y":60,"short_name":"stethoscope","short_names":["stethoscope"],"text":null,"texts":null,"category":"Objects","subcategory":"medical","sort_order":1376,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"X-RAY","unified":"1FA7B","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa7b.png","sheet_x":54,"sheet_y":61,"short_name":"x-ray","short_names":["x-ray"],"text":null,"texts":null,"category":"Objects","subcategory":"medical","sort_order":1377,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CRUTCH","unified":"1FA7C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa7c.png","sheet_x":55,"sheet_y":0,"short_name":"crutch","short_names":["crutch"],"text":null,"texts":null,"category":"Objects","subcategory":"medical","sort_order":1375,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"YO-YO","unified":"1FA80","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa80.png","sheet_x":55,"sheet_y":1,"short_name":"yo-yo","short_names":["yo-yo"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1120,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"KITE","unified":"1FA81","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa81.png","sheet_x":55,"sheet_y":2,"short_name":"kite","short_names":["kite"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1121,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PARACHUTE","unified":"1FA82","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa82.png","sheet_x":55,"sheet_y":3,"short_name":"parachute","short_names":["parachute"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-air","sort_order":976,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BOOMERANG","unified":"1FA83","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa83.png","sheet_x":55,"sheet_y":4,"short_name":"boomerang","short_names":["boomerang"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1346,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MAGIC WAND","unified":"1FA84","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa84.png","sheet_x":55,"sheet_y":5,"short_name":"magic_wand","short_names":["magic_wand"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1125,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PINATA","unified":"1FA85","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa85.png","sheet_x":55,"sheet_y":6,"short_name":"pinata","short_names":["pinata"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1132,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NESTING DOLLS","unified":"1FA86","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa86.png","sheet_x":55,"sheet_y":7,"short_name":"nesting_dolls","short_names":["nesting_dolls"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1134,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MARACAS","unified":"1FA87","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa87.png","sheet_x":55,"sheet_y":8,"short_name":"maracas","short_names":["maracas"],"text":null,"texts":null,"category":"Objects","subcategory":"musical-instrument","sort_order":1224,"added_in":"15.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FLUTE","unified":"1FA88","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa88.png","sheet_x":55,"sheet_y":9,"short_name":"flute","short_names":["flute"],"text":null,"texts":null,"category":"Objects","subcategory":"musical-instrument","sort_order":1225,"added_in":"15.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RINGED PLANET","unified":"1FA90","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa90.png","sheet_x":55,"sheet_y":10,"short_name":"ringed_planet","short_names":["ringed_planet"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1034,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CHAIR","unified":"1FA91","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa91.png","sheet_x":55,"sheet_y":11,"short_name":"chair","short_names":["chair"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1384,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RAZOR","unified":"1FA92","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa92.png","sheet_x":55,"sheet_y":12,"short_name":"razor","short_names":["razor"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1390,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"AXE","unified":"1FA93","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa93.png","sheet_x":55,"sheet_y":13,"short_name":"axe","short_names":["axe"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1339,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DIYA LAMP","unified":"1FA94","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa94.png","sheet_x":55,"sheet_y":14,"short_name":"diya_lamp","short_names":["diya_lamp"],"text":null,"texts":null,"category":"Objects","subcategory":"light & video","sort_order":1261,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BANJO","unified":"1FA95","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa95.png","sheet_x":55,"sheet_y":15,"short_name":"banjo","short_names":["banjo"],"text":null,"texts":null,"category":"Objects","subcategory":"musical-instrument","sort_order":1221,"added_in":"12.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MILITARY HELMET","unified":"1FA96","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa96.png","sheet_x":55,"sheet_y":16,"short_name":"military_helmet","short_names":["military_helmet"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1191,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ACCORDION","unified":"1FA97","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa97.png","sheet_x":55,"sheet_y":17,"short_name":"accordion","short_names":["accordion"],"text":null,"texts":null,"category":"Objects","subcategory":"musical-instrument","sort_order":1216,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LONG DRUM","unified":"1FA98","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa98.png","sheet_x":55,"sheet_y":18,"short_name":"long_drum","short_names":["long_drum"],"text":null,"texts":null,"category":"Objects","subcategory":"musical-instrument","sort_order":1223,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"COIN","unified":"1FA99","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa99.png","sheet_x":55,"sheet_y":19,"short_name":"coin","short_names":["coin"],"text":null,"texts":null,"category":"Objects","subcategory":"money","sort_order":1280,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CARPENTRY SAW","unified":"1FA9A","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa9a.png","sheet_x":55,"sheet_y":20,"short_name":"carpentry_saw","short_names":["carpentry_saw"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1349,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SCREWDRIVER","unified":"1FA9B","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa9b.png","sheet_x":55,"sheet_y":21,"short_name":"screwdriver","short_names":["screwdriver"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1351,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LADDER","unified":"1FA9C","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa9c.png","sheet_x":55,"sheet_y":22,"short_name":"ladder","short_names":["ladder"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1363,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HOOK","unified":"1FA9D","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa9d.png","sheet_x":55,"sheet_y":23,"short_name":"hook","short_names":["hook"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1360,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MIRROR","unified":"1FA9E","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa9e.png","sheet_x":55,"sheet_y":24,"short_name":"mirror","short_names":["mirror"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1380,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WINDOW","unified":"1FA9F","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fa9f.png","sheet_x":55,"sheet_y":25,"short_name":"window","short_names":["window"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1381,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PLUNGER","unified":"1FAA0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faa0.png","sheet_x":55,"sheet_y":26,"short_name":"plunger","short_names":["plunger"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1386,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SEWING NEEDLE","unified":"1FAA1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faa1.png","sheet_x":55,"sheet_y":27,"short_name":"sewing_needle","short_names":["sewing_needle"],"text":null,"texts":null,"category":"Activities","subcategory":"arts & crafts","sort_order":1147,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"KNOT","unified":"1FAA2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faa2.png","sheet_x":55,"sheet_y":28,"short_name":"knot","short_names":["knot"],"text":null,"texts":null,"category":"Activities","subcategory":"arts & crafts","sort_order":1149,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BUCKET","unified":"1FAA3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faa3.png","sheet_x":55,"sheet_y":29,"short_name":"bucket","short_names":["bucket"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1396,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MOUSE TRAP","unified":"1FAA4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faa4.png","sheet_x":55,"sheet_y":30,"short_name":"mouse_trap","short_names":["mouse_trap"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1389,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TOOTHBRUSH","unified":"1FAA5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faa5.png","sheet_x":55,"sheet_y":31,"short_name":"toothbrush","short_names":["toothbrush"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1399,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HEADSTONE","unified":"1FAA6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faa6.png","sheet_x":55,"sheet_y":32,"short_name":"headstone","short_names":["headstone"],"text":null,"texts":null,"category":"Objects","subcategory":"other-object","sort_order":1405,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PLACARD","unified":"1FAA7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faa7.png","sheet_x":55,"sheet_y":33,"short_name":"placard","short_names":["placard"],"text":null,"texts":null,"category":"Objects","subcategory":"other-object","sort_order":1410,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ROCK","unified":"1FAA8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faa8.png","sheet_x":55,"sheet_y":34,"short_name":"rock","short_names":["rock"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":867,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MIRROR BALL","unified":"1FAA9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faa9.png","sheet_x":55,"sheet_y":35,"short_name":"mirror_ball","short_names":["mirror_ball"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1133,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"IDENTIFICATION CARD","unified":"1FAAA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faaa.png","sheet_x":55,"sheet_y":36,"short_name":"identification_card","short_names":["identification_card"],"text":null,"texts":null,"category":"Objects","subcategory":"other-object","sort_order":1411,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LOW BATTERY","unified":"1FAAB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faab.png","sheet_x":55,"sheet_y":37,"short_name":"low_battery","short_names":["low_battery"],"text":null,"texts":null,"category":"Objects","subcategory":"computer","sort_order":1233,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HAMSA","unified":"1FAAC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faac.png","sheet_x":55,"sheet_y":38,"short_name":"hamsa","short_names":["hamsa"],"text":null,"texts":null,"category":"Objects","subcategory":"other-object","sort_order":1408,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FOLDING HAND FAN","unified":"1FAAD","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faad.png","sheet_x":55,"sheet_y":39,"short_name":"folding_hand_fan","short_names":["folding_hand_fan"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1170,"added_in":"15.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HAIR PICK","unified":"1FAAE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faae.png","sheet_x":55,"sheet_y":40,"short_name":"hair_pick","short_names":["hair_pick"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1185,"added_in":"15.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"KHANDA","unified":"1FAAF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faaf.png","sheet_x":55,"sheet_y":41,"short_name":"khanda","short_names":["khanda"],"text":null,"texts":null,"category":"Symbols","subcategory":"religion","sort_order":1471,"added_in":"15.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FLY","unified":"1FAB0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fab0.png","sheet_x":55,"sheet_y":42,"short_name":"fly","short_names":["fly"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bug","sort_order":681,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WORM","unified":"1FAB1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fab1.png","sheet_x":55,"sheet_y":43,"short_name":"worm","short_names":["worm"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bug","sort_order":682,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BEETLE","unified":"1FAB2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fab2.png","sheet_x":55,"sheet_y":44,"short_name":"beetle","short_names":["beetle"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bug","sort_order":673,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"COCKROACH","unified":"1FAB3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fab3.png","sheet_x":55,"sheet_y":45,"short_name":"cockroach","short_names":["cockroach"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bug","sort_order":676,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"POTTED PLANT","unified":"1FAB4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fab4.png","sheet_x":55,"sheet_y":46,"short_name":"potted_plant","short_names":["potted_plant"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-other","sort_order":697,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WOOD","unified":"1FAB5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fab5.png","sheet_x":55,"sheet_y":47,"short_name":"wood","short_names":["wood"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-building","sort_order":868,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FEATHER","unified":"1FAB6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fab6.png","sheet_x":55,"sheet_y":48,"short_name":"feather","short_names":["feather"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bird","sort_order":639,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LOTUS","unified":"1FAB7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fab7.png","sheet_x":55,"sheet_y":49,"short_name":"lotus","short_names":["lotus"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-flower","sort_order":687,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CORAL","unified":"1FAB8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fab8.png","sheet_x":55,"sheet_y":50,"short_name":"coral","short_names":["coral"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-marine","sort_order":666,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EMPTY NEST","unified":"1FAB9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fab9.png","sheet_x":55,"sheet_y":51,"short_name":"empty_nest","short_names":["empty_nest"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-other","sort_order":709,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NEST WITH EGGS","unified":"1FABA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faba.png","sheet_x":55,"sheet_y":52,"short_name":"nest_with_eggs","short_names":["nest_with_eggs"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-other","sort_order":710,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HYACINTH","unified":"1FABB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fabb.png","sheet_x":55,"sheet_y":53,"short_name":"hyacinth","short_names":["hyacinth"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-flower","sort_order":695,"added_in":"15.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"JELLYFISH","unified":"1FABC","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fabc.png","sheet_x":55,"sheet_y":54,"short_name":"jellyfish","short_names":["jellyfish"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-marine","sort_order":667,"added_in":"15.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WING","unified":"1FABD","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fabd.png","sheet_x":55,"sheet_y":55,"short_name":"wing","short_names":["wing"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bird","sort_order":643,"added_in":"15.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GOOSE","unified":"1FABF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fabf.png","sheet_x":55,"sheet_y":56,"short_name":"goose","short_names":["goose"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-bird","sort_order":645,"added_in":"15.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ANATOMICAL HEART","unified":"1FAC0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fac0.png","sheet_x":55,"sheet_y":57,"short_name":"anatomical_heart","short_names":["anatomical_heart"],"text":null,"texts":null,"category":"People & Body","subcategory":"body-parts","sort_order":221,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LUNGS","unified":"1FAC1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fac1.png","sheet_x":55,"sheet_y":58,"short_name":"lungs","short_names":["lungs"],"text":null,"texts":null,"category":"People & Body","subcategory":"body-parts","sort_order":222,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PEOPLE HUGGING","unified":"1FAC2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fac2.png","sheet_x":55,"sheet_y":59,"short_name":"people_hugging","short_names":["people_hugging"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-symbol","sort_order":547,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PREGNANT MAN","unified":"1FAC3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fac3.png","sheet_x":55,"sheet_y":60,"short_name":"pregnant_man","short_names":["pregnant_man"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":364,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1FAC3-1F3FB","non_qualified":null,"image":"1fac3-1f3fb.png","sheet_x":55,"sheet_y":61,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1FAC3-1F3FC","non_qualified":null,"image":"1fac3-1f3fc.png","sheet_x":56,"sheet_y":0,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1FAC3-1F3FD","non_qualified":null,"image":"1fac3-1f3fd.png","sheet_x":56,"sheet_y":1,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1FAC3-1F3FE","non_qualified":null,"image":"1fac3-1f3fe.png","sheet_x":56,"sheet_y":2,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1FAC3-1F3FF","non_qualified":null,"image":"1fac3-1f3ff.png","sheet_x":56,"sheet_y":3,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"PREGNANT PERSON","unified":"1FAC4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fac4.png","sheet_x":56,"sheet_y":4,"short_name":"pregnant_person","short_names":["pregnant_person"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":365,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1FAC4-1F3FB","non_qualified":null,"image":"1fac4-1f3fb.png","sheet_x":56,"sheet_y":5,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1FAC4-1F3FC","non_qualified":null,"image":"1fac4-1f3fc.png","sheet_x":56,"sheet_y":6,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1FAC4-1F3FD","non_qualified":null,"image":"1fac4-1f3fd.png","sheet_x":56,"sheet_y":7,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1FAC4-1F3FE","non_qualified":null,"image":"1fac4-1f3fe.png","sheet_x":56,"sheet_y":8,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1FAC4-1F3FF","non_qualified":null,"image":"1fac4-1f3ff.png","sheet_x":56,"sheet_y":9,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"PERSON WITH CROWN","unified":"1FAC5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fac5.png","sheet_x":56,"sheet_y":10,"short_name":"person_with_crown","short_names":["person_with_crown"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-role","sort_order":349,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1FAC5-1F3FB","non_qualified":null,"image":"1fac5-1f3fb.png","sheet_x":56,"sheet_y":11,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1FAC5-1F3FC","non_qualified":null,"image":"1fac5-1f3fc.png","sheet_x":56,"sheet_y":12,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1FAC5-1F3FD","non_qualified":null,"image":"1fac5-1f3fd.png","sheet_x":56,"sheet_y":13,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1FAC5-1F3FE","non_qualified":null,"image":"1fac5-1f3fe.png","sheet_x":56,"sheet_y":14,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1FAC5-1F3FF","non_qualified":null,"image":"1fac5-1f3ff.png","sheet_x":56,"sheet_y":15,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MOOSE","unified":"1FACE","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1face.png","sheet_x":56,"sheet_y":16,"short_name":"moose","short_names":["moose"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":579,"added_in":"15.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DONKEY","unified":"1FACF","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1facf.png","sheet_x":56,"sheet_y":17,"short_name":"donkey","short_names":["donkey"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"animal-mammal","sort_order":580,"added_in":"15.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLUEBERRIES","unified":"1FAD0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fad0.png","sheet_x":56,"sheet_y":18,"short_name":"blueberries","short_names":["blueberries"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-fruit","sort_order":727,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BELL PEPPER","unified":"1FAD1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fad1.png","sheet_x":56,"sheet_y":19,"short_name":"bell_pepper","short_names":["bell_pepper"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-vegetable","sort_order":738,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"OLIVE","unified":"1FAD2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fad2.png","sheet_x":56,"sheet_y":20,"short_name":"olive","short_names":["olive"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-fruit","sort_order":730,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FLATBREAD","unified":"1FAD3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fad3.png","sheet_x":56,"sheet_y":21,"short_name":"flatbread","short_names":["flatbread"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":753,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TAMALE","unified":"1FAD4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fad4.png","sheet_x":56,"sheet_y":22,"short_name":"tamale","short_names":["tamale"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":770,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FONDUE","unified":"1FAD5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fad5.png","sheet_x":56,"sheet_y":23,"short_name":"fondue","short_names":["fondue"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-prepared","sort_order":777,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TEAPOT","unified":"1FAD6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fad6.png","sheet_x":56,"sheet_y":24,"short_name":"teapot","short_names":["teapot"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"drink","sort_order":823,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"POURING LIQUID","unified":"1FAD7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fad7.png","sheet_x":56,"sheet_y":25,"short_name":"pouring_liquid","short_names":["pouring_liquid"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"drink","sort_order":834,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BEANS","unified":"1FAD8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fad8.png","sheet_x":56,"sheet_y":26,"short_name":"beans","short_names":["beans"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-vegetable","sort_order":745,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"JAR","unified":"1FAD9","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fad9.png","sheet_x":56,"sheet_y":27,"short_name":"jar","short_names":["jar"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"dishware","sort_order":845,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GINGER ROOT","unified":"1FADA","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fada.png","sheet_x":56,"sheet_y":28,"short_name":"ginger_root","short_names":["ginger_root"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-vegetable","sort_order":747,"added_in":"15.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PEA POD","unified":"1FADB","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fadb.png","sheet_x":56,"sheet_y":29,"short_name":"pea_pod","short_names":["pea_pod"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"food-vegetable","sort_order":748,"added_in":"15.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MELTING FACE","unified":"1FAE0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fae0.png","sheet_x":56,"sheet_y":30,"short_name":"melting_face","short_names":["melting_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-smiling","sort_order":11,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SALUTING FACE","unified":"1FAE1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fae1.png","sheet_x":56,"sheet_y":31,"short_name":"saluting_face","short_names":["saluting_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-hand","sort_order":36,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE WITH OPEN EYES AND HAND OVER MOUTH","unified":"1FAE2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fae2.png","sheet_x":56,"sheet_y":32,"short_name":"face_with_open_eyes_and_hand_over_mouth","short_names":["face_with_open_eyes_and_hand_over_mouth"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-hand","sort_order":32,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE WITH PEEKING EYE","unified":"1FAE3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fae3.png","sheet_x":56,"sheet_y":33,"short_name":"face_with_peeking_eye","short_names":["face_with_peeking_eye"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-hand","sort_order":33,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FACE WITH DIAGONAL MOUTH","unified":"1FAE4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fae4.png","sheet_x":56,"sheet_y":34,"short_name":"face_with_diagonal_mouth","short_names":["face_with_diagonal_mouth"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":77,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DOTTED LINE FACE","unified":"1FAE5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fae5.png","sheet_x":56,"sheet_y":35,"short_name":"dotted_line_face","short_names":["dotted_line_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-neutral-skeptical","sort_order":42,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BITING LIP","unified":"1FAE6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fae6.png","sheet_x":56,"sheet_y":36,"short_name":"biting_lip","short_names":["biting_lip"],"text":null,"texts":null,"category":"People & Body","subcategory":"body-parts","sort_order":229,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BUBBLES","unified":"1FAE7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fae7.png","sheet_x":56,"sheet_y":37,"short_name":"bubbles","short_names":["bubbles"],"text":null,"texts":null,"category":"Objects","subcategory":"household","sort_order":1398,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SHAKING FACE","unified":"1FAE8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1fae8.png","sheet_x":56,"sheet_y":38,"short_name":"shaking_face","short_names":["shaking_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-neutral-skeptical","sort_order":50,"added_in":"15.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HAND WITH INDEX FINGER AND THUMB CROSSED","unified":"1FAF0","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faf0.png","sheet_x":56,"sheet_y":39,"short_name":"hand_with_index_finger_and_thumb_crossed","short_names":["hand_with_index_finger_and_thumb_crossed"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-partial","sort_order":185,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1FAF0-1F3FB","non_qualified":null,"image":"1faf0-1f3fb.png","sheet_x":56,"sheet_y":40,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1FAF0-1F3FC","non_qualified":null,"image":"1faf0-1f3fc.png","sheet_x":56,"sheet_y":41,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1FAF0-1F3FD","non_qualified":null,"image":"1faf0-1f3fd.png","sheet_x":56,"sheet_y":42,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1FAF0-1F3FE","non_qualified":null,"image":"1faf0-1f3fe.png","sheet_x":56,"sheet_y":43,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1FAF0-1F3FF","non_qualified":null,"image":"1faf0-1f3ff.png","sheet_x":56,"sheet_y":44,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"RIGHTWARDS HAND","unified":"1FAF1","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faf1.png","sheet_x":56,"sheet_y":45,"short_name":"rightwards_hand","short_names":["rightwards_hand"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-open","sort_order":174,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1FAF1-1F3FB","non_qualified":null,"image":"1faf1-1f3fb.png","sheet_x":56,"sheet_y":46,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1FAF1-1F3FC","non_qualified":null,"image":"1faf1-1f3fc.png","sheet_x":56,"sheet_y":47,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1FAF1-1F3FD","non_qualified":null,"image":"1faf1-1f3fd.png","sheet_x":56,"sheet_y":48,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1FAF1-1F3FE","non_qualified":null,"image":"1faf1-1f3fe.png","sheet_x":56,"sheet_y":49,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1FAF1-1F3FF","non_qualified":null,"image":"1faf1-1f3ff.png","sheet_x":56,"sheet_y":50,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"LEFTWARDS HAND","unified":"1FAF2","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faf2.png","sheet_x":56,"sheet_y":51,"short_name":"leftwards_hand","short_names":["leftwards_hand"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-open","sort_order":175,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1FAF2-1F3FB","non_qualified":null,"image":"1faf2-1f3fb.png","sheet_x":56,"sheet_y":52,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1FAF2-1F3FC","non_qualified":null,"image":"1faf2-1f3fc.png","sheet_x":56,"sheet_y":53,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1FAF2-1F3FD","non_qualified":null,"image":"1faf2-1f3fd.png","sheet_x":56,"sheet_y":54,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1FAF2-1F3FE","non_qualified":null,"image":"1faf2-1f3fe.png","sheet_x":56,"sheet_y":55,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1FAF2-1F3FF","non_qualified":null,"image":"1faf2-1f3ff.png","sheet_x":56,"sheet_y":56,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"PALM DOWN HAND","unified":"1FAF3","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faf3.png","sheet_x":56,"sheet_y":57,"short_name":"palm_down_hand","short_names":["palm_down_hand"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-open","sort_order":176,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1FAF3-1F3FB","non_qualified":null,"image":"1faf3-1f3fb.png","sheet_x":56,"sheet_y":58,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1FAF3-1F3FC","non_qualified":null,"image":"1faf3-1f3fc.png","sheet_x":56,"sheet_y":59,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1FAF3-1F3FD","non_qualified":null,"image":"1faf3-1f3fd.png","sheet_x":56,"sheet_y":60,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1FAF3-1F3FE","non_qualified":null,"image":"1faf3-1f3fe.png","sheet_x":56,"sheet_y":61,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1FAF3-1F3FF","non_qualified":null,"image":"1faf3-1f3ff.png","sheet_x":57,"sheet_y":0,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"PALM UP HAND","unified":"1FAF4","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faf4.png","sheet_x":57,"sheet_y":1,"short_name":"palm_up_hand","short_names":["palm_up_hand"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-open","sort_order":177,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1FAF4-1F3FB","non_qualified":null,"image":"1faf4-1f3fb.png","sheet_x":57,"sheet_y":2,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1FAF4-1F3FC","non_qualified":null,"image":"1faf4-1f3fc.png","sheet_x":57,"sheet_y":3,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1FAF4-1F3FD","non_qualified":null,"image":"1faf4-1f3fd.png","sheet_x":57,"sheet_y":4,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1FAF4-1F3FE","non_qualified":null,"image":"1faf4-1f3fe.png","sheet_x":57,"sheet_y":5,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1FAF4-1F3FF","non_qualified":null,"image":"1faf4-1f3ff.png","sheet_x":57,"sheet_y":6,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"INDEX POINTING AT THE VIEWER","unified":"1FAF5","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faf5.png","sheet_x":57,"sheet_y":7,"short_name":"index_pointing_at_the_viewer","short_names":["index_pointing_at_the_viewer"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-single-finger","sort_order":195,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1FAF5-1F3FB","non_qualified":null,"image":"1faf5-1f3fb.png","sheet_x":57,"sheet_y":8,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1FAF5-1F3FC","non_qualified":null,"image":"1faf5-1f3fc.png","sheet_x":57,"sheet_y":9,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1FAF5-1F3FD","non_qualified":null,"image":"1faf5-1f3fd.png","sheet_x":57,"sheet_y":10,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1FAF5-1F3FE","non_qualified":null,"image":"1faf5-1f3fe.png","sheet_x":57,"sheet_y":11,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1FAF5-1F3FF","non_qualified":null,"image":"1faf5-1f3ff.png","sheet_x":57,"sheet_y":12,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"HEART HANDS","unified":"1FAF6","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faf6.png","sheet_x":57,"sheet_y":13,"short_name":"heart_hands","short_names":["heart_hands"],"text":null,"texts":null,"category":"People & Body","subcategory":"hands","sort_order":204,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1FAF6-1F3FB","non_qualified":null,"image":"1faf6-1f3fb.png","sheet_x":57,"sheet_y":14,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1FAF6-1F3FC","non_qualified":null,"image":"1faf6-1f3fc.png","sheet_x":57,"sheet_y":15,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1FAF6-1F3FD","non_qualified":null,"image":"1faf6-1f3fd.png","sheet_x":57,"sheet_y":16,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1FAF6-1F3FE","non_qualified":null,"image":"1faf6-1f3fe.png","sheet_x":57,"sheet_y":17,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1FAF6-1F3FF","non_qualified":null,"image":"1faf6-1f3ff.png","sheet_x":57,"sheet_y":18,"added_in":"14.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"LEFTWARDS PUSHING HAND","unified":"1FAF7","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faf7.png","sheet_x":57,"sheet_y":19,"short_name":"leftwards_pushing_hand","short_names":["leftwards_pushing_hand"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-open","sort_order":178,"added_in":"15.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1FAF7-1F3FB","non_qualified":null,"image":"1faf7-1f3fb.png","sheet_x":57,"sheet_y":20,"added_in":"15.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1FAF7-1F3FC","non_qualified":null,"image":"1faf7-1f3fc.png","sheet_x":57,"sheet_y":21,"added_in":"15.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1FAF7-1F3FD","non_qualified":null,"image":"1faf7-1f3fd.png","sheet_x":57,"sheet_y":22,"added_in":"15.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1FAF7-1F3FE","non_qualified":null,"image":"1faf7-1f3fe.png","sheet_x":57,"sheet_y":23,"added_in":"15.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1FAF7-1F3FF","non_qualified":null,"image":"1faf7-1f3ff.png","sheet_x":57,"sheet_y":24,"added_in":"15.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"RIGHTWARDS PUSHING HAND","unified":"1FAF8","non_qualified":null,"docomo":null,"au":null,"softbank":null,"google":null,"image":"1faf8.png","sheet_x":57,"sheet_y":25,"short_name":"rightwards_pushing_hand","short_names":["rightwards_pushing_hand"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-open","sort_order":179,"added_in":"15.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"1FAF8-1F3FB","non_qualified":null,"image":"1faf8-1f3fb.png","sheet_x":57,"sheet_y":26,"added_in":"15.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"1FAF8-1F3FC","non_qualified":null,"image":"1faf8-1f3fc.png","sheet_x":57,"sheet_y":27,"added_in":"15.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"1FAF8-1F3FD","non_qualified":null,"image":"1faf8-1f3fd.png","sheet_x":57,"sheet_y":28,"added_in":"15.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"1FAF8-1F3FE","non_qualified":null,"image":"1faf8-1f3fe.png","sheet_x":57,"sheet_y":29,"added_in":"15.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"1FAF8-1F3FF","non_qualified":null,"image":"1faf8-1f3ff.png","sheet_x":57,"sheet_y":30,"added_in":"15.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"DOUBLE EXCLAMATION MARK","unified":"203C-FE0F","non_qualified":"203C","docomo":"E704","au":"EB30","softbank":null,"google":"FEB06","image":"203c-fe0f.png","sheet_x":57,"sheet_y":31,"short_name":"bangbang","short_names":["bangbang"],"text":null,"texts":null,"category":"Symbols","subcategory":"punctuation","sort_order":1519,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EXCLAMATION QUESTION MARK","unified":"2049-FE0F","non_qualified":"2049","docomo":"E703","au":"EB2F","softbank":null,"google":"FEB05","image":"2049-fe0f.png","sheet_x":57,"sheet_y":32,"short_name":"interrobang","short_names":["interrobang"],"text":null,"texts":null,"category":"Symbols","subcategory":"punctuation","sort_order":1520,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TRADE MARK SIGN","unified":"2122-FE0F","non_qualified":"2122","docomo":"E732","au":"E54E","softbank":"E537","google":"FEB2A","image":"2122-fe0f.png","sheet_x":57,"sheet_y":33,"short_name":"tm","short_names":["tm"],"text":null,"texts":null,"category":"Symbols","subcategory":"other-symbol","sort_order":1548,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"INFORMATION SOURCE","unified":"2139-FE0F","non_qualified":"2139","docomo":null,"au":"E533","softbank":null,"google":"FEB47","image":"2139-fe0f.png","sheet_x":57,"sheet_y":34,"short_name":"information_source","short_names":["information_source"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1573,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LEFT RIGHT ARROW","unified":"2194-FE0F","non_qualified":"2194","docomo":"E73C","au":"EB7A","softbank":null,"google":"FEAF6","image":"2194-fe0f.png","sheet_x":57,"sheet_y":35,"short_name":"left_right_arrow","short_names":["left_right_arrow"],"text":null,"texts":null,"category":"Symbols","subcategory":"arrow","sort_order":1447,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"UP DOWN ARROW","unified":"2195-FE0F","non_qualified":"2195","docomo":"E73D","au":"EB7B","softbank":null,"google":"FEAF7","image":"2195-fe0f.png","sheet_x":57,"sheet_y":36,"short_name":"arrow_up_down","short_names":["arrow_up_down"],"text":null,"texts":null,"category":"Symbols","subcategory":"arrow","sort_order":1446,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NORTH WEST ARROW","unified":"2196-FE0F","non_qualified":"2196","docomo":"E697","au":"E54C","softbank":"E237","google":"FEAF2","image":"2196-fe0f.png","sheet_x":57,"sheet_y":37,"short_name":"arrow_upper_left","short_names":["arrow_upper_left"],"text":null,"texts":null,"category":"Symbols","subcategory":"arrow","sort_order":1445,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NORTH EAST ARROW","unified":"2197-FE0F","non_qualified":"2197","docomo":"E678","au":"E555","softbank":"E236","google":"FEAF0","image":"2197-fe0f.png","sheet_x":57,"sheet_y":38,"short_name":"arrow_upper_right","short_names":["arrow_upper_right"],"text":null,"texts":null,"category":"Symbols","subcategory":"arrow","sort_order":1439,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SOUTH EAST ARROW","unified":"2198-FE0F","non_qualified":"2198","docomo":"E696","au":"E54D","softbank":"E238","google":"FEAF1","image":"2198-fe0f.png","sheet_x":57,"sheet_y":39,"short_name":"arrow_lower_right","short_names":["arrow_lower_right"],"text":null,"texts":null,"category":"Symbols","subcategory":"arrow","sort_order":1441,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SOUTH WEST ARROW","unified":"2199-FE0F","non_qualified":"2199","docomo":"E6A5","au":"E556","softbank":"E239","google":"FEAF3","image":"2199-fe0f.png","sheet_x":57,"sheet_y":40,"short_name":"arrow_lower_left","short_names":["arrow_lower_left"],"text":null,"texts":null,"category":"Symbols","subcategory":"arrow","sort_order":1443,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LEFTWARDS ARROW WITH HOOK","unified":"21A9-FE0F","non_qualified":"21A9","docomo":"E6DA","au":"E55D","softbank":null,"google":"FEB83","image":"21a9-fe0f.png","sheet_x":57,"sheet_y":41,"short_name":"leftwards_arrow_with_hook","short_names":["leftwards_arrow_with_hook"],"text":null,"texts":null,"category":"Symbols","subcategory":"arrow","sort_order":1448,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RIGHTWARDS ARROW WITH HOOK","unified":"21AA-FE0F","non_qualified":"21AA","docomo":null,"au":"E55C","softbank":null,"google":"FEB88","image":"21aa-fe0f.png","sheet_x":57,"sheet_y":42,"short_name":"arrow_right_hook","short_names":["arrow_right_hook"],"text":null,"texts":null,"category":"Symbols","subcategory":"arrow","sort_order":1449,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WATCH","unified":"231A","non_qualified":null,"docomo":"E71F","au":"E57A","softbank":null,"google":"FE01D","image":"231a.png","sheet_x":57,"sheet_y":43,"short_name":"watch","short_names":["watch"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":989,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HOURGLASS","unified":"231B","non_qualified":null,"docomo":"E71C","au":"E57B","softbank":null,"google":"FE01C","image":"231b.png","sheet_x":57,"sheet_y":44,"short_name":"hourglass","short_names":["hourglass"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":987,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"KEYBOARD","unified":"2328-FE0F","non_qualified":"2328","docomo":null,"au":null,"softbank":null,"google":null,"image":"2328-fe0f.png","sheet_x":57,"sheet_y":45,"short_name":"keyboard","short_names":["keyboard"],"text":null,"texts":null,"category":"Objects","subcategory":"computer","sort_order":1238,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EJECT BUTTON","unified":"23CF-FE0F","non_qualified":"23CF","docomo":null,"au":null,"softbank":null,"google":null,"image":"23cf-fe0f.png","sheet_x":57,"sheet_y":46,"short_name":"eject","short_names":["eject"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1502,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK RIGHT-POINTING DOUBLE TRIANGLE","unified":"23E9","non_qualified":null,"docomo":null,"au":"E530","softbank":"E23C","google":"FEAFE","image":"23e9.png","sheet_x":57,"sheet_y":47,"short_name":"fast_forward","short_names":["fast_forward"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1489,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK LEFT-POINTING DOUBLE TRIANGLE","unified":"23EA","non_qualified":null,"docomo":null,"au":"E52F","softbank":"E23D","google":"FEAFF","image":"23ea.png","sheet_x":57,"sheet_y":48,"short_name":"rewind","short_names":["rewind"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1493,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK UP-POINTING DOUBLE TRIANGLE","unified":"23EB","non_qualified":null,"docomo":null,"au":"E545","softbank":null,"google":"FEB03","image":"23eb.png","sheet_x":57,"sheet_y":49,"short_name":"arrow_double_up","short_names":["arrow_double_up"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1496,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK DOWN-POINTING DOUBLE TRIANGLE","unified":"23EC","non_qualified":null,"docomo":null,"au":"E544","softbank":null,"google":"FEB02","image":"23ec.png","sheet_x":57,"sheet_y":50,"short_name":"arrow_double_down","short_names":["arrow_double_down"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1498,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NEXT TRACK BUTTON","unified":"23ED-FE0F","non_qualified":"23ED","docomo":null,"au":null,"softbank":null,"google":null,"image":"23ed-fe0f.png","sheet_x":57,"sheet_y":51,"short_name":"black_right_pointing_double_triangle_with_vertical_bar","short_names":["black_right_pointing_double_triangle_with_vertical_bar"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1490,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LAST TRACK BUTTON","unified":"23EE-FE0F","non_qualified":"23EE","docomo":null,"au":null,"softbank":null,"google":null,"image":"23ee-fe0f.png","sheet_x":57,"sheet_y":52,"short_name":"black_left_pointing_double_triangle_with_vertical_bar","short_names":["black_left_pointing_double_triangle_with_vertical_bar"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1494,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PLAY OR PAUSE BUTTON","unified":"23EF-FE0F","non_qualified":"23EF","docomo":null,"au":null,"softbank":null,"google":null,"image":"23ef-fe0f.png","sheet_x":57,"sheet_y":53,"short_name":"black_right_pointing_triangle_with_double_vertical_bar","short_names":["black_right_pointing_triangle_with_double_vertical_bar"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1491,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ALARM CLOCK","unified":"23F0","non_qualified":null,"docomo":"E6BA","au":"E594","softbank":null,"google":"FE02A","image":"23f0.png","sheet_x":57,"sheet_y":54,"short_name":"alarm_clock","short_names":["alarm_clock"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":990,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"STOPWATCH","unified":"23F1-FE0F","non_qualified":"23F1","docomo":null,"au":null,"softbank":null,"google":null,"image":"23f1-fe0f.png","sheet_x":57,"sheet_y":55,"short_name":"stopwatch","short_names":["stopwatch"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":991,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TIMER CLOCK","unified":"23F2-FE0F","non_qualified":"23F2","docomo":null,"au":null,"softbank":null,"google":null,"image":"23f2-fe0f.png","sheet_x":57,"sheet_y":56,"short_name":"timer_clock","short_names":["timer_clock"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":992,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HOURGLASS WITH FLOWING SAND","unified":"23F3","non_qualified":null,"docomo":"E71C","au":"E47C","softbank":null,"google":"FE01B","image":"23f3.png","sheet_x":57,"sheet_y":57,"short_name":"hourglass_flowing_sand","short_names":["hourglass_flowing_sand"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"time","sort_order":988,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PAUSE BUTTON","unified":"23F8-FE0F","non_qualified":"23F8","docomo":null,"au":null,"softbank":null,"google":null,"image":"23f8-fe0f.png","sheet_x":57,"sheet_y":58,"short_name":"double_vertical_bar","short_names":["double_vertical_bar"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1499,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"STOP BUTTON","unified":"23F9-FE0F","non_qualified":"23F9","docomo":null,"au":null,"softbank":null,"google":null,"image":"23f9-fe0f.png","sheet_x":57,"sheet_y":59,"short_name":"black_square_for_stop","short_names":["black_square_for_stop"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1500,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RECORD BUTTON","unified":"23FA-FE0F","non_qualified":"23FA","docomo":null,"au":null,"softbank":null,"google":null,"image":"23fa-fe0f.png","sheet_x":57,"sheet_y":60,"short_name":"black_circle_for_record","short_names":["black_circle_for_record"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1501,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CIRCLED LATIN CAPITAL LETTER M","unified":"24C2-FE0F","non_qualified":"24C2","docomo":"E65C","au":"E5BC","softbank":null,"google":"FE7E1","image":"24c2-fe0f.png","sheet_x":57,"sheet_y":61,"short_name":"m","short_names":["m"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1575,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK SMALL SQUARE","unified":"25AA-FE0F","non_qualified":"25AA","docomo":null,"au":"E532","softbank":null,"google":"FEB6E","image":"25aa-fe0f.png","sheet_x":58,"sheet_y":0,"short_name":"black_small_square","short_names":["black_small_square"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1623,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WHITE SMALL SQUARE","unified":"25AB-FE0F","non_qualified":"25AB","docomo":null,"au":"E531","softbank":null,"google":"FEB6D","image":"25ab-fe0f.png","sheet_x":58,"sheet_y":1,"short_name":"white_small_square","short_names":["white_small_square"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1624,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK RIGHT-POINTING TRIANGLE","unified":"25B6-FE0F","non_qualified":"25B6","docomo":null,"au":"E52E","softbank":"E23A","google":"FEAFC","image":"25b6-fe0f.png","sheet_x":58,"sheet_y":2,"short_name":"arrow_forward","short_names":["arrow_forward"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1488,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK LEFT-POINTING TRIANGLE","unified":"25C0-FE0F","non_qualified":"25C0","docomo":null,"au":"E52D","softbank":"E23B","google":"FEAFD","image":"25c0-fe0f.png","sheet_x":58,"sheet_y":3,"short_name":"arrow_backward","short_names":["arrow_backward"],"text":null,"texts":null,"category":"Symbols","subcategory":"av-symbol","sort_order":1492,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WHITE MEDIUM SQUARE","unified":"25FB-FE0F","non_qualified":"25FB","docomo":null,"au":"E538","softbank":null,"google":"FEB71","image":"25fb-fe0f.png","sheet_x":58,"sheet_y":4,"short_name":"white_medium_square","short_names":["white_medium_square"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1620,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK MEDIUM SQUARE","unified":"25FC-FE0F","non_qualified":"25FC","docomo":null,"au":"E539","softbank":null,"google":"FEB72","image":"25fc-fe0f.png","sheet_x":58,"sheet_y":5,"short_name":"black_medium_square","short_names":["black_medium_square"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1619,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WHITE MEDIUM SMALL SQUARE","unified":"25FD","non_qualified":null,"docomo":null,"au":"E534","softbank":null,"google":"FEB6F","image":"25fd.png","sheet_x":58,"sheet_y":6,"short_name":"white_medium_small_square","short_names":["white_medium_small_square"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1622,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK MEDIUM SMALL SQUARE","unified":"25FE","non_qualified":null,"docomo":null,"au":"E535","softbank":null,"google":"FEB70","image":"25fe.png","sheet_x":58,"sheet_y":7,"short_name":"black_medium_small_square","short_names":["black_medium_small_square"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1621,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK SUN WITH RAYS","unified":"2600-FE0F","non_qualified":"2600","docomo":"E63E","au":"E488","softbank":"E04A","google":"FE000","image":"2600-fe0f.png","sheet_x":58,"sheet_y":8,"short_name":"sunny","short_names":["sunny"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1031,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOUD","unified":"2601-FE0F","non_qualified":"2601","docomo":"E63F","au":"E48D","softbank":"E049","google":"FE001","image":"2601-fe0f.png","sheet_x":58,"sheet_y":9,"short_name":"cloud","short_names":["cloud"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1039,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"UMBRELLA","unified":"2602-FE0F","non_qualified":"2602","docomo":null,"au":null,"softbank":null,"google":null,"image":"2602-fe0f.png","sheet_x":58,"sheet_y":10,"short_name":"umbrella","short_names":["umbrella"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1054,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SNOWMAN","unified":"2603-FE0F","non_qualified":"2603","docomo":null,"au":null,"softbank":null,"google":null,"image":"2603-fe0f.png","sheet_x":58,"sheet_y":11,"short_name":"snowman","short_names":["snowman"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1059,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"COMET","unified":"2604-FE0F","non_qualified":"2604","docomo":null,"au":null,"softbank":null,"google":null,"image":"2604-fe0f.png","sheet_x":58,"sheet_y":12,"short_name":"comet","short_names":["comet"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1061,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK TELEPHONE","unified":"260E-FE0F","non_qualified":"260E","docomo":"E687","au":"E596","softbank":"E009","google":"FE523","image":"260e-fe0f.png","sheet_x":58,"sheet_y":13,"short_name":"phone","short_names":["phone","telephone"],"text":null,"texts":null,"category":"Objects","subcategory":"phone","sort_order":1228,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BALLOT BOX WITH CHECK","unified":"2611-FE0F","non_qualified":"2611","docomo":null,"au":"EB02","softbank":null,"google":"FEB8B","image":"2611-fe0f.png","sheet_x":58,"sheet_y":14,"short_name":"ballot_box_with_check","short_names":["ballot_box_with_check"],"text":null,"texts":null,"category":"Symbols","subcategory":"other-symbol","sort_order":1536,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"UMBRELLA WITH RAIN DROPS","unified":"2614","non_qualified":null,"docomo":"E640","au":"E48C","softbank":"E04B","google":"FE002","image":"2614.png","sheet_x":58,"sheet_y":15,"short_name":"umbrella_with_rain_drops","short_names":["umbrella_with_rain_drops"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1055,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HOT BEVERAGE","unified":"2615","non_qualified":null,"docomo":"E670","au":"E597","softbank":"E045","google":"FE981","image":"2615.png","sheet_x":58,"sheet_y":16,"short_name":"coffee","short_names":["coffee"],"text":null,"texts":null,"category":"Food & Drink","subcategory":"drink","sort_order":822,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SHAMROCK","unified":"2618-FE0F","non_qualified":"2618","docomo":null,"au":null,"softbank":null,"google":null,"image":"2618-fe0f.png","sheet_x":58,"sheet_y":17,"short_name":"shamrock","short_names":["shamrock"],"text":null,"texts":null,"category":"Animals & Nature","subcategory":"plant-other","sort_order":704,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WHITE UP POINTING INDEX","unified":"261D-FE0F","non_qualified":"261D","docomo":null,"au":"E4F6","softbank":"E00F","google":"FEB98","image":"261d-fe0f.png","sheet_x":58,"sheet_y":18,"short_name":"point_up","short_names":["point_up"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-single-finger","sort_order":194,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"261D-1F3FB","non_qualified":null,"image":"261d-1f3fb.png","sheet_x":58,"sheet_y":19,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"261D-1F3FC","non_qualified":null,"image":"261d-1f3fc.png","sheet_x":58,"sheet_y":20,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"261D-1F3FD","non_qualified":null,"image":"261d-1f3fd.png","sheet_x":58,"sheet_y":21,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"261D-1F3FE","non_qualified":null,"image":"261d-1f3fe.png","sheet_x":58,"sheet_y":22,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"261D-1F3FF","non_qualified":null,"image":"261d-1f3ff.png","sheet_x":58,"sheet_y":23,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"SKULL AND CROSSBONES","unified":"2620-FE0F","non_qualified":"2620","docomo":null,"au":null,"softbank":null,"google":null,"image":"2620-fe0f.png","sheet_x":58,"sheet_y":24,"short_name":"skull_and_crossbones","short_names":["skull_and_crossbones"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-negative","sort_order":109,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RADIOACTIVE","unified":"2622-FE0F","non_qualified":"2622","docomo":null,"au":null,"softbank":null,"google":null,"image":"2622-fe0f.png","sheet_x":58,"sheet_y":25,"short_name":"radioactive_sign","short_names":["radioactive_sign"],"text":null,"texts":null,"category":"Symbols","subcategory":"warning","sort_order":1436,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BIOHAZARD","unified":"2623-FE0F","non_qualified":"2623","docomo":null,"au":null,"softbank":null,"google":null,"image":"2623-fe0f.png","sheet_x":58,"sheet_y":26,"short_name":"biohazard_sign","short_names":["biohazard_sign"],"text":null,"texts":null,"category":"Symbols","subcategory":"warning","sort_order":1437,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ORTHODOX CROSS","unified":"2626-FE0F","non_qualified":"2626","docomo":null,"au":null,"softbank":null,"google":null,"image":"2626-fe0f.png","sheet_x":58,"sheet_y":27,"short_name":"orthodox_cross","short_names":["orthodox_cross"],"text":null,"texts":null,"category":"Symbols","subcategory":"religion","sort_order":1466,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"STAR AND CRESCENT","unified":"262A-FE0F","non_qualified":"262A","docomo":null,"au":null,"softbank":null,"google":null,"image":"262a-fe0f.png","sheet_x":58,"sheet_y":28,"short_name":"star_and_crescent","short_names":["star_and_crescent"],"text":null,"texts":null,"category":"Symbols","subcategory":"religion","sort_order":1467,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PEACE SYMBOL","unified":"262E-FE0F","non_qualified":"262E","docomo":null,"au":null,"softbank":null,"google":null,"image":"262e-fe0f.png","sheet_x":58,"sheet_y":29,"short_name":"peace_symbol","short_names":["peace_symbol"],"text":null,"texts":null,"category":"Symbols","subcategory":"religion","sort_order":1468,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"YIN YANG","unified":"262F-FE0F","non_qualified":"262F","docomo":null,"au":null,"softbank":null,"google":null,"image":"262f-fe0f.png","sheet_x":58,"sheet_y":30,"short_name":"yin_yang","short_names":["yin_yang"],"text":null,"texts":null,"category":"Symbols","subcategory":"religion","sort_order":1464,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WHEEL OF DHARMA","unified":"2638-FE0F","non_qualified":"2638","docomo":null,"au":null,"softbank":null,"google":null,"image":"2638-fe0f.png","sheet_x":58,"sheet_y":31,"short_name":"wheel_of_dharma","short_names":["wheel_of_dharma"],"text":null,"texts":null,"category":"Symbols","subcategory":"religion","sort_order":1463,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FROWNING FACE","unified":"2639-FE0F","non_qualified":"2639","docomo":null,"au":null,"softbank":null,"google":null,"image":"2639-fe0f.png","sheet_x":58,"sheet_y":32,"short_name":"white_frowning_face","short_names":["white_frowning_face"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-concerned","sort_order":80,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WHITE SMILING FACE","unified":"263A-FE0F","non_qualified":"263A","docomo":"E6F0","au":"E4FB","softbank":"E414","google":"FE336","image":"263a-fe0f.png","sheet_x":58,"sheet_y":33,"short_name":"relaxed","short_names":["relaxed"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"face-affection","sort_order":20,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FEMALE SIGN","unified":"2640-FE0F","non_qualified":"2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"2640-fe0f.png","sheet_x":58,"sheet_y":34,"short_name":"female_sign","short_names":["female_sign"],"text":null,"texts":null,"category":"Symbols","subcategory":"gender","sort_order":1510,"added_in":"4.0","has_img_apple":false,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MALE SIGN","unified":"2642-FE0F","non_qualified":"2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"2642-fe0f.png","sheet_x":58,"sheet_y":35,"short_name":"male_sign","short_names":["male_sign"],"text":null,"texts":null,"category":"Symbols","subcategory":"gender","sort_order":1511,"added_in":"4.0","has_img_apple":false,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ARIES","unified":"2648","non_qualified":null,"docomo":"E646","au":"E48F","softbank":"E23F","google":"FE02B","image":"2648.png","sheet_x":58,"sheet_y":36,"short_name":"aries","short_names":["aries"],"text":null,"texts":null,"category":"Symbols","subcategory":"zodiac","sort_order":1472,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TAURUS","unified":"2649","non_qualified":null,"docomo":"E647","au":"E490","softbank":"E240","google":"FE02C","image":"2649.png","sheet_x":58,"sheet_y":37,"short_name":"taurus","short_names":["taurus"],"text":null,"texts":null,"category":"Symbols","subcategory":"zodiac","sort_order":1473,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GEMINI","unified":"264A","non_qualified":null,"docomo":"E648","au":"E491","softbank":"E241","google":"FE02D","image":"264a.png","sheet_x":58,"sheet_y":38,"short_name":"gemini","short_names":["gemini"],"text":null,"texts":null,"category":"Symbols","subcategory":"zodiac","sort_order":1474,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CANCER","unified":"264B","non_qualified":null,"docomo":"E649","au":"E492","softbank":"E242","google":"FE02E","image":"264b.png","sheet_x":58,"sheet_y":39,"short_name":"cancer","short_names":["cancer"],"text":null,"texts":null,"category":"Symbols","subcategory":"zodiac","sort_order":1475,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LEO","unified":"264C","non_qualified":null,"docomo":"E64A","au":"E493","softbank":"E243","google":"FE02F","image":"264c.png","sheet_x":58,"sheet_y":40,"short_name":"leo","short_names":["leo"],"text":null,"texts":null,"category":"Symbols","subcategory":"zodiac","sort_order":1476,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"VIRGO","unified":"264D","non_qualified":null,"docomo":"E64B","au":"E494","softbank":"E244","google":"FE030","image":"264d.png","sheet_x":58,"sheet_y":41,"short_name":"virgo","short_names":["virgo"],"text":null,"texts":null,"category":"Symbols","subcategory":"zodiac","sort_order":1477,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LIBRA","unified":"264E","non_qualified":null,"docomo":"E64C","au":"E495","softbank":"E245","google":"FE031","image":"264e.png","sheet_x":58,"sheet_y":42,"short_name":"libra","short_names":["libra"],"text":null,"texts":null,"category":"Symbols","subcategory":"zodiac","sort_order":1478,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SCORPIUS","unified":"264F","non_qualified":null,"docomo":"E64D","au":"E496","softbank":"E246","google":"FE032","image":"264f.png","sheet_x":58,"sheet_y":43,"short_name":"scorpius","short_names":["scorpius"],"text":null,"texts":null,"category":"Symbols","subcategory":"zodiac","sort_order":1479,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SAGITTARIUS","unified":"2650","non_qualified":null,"docomo":"E64E","au":"E497","softbank":"E247","google":"FE033","image":"2650.png","sheet_x":58,"sheet_y":44,"short_name":"sagittarius","short_names":["sagittarius"],"text":null,"texts":null,"category":"Symbols","subcategory":"zodiac","sort_order":1480,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CAPRICORN","unified":"2651","non_qualified":null,"docomo":"E64F","au":"E498","softbank":"E248","google":"FE034","image":"2651.png","sheet_x":58,"sheet_y":45,"short_name":"capricorn","short_names":["capricorn"],"text":null,"texts":null,"category":"Symbols","subcategory":"zodiac","sort_order":1481,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"AQUARIUS","unified":"2652","non_qualified":null,"docomo":"E650","au":"E499","softbank":"E249","google":"FE035","image":"2652.png","sheet_x":58,"sheet_y":46,"short_name":"aquarius","short_names":["aquarius"],"text":null,"texts":null,"category":"Symbols","subcategory":"zodiac","sort_order":1482,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PISCES","unified":"2653","non_qualified":null,"docomo":"E651","au":"E49A","softbank":"E24A","google":"FE036","image":"2653.png","sheet_x":58,"sheet_y":47,"short_name":"pisces","short_names":["pisces"],"text":null,"texts":null,"category":"Symbols","subcategory":"zodiac","sort_order":1483,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CHESS PAWN","unified":"265F-FE0F","non_qualified":"265F","docomo":null,"au":null,"softbank":null,"google":null,"image":"265f-fe0f.png","sheet_x":58,"sheet_y":48,"short_name":"chess_pawn","short_names":["chess_pawn"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1139,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK SPADE SUIT","unified":"2660-FE0F","non_qualified":"2660","docomo":"E68E","au":"E5A1","softbank":"E20E","google":"FEB1B","image":"2660-fe0f.png","sheet_x":58,"sheet_y":49,"short_name":"spades","short_names":["spades"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1135,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK CLUB SUIT","unified":"2663-FE0F","non_qualified":"2663","docomo":"E690","au":"E5A3","softbank":"E20F","google":"FEB1D","image":"2663-fe0f.png","sheet_x":58,"sheet_y":50,"short_name":"clubs","short_names":["clubs"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1138,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK HEART SUIT","unified":"2665-FE0F","non_qualified":"2665","docomo":"E68D","au":"EAA5","softbank":"E20C","google":"FEB1A","image":"2665-fe0f.png","sheet_x":58,"sheet_y":51,"short_name":"hearts","short_names":["hearts"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1136,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK DIAMOND SUIT","unified":"2666-FE0F","non_qualified":"2666","docomo":"E68F","au":"E5A2","softbank":"E20D","google":"FEB1C","image":"2666-fe0f.png","sheet_x":58,"sheet_y":52,"short_name":"diamonds","short_names":["diamonds"],"text":null,"texts":null,"category":"Activities","subcategory":"game","sort_order":1137,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HOT SPRINGS","unified":"2668-FE0F","non_qualified":"2668","docomo":"E6F7","au":"E4BC","softbank":"E123","google":"FE7FA","image":"2668-fe0f.png","sheet_x":58,"sheet_y":53,"short_name":"hotsprings","short_names":["hotsprings"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-other","sort_order":906,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK UNIVERSAL RECYCLING SYMBOL","unified":"267B-FE0F","non_qualified":"267B","docomo":"E735","au":"EB79","softbank":null,"google":"FEB2C","image":"267b-fe0f.png","sheet_x":58,"sheet_y":54,"short_name":"recycle","short_names":["recycle"],"text":null,"texts":null,"category":"Symbols","subcategory":"other-symbol","sort_order":1529,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"INFINITY","unified":"267E-FE0F","non_qualified":"267E","docomo":null,"au":null,"softbank":null,"google":null,"image":"267e-fe0f.png","sheet_x":58,"sheet_y":55,"short_name":"infinity","short_names":["infinity"],"text":null,"texts":null,"category":"Symbols","subcategory":"math","sort_order":1518,"added_in":"11.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WHEELCHAIR SYMBOL","unified":"267F","non_qualified":null,"docomo":"E69B","au":"E47F","softbank":"E20A","google":"FEB20","image":"267f.png","sheet_x":58,"sheet_y":56,"short_name":"wheelchair","short_names":["wheelchair"],"text":null,"texts":null,"category":"Symbols","subcategory":"transport-sign","sort_order":1415,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HAMMER AND PICK","unified":"2692-FE0F","non_qualified":"2692","docomo":null,"au":null,"softbank":null,"google":null,"image":"2692-fe0f.png","sheet_x":58,"sheet_y":57,"short_name":"hammer_and_pick","short_names":["hammer_and_pick"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1341,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ANCHOR","unified":"2693","non_qualified":null,"docomo":"E661","au":"E4A9","softbank":null,"google":"FE4C1","image":"2693.png","sheet_x":58,"sheet_y":58,"short_name":"anchor","short_names":["anchor"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-water","sort_order":963,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CROSSED SWORDS","unified":"2694-FE0F","non_qualified":"2694","docomo":null,"au":null,"softbank":null,"google":null,"image":"2694-fe0f.png","sheet_x":58,"sheet_y":59,"short_name":"crossed_swords","short_names":["crossed_swords"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1344,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MEDICAL SYMBOL","unified":"2695-FE0F","non_qualified":"2695","docomo":null,"au":null,"softbank":null,"google":null,"image":"2695-fe0f.png","sheet_x":58,"sheet_y":60,"short_name":"medical_symbol","short_names":["medical_symbol","staff_of_aesculapius"],"text":null,"texts":null,"category":"Symbols","subcategory":"other-symbol","sort_order":1528,"added_in":"4.0","has_img_apple":false,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BALANCE SCALE","unified":"2696-FE0F","non_qualified":"2696","docomo":null,"au":null,"softbank":null,"google":null,"image":"2696-fe0f.png","sheet_x":58,"sheet_y":61,"short_name":"scales","short_names":["scales"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1355,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ALEMBIC","unified":"2697-FE0F","non_qualified":"2697","docomo":null,"au":null,"softbank":null,"google":null,"image":"2697-fe0f.png","sheet_x":59,"sheet_y":0,"short_name":"alembic","short_names":["alembic"],"text":null,"texts":null,"category":"Objects","subcategory":"science","sort_order":1364,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"GEAR","unified":"2699-FE0F","non_qualified":"2699","docomo":null,"au":null,"softbank":null,"google":null,"image":"2699-fe0f.png","sheet_x":59,"sheet_y":1,"short_name":"gear","short_names":["gear"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1353,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ATOM SYMBOL","unified":"269B-FE0F","non_qualified":"269B","docomo":null,"au":null,"softbank":null,"google":null,"image":"269b-fe0f.png","sheet_x":59,"sheet_y":2,"short_name":"atom_symbol","short_names":["atom_symbol"],"text":null,"texts":null,"category":"Symbols","subcategory":"religion","sort_order":1460,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FLEUR-DE-LIS","unified":"269C-FE0F","non_qualified":"269C","docomo":null,"au":null,"softbank":null,"google":null,"image":"269c-fe0f.png","sheet_x":59,"sheet_y":3,"short_name":"fleur_de_lis","short_names":["fleur_de_lis"],"text":null,"texts":null,"category":"Symbols","subcategory":"other-symbol","sort_order":1530,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WARNING SIGN","unified":"26A0-FE0F","non_qualified":"26A0","docomo":"E737","au":"E481","softbank":"E252","google":"FEB23","image":"26a0-fe0f.png","sheet_x":59,"sheet_y":4,"short_name":"warning","short_names":["warning"],"text":null,"texts":null,"category":"Symbols","subcategory":"warning","sort_order":1425,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HIGH VOLTAGE SIGN","unified":"26A1","non_qualified":null,"docomo":"E642","au":"E487","softbank":"E13D","google":"FE004","image":"26a1.png","sheet_x":59,"sheet_y":5,"short_name":"zap","short_names":["zap"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1057,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"TRANSGENDER SYMBOL","unified":"26A7-FE0F","non_qualified":"26A7","docomo":null,"au":null,"softbank":null,"google":null,"image":"26a7-fe0f.png","sheet_x":59,"sheet_y":6,"short_name":"transgender_symbol","short_names":["transgender_symbol"],"text":null,"texts":null,"category":"Symbols","subcategory":"gender","sort_order":1512,"added_in":"13.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MEDIUM WHITE CIRCLE","unified":"26AA","non_qualified":null,"docomo":"E69C","au":"E53A","softbank":null,"google":"FEB65","image":"26aa.png","sheet_x":59,"sheet_y":7,"short_name":"white_circle","short_names":["white_circle"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1609,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MEDIUM BLACK CIRCLE","unified":"26AB","non_qualified":null,"docomo":"E69C","au":"E53B","softbank":null,"google":"FEB66","image":"26ab.png","sheet_x":59,"sheet_y":8,"short_name":"black_circle","short_names":["black_circle"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1608,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"COFFIN","unified":"26B0-FE0F","non_qualified":"26B0","docomo":null,"au":null,"softbank":null,"google":null,"image":"26b0-fe0f.png","sheet_x":59,"sheet_y":9,"short_name":"coffin","short_names":["coffin"],"text":null,"texts":null,"category":"Objects","subcategory":"other-object","sort_order":1404,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FUNERAL URN","unified":"26B1-FE0F","non_qualified":"26B1","docomo":null,"au":null,"softbank":null,"google":null,"image":"26b1-fe0f.png","sheet_x":59,"sheet_y":10,"short_name":"funeral_urn","short_names":["funeral_urn"],"text":null,"texts":null,"category":"Objects","subcategory":"other-object","sort_order":1406,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SOCCER BALL","unified":"26BD","non_qualified":null,"docomo":"E656","au":"E4B6","softbank":"E018","google":"FE7D4","image":"26bd.png","sheet_x":59,"sheet_y":11,"short_name":"soccer","short_names":["soccer"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1092,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BASEBALL","unified":"26BE","non_qualified":null,"docomo":"E653","au":"E4BA","softbank":"E016","google":"FE7D1","image":"26be.png","sheet_x":59,"sheet_y":12,"short_name":"baseball","short_names":["baseball"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1093,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SNOWMAN WITHOUT SNOW","unified":"26C4","non_qualified":null,"docomo":"E641","au":"E485","softbank":"E048","google":"FE003","image":"26c4.png","sheet_x":59,"sheet_y":13,"short_name":"snowman_without_snow","short_names":["snowman_without_snow"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1060,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SUN BEHIND CLOUD","unified":"26C5","non_qualified":null,"docomo":"E63E-E63F","au":"E48E","softbank":null,"google":"FE00F","image":"26c5.png","sheet_x":59,"sheet_y":14,"short_name":"partly_sunny","short_names":["partly_sunny"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1040,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CLOUD WITH LIGHTNING AND RAIN","unified":"26C8-FE0F","non_qualified":"26C8","docomo":null,"au":null,"softbank":null,"google":null,"image":"26c8-fe0f.png","sheet_x":59,"sheet_y":15,"short_name":"thunder_cloud_and_rain","short_names":["thunder_cloud_and_rain"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1041,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"OPHIUCHUS","unified":"26CE","non_qualified":null,"docomo":null,"au":"E49B","softbank":"E24B","google":"FE037","image":"26ce.png","sheet_x":59,"sheet_y":16,"short_name":"ophiuchus","short_names":["ophiuchus"],"text":null,"texts":null,"category":"Symbols","subcategory":"zodiac","sort_order":1484,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PICK","unified":"26CF-FE0F","non_qualified":"26CF","docomo":null,"au":null,"softbank":null,"google":null,"image":"26cf-fe0f.png","sheet_x":59,"sheet_y":17,"short_name":"pick","short_names":["pick"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1340,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RESCUE WORKER\u2019S HELMET","unified":"26D1-FE0F","non_qualified":"26D1","docomo":null,"au":null,"softbank":null,"google":null,"image":"26d1-fe0f.png","sheet_x":59,"sheet_y":18,"short_name":"helmet_with_white_cross","short_names":["helmet_with_white_cross"],"text":null,"texts":null,"category":"Objects","subcategory":"clothing","sort_order":1192,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BROKEN CHAIN","unified":"26D3-FE0F-200D-1F4A5","non_qualified":"26D3-200D-1F4A5","docomo":null,"au":null,"softbank":null,"google":null,"image":"26d3-fe0f-200d-1f4a5.png","sheet_x":59,"sheet_y":19,"short_name":"broken_chain","short_names":["broken_chain"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1358,"added_in":"15.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":false,"has_img_facebook":false},{"name":"CHAINS","unified":"26D3-FE0F","non_qualified":"26D3","docomo":null,"au":null,"softbank":null,"google":null,"image":"26d3-fe0f.png","sheet_x":59,"sheet_y":20,"short_name":"chains","short_names":["chains"],"text":null,"texts":null,"category":"Objects","subcategory":"tool","sort_order":1359,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NO ENTRY","unified":"26D4","non_qualified":null,"docomo":"E72F","au":"E484","softbank":null,"google":"FEB26","image":"26d4.png","sheet_x":59,"sheet_y":21,"short_name":"no_entry","short_names":["no_entry"],"text":null,"texts":null,"category":"Symbols","subcategory":"warning","sort_order":1427,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SHINTO SHRINE","unified":"26E9-FE0F","non_qualified":"26E9","docomo":null,"au":null,"softbank":null,"google":null,"image":"26e9-fe0f.png","sheet_x":59,"sheet_y":22,"short_name":"shinto_shrine","short_names":["shinto_shrine"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-religious","sort_order":894,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CHURCH","unified":"26EA","non_qualified":null,"docomo":null,"au":"E5BB","softbank":"E037","google":"FE4BB","image":"26ea.png","sheet_x":59,"sheet_y":23,"short_name":"church","short_names":["church"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-religious","sort_order":890,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MOUNTAIN","unified":"26F0-FE0F","non_qualified":"26F0","docomo":null,"au":null,"softbank":null,"google":null,"image":"26f0-fe0f.png","sheet_x":59,"sheet_y":24,"short_name":"mountain","short_names":["mountain"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-geographic","sort_order":855,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"UMBRELLA ON GROUND","unified":"26F1-FE0F","non_qualified":"26F1","docomo":null,"au":null,"softbank":null,"google":null,"image":"26f1-fe0f.png","sheet_x":59,"sheet_y":25,"short_name":"umbrella_on_ground","short_names":["umbrella_on_ground"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1056,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FOUNTAIN","unified":"26F2","non_qualified":null,"docomo":null,"au":"E5CF","softbank":"E121","google":"FE4BC","image":"26f2.png","sheet_x":59,"sheet_y":26,"short_name":"fountain","short_names":["fountain"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-other","sort_order":896,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FLAG IN HOLE","unified":"26F3","non_qualified":null,"docomo":"E654","au":"E599","softbank":"E014","google":"FE7D2","image":"26f3.png","sheet_x":59,"sheet_y":27,"short_name":"golf","short_names":["golf"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1111,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FERRY","unified":"26F4-FE0F","non_qualified":"26F4","docomo":null,"au":null,"softbank":null,"google":null,"image":"26f4-fe0f.png","sheet_x":59,"sheet_y":28,"short_name":"ferry","short_names":["ferry"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-water","sort_order":969,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SAILBOAT","unified":"26F5","non_qualified":null,"docomo":"E6A3","au":"E4B4","softbank":"E01C","google":"FE7EA","image":"26f5.png","sheet_x":59,"sheet_y":29,"short_name":"boat","short_names":["boat","sailboat"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-water","sort_order":965,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SKIER","unified":"26F7-FE0F","non_qualified":"26F7","docomo":null,"au":null,"softbank":null,"google":null,"image":"26f7-fe0f.png","sheet_x":59,"sheet_y":30,"short_name":"skier","short_names":["skier"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":461,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ICE SKATE","unified":"26F8-FE0F","non_qualified":"26F8","docomo":null,"au":null,"softbank":null,"google":null,"image":"26f8-fe0f.png","sheet_x":59,"sheet_y":31,"short_name":"ice_skate","short_names":["ice_skate"],"text":null,"texts":null,"category":"Activities","subcategory":"sport","sort_order":1112,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WOMAN BOUNCING BALL","unified":"26F9-FE0F-200D-2640-FE0F","non_qualified":"26F9-200D-2640","docomo":null,"au":null,"softbank":null,"google":null,"image":"26f9-fe0f-200d-2640-fe0f.png","sheet_x":59,"sheet_y":32,"short_name":"woman-bouncing-ball","short_names":["woman-bouncing-ball"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":477,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"26F9-1F3FB-200D-2640-FE0F","non_qualified":"26F9-1F3FB-200D-2640","image":"26f9-1f3fb-200d-2640-fe0f.png","sheet_x":59,"sheet_y":33,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"26F9-1F3FC-200D-2640-FE0F","non_qualified":"26F9-1F3FC-200D-2640","image":"26f9-1f3fc-200d-2640-fe0f.png","sheet_x":59,"sheet_y":34,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"26F9-1F3FD-200D-2640-FE0F","non_qualified":"26F9-1F3FD-200D-2640","image":"26f9-1f3fd-200d-2640-fe0f.png","sheet_x":59,"sheet_y":35,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"26F9-1F3FE-200D-2640-FE0F","non_qualified":"26F9-1F3FE-200D-2640","image":"26f9-1f3fe-200d-2640-fe0f.png","sheet_x":59,"sheet_y":36,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"26F9-1F3FF-200D-2640-FE0F","non_qualified":"26F9-1F3FF-200D-2640","image":"26f9-1f3ff-200d-2640-fe0f.png","sheet_x":59,"sheet_y":37,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"MAN BOUNCING BALL","unified":"26F9-FE0F-200D-2642-FE0F","non_qualified":"26F9-200D-2642","docomo":null,"au":null,"softbank":null,"google":null,"image":"26f9-fe0f-200d-2642-fe0f.png","sheet_x":59,"sheet_y":38,"short_name":"man-bouncing-ball","short_names":["man-bouncing-ball"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":476,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":false,"skin_variations":{"1F3FB":{"unified":"26F9-1F3FB-200D-2642-FE0F","non_qualified":"26F9-1F3FB-200D-2642","image":"26f9-1f3fb-200d-2642-fe0f.png","sheet_x":59,"sheet_y":39,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"26F9-1F3FC-200D-2642-FE0F","non_qualified":"26F9-1F3FC-200D-2642","image":"26f9-1f3fc-200d-2642-fe0f.png","sheet_x":59,"sheet_y":40,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"26F9-1F3FD-200D-2642-FE0F","non_qualified":"26F9-1F3FD-200D-2642","image":"26f9-1f3fd-200d-2642-fe0f.png","sheet_x":59,"sheet_y":41,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"26F9-1F3FE-200D-2642-FE0F","non_qualified":"26F9-1F3FE-200D-2642","image":"26f9-1f3fe-200d-2642-fe0f.png","sheet_x":59,"sheet_y":42,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"26F9-1F3FF-200D-2642-FE0F","non_qualified":"26F9-1F3FF-200D-2642","image":"26f9-1f3ff-200d-2642-fe0f.png","sheet_x":59,"sheet_y":43,"added_in":"4.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoletes":"26F9-FE0F"},{"name":"PERSON BOUNCING BALL","unified":"26F9-FE0F","non_qualified":"26F9","docomo":null,"au":null,"softbank":null,"google":null,"image":"26f9-fe0f.png","sheet_x":59,"sheet_y":44,"short_name":"person_with_ball","short_names":["person_with_ball"],"text":null,"texts":null,"category":"People & Body","subcategory":"person-sport","sort_order":475,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"26F9-1F3FB","non_qualified":null,"image":"26f9-1f3fb.png","sheet_x":59,"sheet_y":45,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"26F9-1F3FC","non_qualified":null,"image":"26f9-1f3fc.png","sheet_x":59,"sheet_y":46,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"26F9-1F3FD","non_qualified":null,"image":"26f9-1f3fd.png","sheet_x":59,"sheet_y":47,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"26F9-1F3FE","non_qualified":null,"image":"26f9-1f3fe.png","sheet_x":59,"sheet_y":48,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"26F9-1F3FF","non_qualified":null,"image":"26f9-1f3ff.png","sheet_x":59,"sheet_y":49,"added_in":"2.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}},"obsoleted_by":"26F9-FE0F-200D-2642-FE0F"},{"name":"TENT","unified":"26FA","non_qualified":null,"docomo":null,"au":"E5D0","softbank":"E122","google":"FE7FB","image":"26fa.png","sheet_x":59,"sheet_y":50,"short_name":"tent","short_names":["tent"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"place-other","sort_order":897,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"FUEL PUMP","unified":"26FD","non_qualified":null,"docomo":"E66B","au":"E571","softbank":"E03A","google":"FE7F5","image":"26fd.png","sheet_x":59,"sheet_y":51,"short_name":"fuelpump","short_names":["fuelpump"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-ground","sort_order":956,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK SCISSORS","unified":"2702-FE0F","non_qualified":"2702","docomo":"E675","au":"E516","softbank":"E313","google":"FE53E","image":"2702-fe0f.png","sheet_x":59,"sheet_y":52,"short_name":"scissors","short_names":["scissors"],"text":null,"texts":null,"category":"Objects","subcategory":"office","sort_order":1328,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WHITE HEAVY CHECK MARK","unified":"2705","non_qualified":null,"docomo":null,"au":"E55E","softbank":null,"google":"FEB4A","image":"2705.png","sheet_x":59,"sheet_y":53,"short_name":"white_check_mark","short_names":["white_check_mark"],"text":null,"texts":null,"category":"Symbols","subcategory":"other-symbol","sort_order":1535,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"AIRPLANE","unified":"2708-FE0F","non_qualified":"2708","docomo":"E662","au":"E4B3","softbank":"E01D","google":"FE7E9","image":"2708-fe0f.png","sheet_x":59,"sheet_y":54,"short_name":"airplane","short_names":["airplane"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"transport-air","sort_order":972,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ENVELOPE","unified":"2709-FE0F","non_qualified":"2709","docomo":"E6D3","au":"E521","softbank":null,"google":"FE529","image":"2709-fe0f.png","sheet_x":59,"sheet_y":55,"short_name":"email","short_names":["email","envelope"],"text":null,"texts":null,"category":"Objects","subcategory":"mail","sort_order":1289,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"RAISED FIST","unified":"270A","non_qualified":null,"docomo":"E693","au":"EB83","softbank":"E010","google":"FEB93","image":"270a.png","sheet_x":59,"sheet_y":56,"short_name":"fist","short_names":["fist"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-closed","sort_order":198,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"270A-1F3FB","non_qualified":null,"image":"270a-1f3fb.png","sheet_x":59,"sheet_y":57,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"270A-1F3FC","non_qualified":null,"image":"270a-1f3fc.png","sheet_x":59,"sheet_y":58,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"270A-1F3FD","non_qualified":null,"image":"270a-1f3fd.png","sheet_x":59,"sheet_y":59,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"270A-1F3FE","non_qualified":null,"image":"270a-1f3fe.png","sheet_x":59,"sheet_y":60,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"270A-1F3FF","non_qualified":null,"image":"270a-1f3ff.png","sheet_x":59,"sheet_y":61,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"RAISED HAND","unified":"270B","non_qualified":null,"docomo":"E695","au":"E5A7","softbank":"E012","google":"FEB95","image":"270b.png","sheet_x":60,"sheet_y":0,"short_name":"hand","short_names":["hand","raised_hand"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-open","sort_order":172,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"270B-1F3FB","non_qualified":null,"image":"270b-1f3fb.png","sheet_x":60,"sheet_y":1,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"270B-1F3FC","non_qualified":null,"image":"270b-1f3fc.png","sheet_x":60,"sheet_y":2,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"270B-1F3FD","non_qualified":null,"image":"270b-1f3fd.png","sheet_x":60,"sheet_y":3,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"270B-1F3FE","non_qualified":null,"image":"270b-1f3fe.png","sheet_x":60,"sheet_y":4,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"270B-1F3FF","non_qualified":null,"image":"270b-1f3ff.png","sheet_x":60,"sheet_y":5,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"VICTORY HAND","unified":"270C-FE0F","non_qualified":"270C","docomo":"E694","au":"E5A6","softbank":"E011","google":"FEB94","image":"270c-fe0f.png","sheet_x":60,"sheet_y":6,"short_name":"v","short_names":["v"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-fingers-partial","sort_order":183,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"270C-1F3FB","non_qualified":null,"image":"270c-1f3fb.png","sheet_x":60,"sheet_y":7,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"270C-1F3FC","non_qualified":null,"image":"270c-1f3fc.png","sheet_x":60,"sheet_y":8,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"270C-1F3FD","non_qualified":null,"image":"270c-1f3fd.png","sheet_x":60,"sheet_y":9,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"270C-1F3FE","non_qualified":null,"image":"270c-1f3fe.png","sheet_x":60,"sheet_y":10,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"270C-1F3FF","non_qualified":null,"image":"270c-1f3ff.png","sheet_x":60,"sheet_y":11,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"WRITING HAND","unified":"270D-FE0F","non_qualified":"270D","docomo":null,"au":null,"softbank":null,"google":null,"image":"270d-fe0f.png","sheet_x":60,"sheet_y":12,"short_name":"writing_hand","short_names":["writing_hand"],"text":null,"texts":null,"category":"People & Body","subcategory":"hand-prop","sort_order":209,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true,"skin_variations":{"1F3FB":{"unified":"270D-1F3FB","non_qualified":null,"image":"270d-1f3fb.png","sheet_x":60,"sheet_y":13,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FC":{"unified":"270D-1F3FC","non_qualified":null,"image":"270d-1f3fc.png","sheet_x":60,"sheet_y":14,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FD":{"unified":"270D-1F3FD","non_qualified":null,"image":"270d-1f3fd.png","sheet_x":60,"sheet_y":15,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FE":{"unified":"270D-1F3FE","non_qualified":null,"image":"270d-1f3fe.png","sheet_x":60,"sheet_y":16,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},"1F3FF":{"unified":"270D-1F3FF","non_qualified":null,"image":"270d-1f3ff.png","sheet_x":60,"sheet_y":17,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}}},{"name":"PENCIL","unified":"270F-FE0F","non_qualified":"270F","docomo":"E719","au":"E4A1","softbank":null,"google":"FE539","image":"270f-fe0f.png","sheet_x":60,"sheet_y":18,"short_name":"pencil2","short_names":["pencil2"],"text":null,"texts":null,"category":"Objects","subcategory":"writing","sort_order":1302,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK NIB","unified":"2712-FE0F","non_qualified":"2712","docomo":"E6AE","au":"EB03","softbank":null,"google":"FE536","image":"2712-fe0f.png","sheet_x":60,"sheet_y":19,"short_name":"black_nib","short_names":["black_nib"],"text":null,"texts":null,"category":"Objects","subcategory":"writing","sort_order":1303,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HEAVY CHECK MARK","unified":"2714-FE0F","non_qualified":"2714","docomo":null,"au":"E557","softbank":null,"google":"FEB49","image":"2714-fe0f.png","sheet_x":60,"sheet_y":20,"short_name":"heavy_check_mark","short_names":["heavy_check_mark"],"text":null,"texts":null,"category":"Symbols","subcategory":"other-symbol","sort_order":1537,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HEAVY MULTIPLICATION X","unified":"2716-FE0F","non_qualified":"2716","docomo":null,"au":"E54F","softbank":null,"google":"FEB53","image":"2716-fe0f.png","sheet_x":60,"sheet_y":21,"short_name":"heavy_multiplication_x","short_names":["heavy_multiplication_x"],"text":null,"texts":null,"category":"Symbols","subcategory":"math","sort_order":1513,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LATIN CROSS","unified":"271D-FE0F","non_qualified":"271D","docomo":null,"au":null,"softbank":null,"google":null,"image":"271d-fe0f.png","sheet_x":60,"sheet_y":22,"short_name":"latin_cross","short_names":["latin_cross"],"text":null,"texts":null,"category":"Symbols","subcategory":"religion","sort_order":1465,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"STAR OF DAVID","unified":"2721-FE0F","non_qualified":"2721","docomo":null,"au":null,"softbank":null,"google":null,"image":"2721-fe0f.png","sheet_x":60,"sheet_y":23,"short_name":"star_of_david","short_names":["star_of_david"],"text":null,"texts":null,"category":"Symbols","subcategory":"religion","sort_order":1462,"added_in":"0.7","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SPARKLES","unified":"2728","non_qualified":null,"docomo":"E6FA","au":"EAAB","softbank":"E32E","google":"FEB60","image":"2728.png","sheet_x":60,"sheet_y":24,"short_name":"sparkles","short_names":["sparkles"],"text":null,"texts":null,"category":"Activities","subcategory":"event","sort_order":1070,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EIGHT SPOKED ASTERISK","unified":"2733-FE0F","non_qualified":"2733","docomo":"E6F8","au":"E53E","softbank":"E206","google":"FEB62","image":"2733-fe0f.png","sheet_x":60,"sheet_y":25,"short_name":"eight_spoked_asterisk","short_names":["eight_spoked_asterisk"],"text":null,"texts":null,"category":"Symbols","subcategory":"other-symbol","sort_order":1543,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"EIGHT POINTED BLACK STAR","unified":"2734-FE0F","non_qualified":"2734","docomo":"E6F8","au":"E479","softbank":"E205","google":"FEB61","image":"2734-fe0f.png","sheet_x":60,"sheet_y":26,"short_name":"eight_pointed_black_star","short_names":["eight_pointed_black_star"],"text":null,"texts":null,"category":"Symbols","subcategory":"other-symbol","sort_order":1544,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SNOWFLAKE","unified":"2744-FE0F","non_qualified":"2744","docomo":null,"au":"E48A","softbank":null,"google":"FE00E","image":"2744-fe0f.png","sheet_x":60,"sheet_y":27,"short_name":"snowflake","short_names":["snowflake"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1058,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"SPARKLE","unified":"2747-FE0F","non_qualified":"2747","docomo":"E6FA","au":"E46C","softbank":null,"google":"FEB77","image":"2747-fe0f.png","sheet_x":60,"sheet_y":28,"short_name":"sparkle","short_names":["sparkle"],"text":null,"texts":null,"category":"Symbols","subcategory":"other-symbol","sort_order":1545,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CROSS MARK","unified":"274C","non_qualified":null,"docomo":null,"au":"E550","softbank":"E333","google":"FEB45","image":"274c.png","sheet_x":60,"sheet_y":29,"short_name":"x","short_names":["x"],"text":null,"texts":null,"category":"Symbols","subcategory":"other-symbol","sort_order":1538,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"NEGATIVE SQUARED CROSS MARK","unified":"274E","non_qualified":null,"docomo":null,"au":"E551","softbank":null,"google":"FEB46","image":"274e.png","sheet_x":60,"sheet_y":30,"short_name":"negative_squared_cross_mark","short_names":["negative_squared_cross_mark"],"text":null,"texts":null,"category":"Symbols","subcategory":"other-symbol","sort_order":1539,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK QUESTION MARK ORNAMENT","unified":"2753","non_qualified":null,"docomo":null,"au":"E483","softbank":"E020","google":"FEB09","image":"2753.png","sheet_x":60,"sheet_y":31,"short_name":"question","short_names":["question"],"text":null,"texts":null,"category":"Symbols","subcategory":"punctuation","sort_order":1521,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WHITE QUESTION MARK ORNAMENT","unified":"2754","non_qualified":null,"docomo":null,"au":"E483","softbank":"E336","google":"FEB0A","image":"2754.png","sheet_x":60,"sheet_y":32,"short_name":"grey_question","short_names":["grey_question"],"text":null,"texts":null,"category":"Symbols","subcategory":"punctuation","sort_order":1522,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WHITE EXCLAMATION MARK ORNAMENT","unified":"2755","non_qualified":null,"docomo":"E702","au":"E482","softbank":"E337","google":"FEB0B","image":"2755.png","sheet_x":60,"sheet_y":33,"short_name":"grey_exclamation","short_names":["grey_exclamation"],"text":null,"texts":null,"category":"Symbols","subcategory":"punctuation","sort_order":1523,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HEAVY EXCLAMATION MARK SYMBOL","unified":"2757","non_qualified":null,"docomo":"E702","au":"E482","softbank":"E021","google":"FEB04","image":"2757.png","sheet_x":60,"sheet_y":34,"short_name":"exclamation","short_names":["exclamation","heavy_exclamation_mark"],"text":null,"texts":null,"category":"Symbols","subcategory":"punctuation","sort_order":1524,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HEART EXCLAMATION","unified":"2763-FE0F","non_qualified":"2763","docomo":null,"au":null,"softbank":null,"google":null,"image":"2763-fe0f.png","sheet_x":60,"sheet_y":35,"short_name":"heavy_heart_exclamation_mark_ornament","short_names":["heavy_heart_exclamation_mark_ornament"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"heart","sort_order":139,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HEART ON FIRE","unified":"2764-FE0F-200D-1F525","non_qualified":"2764-200D-1F525","docomo":null,"au":null,"softbank":null,"google":null,"image":"2764-fe0f-200d-1f525.png","sheet_x":60,"sheet_y":36,"short_name":"heart_on_fire","short_names":["heart_on_fire"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"heart","sort_order":141,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"MENDING HEART","unified":"2764-FE0F-200D-1FA79","non_qualified":"2764-200D-1FA79","docomo":null,"au":null,"softbank":null,"google":null,"image":"2764-fe0f-200d-1fa79.png","sheet_x":60,"sheet_y":37,"short_name":"mending_heart","short_names":["mending_heart"],"text":null,"texts":null,"category":"Smileys & Emotion","subcategory":"heart","sort_order":142,"added_in":"13.1","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HEAVY BLACK HEART","unified":"2764-FE0F","non_qualified":"2764","docomo":"E6EC","au":"E595","softbank":"E022","google":"FEB0C","image":"2764-fe0f.png","sheet_x":60,"sheet_y":38,"short_name":"heart","short_names":["heart"],"text":"<3","texts":["<3"],"category":"Smileys & Emotion","subcategory":"heart","sort_order":143,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HEAVY PLUS SIGN","unified":"2795","non_qualified":null,"docomo":null,"au":"E53C","softbank":null,"google":"FEB51","image":"2795.png","sheet_x":60,"sheet_y":39,"short_name":"heavy_plus_sign","short_names":["heavy_plus_sign"],"text":null,"texts":null,"category":"Symbols","subcategory":"math","sort_order":1514,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HEAVY MINUS SIGN","unified":"2796","non_qualified":null,"docomo":null,"au":"E53D","softbank":null,"google":"FEB52","image":"2796.png","sheet_x":60,"sheet_y":40,"short_name":"heavy_minus_sign","short_names":["heavy_minus_sign"],"text":null,"texts":null,"category":"Symbols","subcategory":"math","sort_order":1515,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HEAVY DIVISION SIGN","unified":"2797","non_qualified":null,"docomo":null,"au":"E554","softbank":null,"google":"FEB54","image":"2797.png","sheet_x":60,"sheet_y":41,"short_name":"heavy_division_sign","short_names":["heavy_division_sign"],"text":null,"texts":null,"category":"Symbols","subcategory":"math","sort_order":1516,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK RIGHTWARDS ARROW","unified":"27A1-FE0F","non_qualified":"27A1","docomo":null,"au":"E552","softbank":"E234","google":"FEAFA","image":"27a1-fe0f.png","sheet_x":60,"sheet_y":42,"short_name":"arrow_right","short_names":["arrow_right"],"text":null,"texts":null,"category":"Symbols","subcategory":"arrow","sort_order":1440,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CURLY LOOP","unified":"27B0","non_qualified":null,"docomo":"E70A","au":"EB31","softbank":null,"google":"FEB08","image":"27b0.png","sheet_x":60,"sheet_y":43,"short_name":"curly_loop","short_names":["curly_loop"],"text":null,"texts":null,"category":"Symbols","subcategory":"other-symbol","sort_order":1540,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DOUBLE CURLY LOOP","unified":"27BF","non_qualified":null,"docomo":"E6DF","au":null,"softbank":"E211","google":"FE82B","image":"27bf.png","sheet_x":60,"sheet_y":44,"short_name":"loop","short_names":["loop"],"text":null,"texts":null,"category":"Symbols","subcategory":"other-symbol","sort_order":1541,"added_in":"1.0","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ARROW POINTING RIGHTWARDS THEN CURVING UPWARDS","unified":"2934-FE0F","non_qualified":"2934","docomo":"E6F5","au":"EB2D","softbank":null,"google":"FEAF4","image":"2934-fe0f.png","sheet_x":60,"sheet_y":45,"short_name":"arrow_heading_up","short_names":["arrow_heading_up"],"text":null,"texts":null,"category":"Symbols","subcategory":"arrow","sort_order":1450,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"ARROW POINTING RIGHTWARDS THEN CURVING DOWNWARDS","unified":"2935-FE0F","non_qualified":"2935","docomo":"E700","au":"EB2E","softbank":null,"google":"FEAF5","image":"2935-fe0f.png","sheet_x":60,"sheet_y":46,"short_name":"arrow_heading_down","short_names":["arrow_heading_down"],"text":null,"texts":null,"category":"Symbols","subcategory":"arrow","sort_order":1451,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"LEFTWARDS BLACK ARROW","unified":"2B05-FE0F","non_qualified":"2B05","docomo":null,"au":"E553","softbank":"E235","google":"FEAFB","image":"2b05-fe0f.png","sheet_x":60,"sheet_y":47,"short_name":"arrow_left","short_names":["arrow_left"],"text":null,"texts":null,"category":"Symbols","subcategory":"arrow","sort_order":1444,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"UPWARDS BLACK ARROW","unified":"2B06-FE0F","non_qualified":"2B06","docomo":null,"au":"E53F","softbank":"E232","google":"FEAF8","image":"2b06-fe0f.png","sheet_x":60,"sheet_y":48,"short_name":"arrow_up","short_names":["arrow_up"],"text":null,"texts":null,"category":"Symbols","subcategory":"arrow","sort_order":1438,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"DOWNWARDS BLACK ARROW","unified":"2B07-FE0F","non_qualified":"2B07","docomo":null,"au":"E540","softbank":"E233","google":"FEAF9","image":"2b07-fe0f.png","sheet_x":60,"sheet_y":49,"short_name":"arrow_down","short_names":["arrow_down"],"text":null,"texts":null,"category":"Symbols","subcategory":"arrow","sort_order":1442,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"BLACK LARGE SQUARE","unified":"2B1B","non_qualified":null,"docomo":null,"au":"E549","softbank":null,"google":"FEB6C","image":"2b1b.png","sheet_x":60,"sheet_y":50,"short_name":"black_large_square","short_names":["black_large_square"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1617,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WHITE LARGE SQUARE","unified":"2B1C","non_qualified":null,"docomo":null,"au":"E548","softbank":null,"google":"FEB6B","image":"2b1c.png","sheet_x":60,"sheet_y":51,"short_name":"white_large_square","short_names":["white_large_square"],"text":null,"texts":null,"category":"Symbols","subcategory":"geometric","sort_order":1618,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WHITE MEDIUM STAR","unified":"2B50","non_qualified":null,"docomo":null,"au":"E48B","softbank":"E32F","google":"FEB68","image":"2b50.png","sheet_x":60,"sheet_y":52,"short_name":"star","short_names":["star"],"text":null,"texts":null,"category":"Travel & Places","subcategory":"sky & weather","sort_order":1035,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"HEAVY LARGE CIRCLE","unified":"2B55","non_qualified":null,"docomo":"E6A0","au":"EAAD","softbank":"E332","google":"FEB44","image":"2b55.png","sheet_x":60,"sheet_y":53,"short_name":"o","short_names":["o"],"text":null,"texts":null,"category":"Symbols","subcategory":"other-symbol","sort_order":1534,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"WAVY DASH","unified":"3030-FE0F","non_qualified":"3030","docomo":"E709","au":null,"softbank":null,"google":"FEB07","image":"3030-fe0f.png","sheet_x":60,"sheet_y":54,"short_name":"wavy_dash","short_names":["wavy_dash"],"text":null,"texts":null,"category":"Symbols","subcategory":"punctuation","sort_order":1525,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"PART ALTERNATION MARK","unified":"303D-FE0F","non_qualified":"303D","docomo":null,"au":null,"softbank":"E12C","google":"FE81B","image":"303d-fe0f.png","sheet_x":60,"sheet_y":55,"short_name":"part_alternation_mark","short_names":["part_alternation_mark"],"text":null,"texts":null,"category":"Symbols","subcategory":"other-symbol","sort_order":1542,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CIRCLED IDEOGRAPH CONGRATULATION","unified":"3297-FE0F","non_qualified":"3297","docomo":null,"au":"EA99","softbank":"E30D","google":"FEB43","image":"3297-fe0f.png","sheet_x":60,"sheet_y":56,"short_name":"congratulations","short_names":["congratulations"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1597,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true},{"name":"CIRCLED IDEOGRAPH SECRET","unified":"3299-FE0F","non_qualified":"3299","docomo":"E734","au":"E4F1","softbank":"E315","google":"FEB2B","image":"3299-fe0f.png","sheet_x":60,"sheet_y":57,"short_name":"secret","short_names":["secret"],"text":null,"texts":null,"category":"Symbols","subcategory":"alphanum","sort_order":1598,"added_in":"0.6","has_img_apple":true,"has_img_google":true,"has_img_twitter":true,"has_img_facebook":true}] \ No newline at end of file diff --git a/resources/error.png b/resources/error.png deleted file mode 100644 index 07fba9f7c..000000000 Binary files a/resources/error.png and /dev/null differ diff --git a/resources/generate_resources.py b/resources/generate_resources.py deleted file mode 100755 index 732f7cc0a..000000000 --- a/resources/generate_resources.py +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env python3 -from pathlib import Path - -from _generate_resources import * - -ignored_files = ['qt.conf', 'resources.qrc', 'resources_autogenerated.qrc', 'windows.rc', - 'generate_resources.py', '_generate_resources.py'] - -ignored_names = ['.gitignore', '.DS_Store'] - -# to ignore all files in a/b, add a/b to ignored_directories. -# this will ignore a/b/c/d.txt and a/b/xd.txt -ignored_directories = ['__pycache__', 'linuxinstall'] - -def isNotIgnored(file): - # check if file exists in an ignored direcory - for ignored_directory in ignored_directories: - if file.parent.as_posix().startswith(ignored_directory): - return False - - return file.as_posix() not in ignored_files and file.name not in ignored_names - -all_files = sorted(list(filter(isNotIgnored, \ - filter(Path.is_file, Path('.').glob('**/*'))))) -image_files = sorted(list(filter(isNotIgnored, \ - filter(Path.is_file, Path('.').glob('**/*.png'))))) - -with open('./resources_autogenerated.qrc', 'w') as out: - out.write(resources_header + '\n') - for file in all_files: - out.write(f" {file.as_posix()}\n") - out.write(resources_footer) - -with open('../src/autogenerated/ResourcesAutogen.cpp', 'w') as out: - out.write(source_header) - for file in sorted(image_files): - var_name = file.with_suffix("").as_posix().replace("/",".") - out.write(f' this->{var_name}') - out.write(f' = QPixmap(":/{file.as_posix()}");\n') - out.write(source_footer) - -def writeHeader(out, name, element, indent): - if isinstance(element, dict): - if name != "": - out.write(f"{indent}struct {{\n") - for (key, value) in element.items(): - writeHeader(out, key, value, indent + ' ') - if name != "": - out.write(f"{indent}}} {name};\n"); - else: - out.write(f"{indent}QPixmap {element};\n") - -with open('../src/autogenerated/ResourcesAutogen.hpp', 'w') as out: - out.write(header_header) - - elements = {} - for file in sorted(image_files): - elements_ref = elements - directories = file.as_posix().split('/')[:-1] - filename = file.stem - for directory in directories: - if directory not in elements_ref: - elements_ref[directory] = {} - elements_ref = elements_ref[directory] - elements_ref[filename] = filename - - writeHeader(out, "", elements, '') - - out.write(header_footer) - diff --git a/resources/icon.png b/resources/icon.png index 9a6f5ba19..a86761b64 100644 Binary files a/resources/icon.png and b/resources/icon.png differ diff --git a/resources/licenses/crashpad.txt b/resources/licenses/crashpad.txt new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/resources/licenses/crashpad.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/resources/licenses/expected-lite.txt b/resources/licenses/expected-lite.txt new file mode 100644 index 000000000..36b7cd93c --- /dev/null +++ b/resources/licenses/expected-lite.txt @@ -0,0 +1,23 @@ +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/resources/licenses/fluenticons.txt b/resources/licenses/fluenticons.txt new file mode 100644 index 000000000..bc9c36b28 --- /dev/null +++ b/resources/licenses/fluenticons.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/resources/licenses/lua.txt b/resources/licenses/lua.txt new file mode 100644 index 000000000..b6ed3539e --- /dev/null +++ b/resources/licenses/lua.txt @@ -0,0 +1,7 @@ +Copyright © 1994–2021 Lua.org, PUC-Rio. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/resources/licenses/miniaudio.txt b/resources/licenses/miniaudio.txt new file mode 100644 index 000000000..a203fa843 --- /dev/null +++ b/resources/licenses/miniaudio.txt @@ -0,0 +1,16 @@ +Copyright 2020 David Reid + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/resources/licenses/semver.txt b/resources/licenses/semver.txt new file mode 100644 index 000000000..48c79ba76 --- /dev/null +++ b/resources/licenses/semver.txt @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2018 - 2021 Daniil Goncharov +Copyright (c) 2020 - 2021 Alexander Gorbunov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/resources/pajaDank.png b/resources/pajaDank.png deleted file mode 100644 index c8746932b..000000000 Binary files a/resources/pajaDank.png and /dev/null differ diff --git a/resources/qss/settings.qss b/resources/qss/settings.qss index ba63133ce..93c69b603 100644 --- a/resources/qss/settings.qss +++ b/resources/qss/settings.qss @@ -1,11 +1,11 @@ * { - font-size: px; + font-size: 14px; font-family: "Segoe UI"; } QCheckBox::indicator { - width: px; - height: px; + width: 14px; + height: 14px; } chatterino--ComboBox { @@ -34,6 +34,13 @@ chatterino--SettingsPage #generalSettingsScrollContent { background: #222; } +chatterino--SettingsPage QToolTip { + padding: 2px; + background-color: #333333; + border: 1px solid #545454; + color: white; +} + chatterino--PageHeader { margin-bottom: 12px; } diff --git a/resources/raw/README.md b/resources/raw/README.md new file mode 100644 index 000000000..12351802c --- /dev/null +++ b/resources/raw/README.md @@ -0,0 +1,4 @@ +# `resources/raw` + +This directory contains raw source files used to generate images in `resources` and isn't included in the final executable. +These files are not optimized/minimized. diff --git a/resources/buttons/copyDark.svg b/resources/raw/buttons/copyDark.svg similarity index 100% rename from resources/buttons/copyDark.svg rename to resources/raw/buttons/copyDark.svg diff --git a/resources/buttons/copyLight.svg b/resources/raw/buttons/copyLight.svg similarity index 100% rename from resources/buttons/copyLight.svg rename to resources/raw/buttons/copyLight.svg diff --git a/resources/raw/buttons/pinDisabledDark.svg b/resources/raw/buttons/pinDisabledDark.svg new file mode 100644 index 000000000..373864e02 --- /dev/null +++ b/resources/raw/buttons/pinDisabledDark.svg @@ -0,0 +1,23 @@ + + + + diff --git a/resources/raw/buttons/pinDisabledLight.svg b/resources/raw/buttons/pinDisabledLight.svg new file mode 100644 index 000000000..eb086e434 --- /dev/null +++ b/resources/raw/buttons/pinDisabledLight.svg @@ -0,0 +1,23 @@ + + + + diff --git a/resources/raw/buttons/pinEnabled.svg b/resources/raw/buttons/pinEnabled.svg new file mode 100644 index 000000000..412d1c99b --- /dev/null +++ b/resources/raw/buttons/pinEnabled.svg @@ -0,0 +1,23 @@ + + + + diff --git a/resources/buttons/replyDark.svg b/resources/raw/buttons/replyDark.svg similarity index 100% rename from resources/buttons/replyDark.svg rename to resources/raw/buttons/replyDark.svg diff --git a/resources/buttons/replyThreadDark.svg b/resources/raw/buttons/replyThreadDark.svg similarity index 100% rename from resources/buttons/replyThreadDark.svg rename to resources/raw/buttons/replyThreadDark.svg diff --git a/resources/raw/buttons/streamerModeEnabledDark.svg b/resources/raw/buttons/streamerModeEnabledDark.svg new file mode 100644 index 000000000..7f95aae5f --- /dev/null +++ b/resources/raw/buttons/streamerModeEnabledDark.svg @@ -0,0 +1,13 @@ + + + + + + + diff --git a/resources/raw/buttons/streamerModeEnabledLight.svg b/resources/raw/buttons/streamerModeEnabledLight.svg new file mode 100644 index 000000000..5f27d4393 --- /dev/null +++ b/resources/raw/buttons/streamerModeEnabledLight.svg @@ -0,0 +1,13 @@ + + + + + + + diff --git a/resources/buttons/trashcan.svg b/resources/raw/buttons/trashcan.svg similarity index 100% rename from resources/buttons/trashcan.svg rename to resources/raw/buttons/trashcan.svg diff --git a/resources/scrolling/downScroll.svg b/resources/raw/scrolling/downScroll.svg similarity index 100% rename from resources/scrolling/downScroll.svg rename to resources/raw/scrolling/downScroll.svg diff --git a/resources/scrolling/neutralScroll.svg b/resources/raw/scrolling/neutralScroll.svg similarity index 100% rename from resources/scrolling/neutralScroll.svg rename to resources/raw/scrolling/neutralScroll.svg diff --git a/resources/scrolling/upScroll.svg b/resources/raw/scrolling/upScroll.svg similarity index 100% rename from resources/scrolling/upScroll.svg rename to resources/raw/scrolling/upScroll.svg diff --git a/resources/resources_autogenerated.qrc b/resources/resources_autogenerated.qrc deleted file mode 100644 index 2c91e0282..000000000 --- a/resources/resources_autogenerated.qrc +++ /dev/null @@ -1,125 +0,0 @@ - - - avatars/_1xelerate.png - avatars/alazymeme.png - avatars/brian6932.png - avatars/fourtf.png - avatars/hicupalot.png - avatars/iprodigy.png - avatars/jaxkey.png - avatars/kararty.png - avatars/karlpolice.png - avatars/matthewde.jpg - avatars/mm2pl.png - avatars/pajlada.png - avatars/revolter.jpg - avatars/slch.png - avatars/xheaveny.png - avatars/zneix.png - buttons/addSplit.png - buttons/addSplitDark.png - buttons/ban.png - buttons/banRed.png - buttons/cancel.svg - buttons/cancelDark.svg - buttons/clearSearch.png - buttons/copyDark.png - buttons/copyDark.svg - buttons/copyLight.png - buttons/copyLight.svg - buttons/emote.svg - buttons/emoteDark.svg - buttons/menuDark.png - buttons/menuLight.png - buttons/mod.png - buttons/modModeDisabled.png - buttons/modModeDisabled2.png - buttons/modModeEnabled.png - buttons/modModeEnabled2.png - buttons/replyDark.png - buttons/replyDark.svg - buttons/replyThreadDark.png - buttons/replyThreadDark.svg - buttons/search.png - buttons/timeout.png - buttons/trashCan.png - buttons/trashcan.svg - buttons/unban.png - buttons/unmod.png - buttons/unvip.png - buttons/update.png - buttons/updateError.png - buttons/viewersDark.png - buttons/viewersLight.png - buttons/vip.png - chatterino.icns - com.chatterino.chatterino.appdata.xml - com.chatterino.chatterino.desktop - contributors.txt - emoji.json - error.png - examples/moving.gif - examples/splitting.gif - icon.ico - icon.png - licenses/boost_boost.txt - licenses/emoji-data-source.txt - licenses/libcommuni_BSD3.txt - licenses/lrucache.txt - licenses/magic_enum.txt - licenses/openssl.txt - licenses/pajlada_settings.txt - licenses/pajlada_signals.txt - licenses/qt_lgpl-3.0.txt - licenses/qtkeychain.txt - licenses/rapidjson.txt - licenses/websocketpp.txt - pajaDank.png - qss/settings.qss - scrolling/downScroll.png - scrolling/downScroll.svg - scrolling/neutralScroll.png - scrolling/neutralScroll.svg - scrolling/upScroll.png - scrolling/upScroll.svg - settings/about.svg - settings/aboutlogo.png - settings/accounts.svg - settings/advanced.svg - settings/behave.svg - settings/browser.svg - settings/commands.svg - settings/emote.svg - settings/externaltools.svg - settings/filters.svg - settings/ignore.svg - settings/keybinds.svg - settings/moderation.svg - settings/notification2.svg - settings/notifications.svg - settings/theme.svg - sounds/ping2.wav - split/down.png - split/left.png - split/move.png - split/right.png - split/up.png - streamerMode.png - switcher/plus.svg - switcher/popup.svg - switcher/switch.svg - tlds.txt - twitch/admin.png - twitch/automod.png - twitch/broadcaster.png - twitch/cheer1.png - twitch/globalmod.png - twitch/moderator.png - twitch/prime.png - twitch/staff.png - twitch/subscriber.png - twitch/turbo.png - twitch/verified.png - twitch/vip.png - - diff --git a/resources/scrolling/downScroll.png b/resources/scrolling/downScroll.png index cc46bb5c8..8a6c0e059 100644 Binary files a/resources/scrolling/downScroll.png and b/resources/scrolling/downScroll.png differ diff --git a/resources/scrolling/neutralScroll.png b/resources/scrolling/neutralScroll.png index 99ad1e7f2..ee90a7665 100644 Binary files a/resources/scrolling/neutralScroll.png and b/resources/scrolling/neutralScroll.png differ diff --git a/resources/scrolling/upScroll.png b/resources/scrolling/upScroll.png index 433642978..d7a5c91c3 100644 Binary files a/resources/scrolling/upScroll.png and b/resources/scrolling/upScroll.png differ diff --git a/resources/settings/about.svg b/resources/settings/about.svg index d0427d04c..a417c3323 100644 --- a/resources/settings/about.svg +++ b/resources/settings/about.svg @@ -1,24 +1 @@ - - - - - - - - - - - + \ No newline at end of file diff --git a/resources/settings/aboutlogo.png b/resources/settings/aboutlogo.png index 3efa9ab7b..c8e1997bb 100644 Binary files a/resources/settings/aboutlogo.png and b/resources/settings/aboutlogo.png differ diff --git a/resources/settings/accounts.svg b/resources/settings/accounts.svg index 8edc7ba06..3413257c6 100644 --- a/resources/settings/accounts.svg +++ b/resources/settings/accounts.svg @@ -1,16 +1 @@ - - - - - - - - - - + \ No newline at end of file diff --git a/resources/settings/advanced.svg b/resources/settings/advanced.svg deleted file mode 100644 index bf0c11a0f..000000000 --- a/resources/settings/advanced.svg +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/resources/settings/behave.svg b/resources/settings/behave.svg deleted file mode 100644 index da58c84c9..000000000 --- a/resources/settings/behave.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - diff --git a/resources/settings/browser.svg b/resources/settings/browser.svg deleted file mode 100644 index a40a4c138..000000000 --- a/resources/settings/browser.svg +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/resources/settings/commands.svg b/resources/settings/commands.svg index 0d29cfc2a..69d7ae589 100644 --- a/resources/settings/commands.svg +++ b/resources/settings/commands.svg @@ -1,23 +1 @@ - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/resources/settings/emote.svg b/resources/settings/emote.svg deleted file mode 100644 index 10e25c9f5..000000000 --- a/resources/settings/emote.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - diff --git a/resources/settings/externaltools.svg b/resources/settings/externaltools.svg index 3bbef37d1..8d970592c 100644 --- a/resources/settings/externaltools.svg +++ b/resources/settings/externaltools.svg @@ -1,47 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/resources/settings/filters.svg b/resources/settings/filters.svg index efee0adc2..7e94717ff 100644 --- a/resources/settings/filters.svg +++ b/resources/settings/filters.svg @@ -1,16 +1 @@ - - - - - -image/svg+xml - - - - - - - - - - + \ No newline at end of file diff --git a/resources/settings/ignore.svg b/resources/settings/ignore.svg index 10f0e691b..ef4058712 100644 --- a/resources/settings/ignore.svg +++ b/resources/settings/ignore.svg @@ -1,22 +1 @@ - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/resources/settings/keybinds.svg b/resources/settings/keybinds.svg index 36991c58e..bbf9ede29 100644 --- a/resources/settings/keybinds.svg +++ b/resources/settings/keybinds.svg @@ -1,11 +1 @@ - - - - - - + \ No newline at end of file diff --git a/resources/settings/moderation.svg b/resources/settings/moderation.svg index bb01a7cce..f97991bc8 100644 --- a/resources/settings/moderation.svg +++ b/resources/settings/moderation.svg @@ -1,46 +1,13 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + \ No newline at end of file diff --git a/resources/settings/notification2.svg b/resources/settings/notification2.svg index d5fb508c7..2f0071f5b 100644 --- a/resources/settings/notification2.svg +++ b/resources/settings/notification2.svg @@ -1,33 +1,14 @@ - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + \ No newline at end of file diff --git a/resources/settings/notifications.svg b/resources/settings/notifications.svg index 2ccd2d97b..fec06e315 100644 --- a/resources/settings/notifications.svg +++ b/resources/settings/notifications.svg @@ -1,20 +1 @@ - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/resources/settings/plugins.svg b/resources/settings/plugins.svg new file mode 100644 index 000000000..686ee6b4d --- /dev/null +++ b/resources/settings/plugins.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/settings/theme.svg b/resources/settings/theme.svg deleted file mode 100644 index fa85cb274..000000000 --- a/resources/settings/theme.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - diff --git a/resources/split/down.png b/resources/split/down.png index 042f1f4c2..cd5162b55 100644 Binary files a/resources/split/down.png and b/resources/split/down.png differ diff --git a/resources/split/left.png b/resources/split/left.png index 210b74b48..fc7ec23aa 100644 Binary files a/resources/split/left.png and b/resources/split/left.png differ diff --git a/resources/split/move.png b/resources/split/move.png index e4ce85cea..7f8b21bbd 100644 Binary files a/resources/split/move.png and b/resources/split/move.png differ diff --git a/resources/split/right.png b/resources/split/right.png index 88963fb35..46e19946e 100644 Binary files a/resources/split/right.png and b/resources/split/right.png differ diff --git a/resources/split/up.png b/resources/split/up.png index 55662c9b0..3446419ed 100644 Binary files a/resources/split/up.png and b/resources/split/up.png differ diff --git a/resources/switcher/plus.svg b/resources/switcher/plus.svg index bf8ead17e..64eba9de3 100644 --- a/resources/switcher/plus.svg +++ b/resources/switcher/plus.svg @@ -1,75 +1 @@ - - - - - - - - - - image/svg+xml - - - - - - - - - - - - + \ No newline at end of file diff --git a/resources/switcher/popup.svg b/resources/switcher/popup.svg index f382de01f..238e9bcdd 100644 --- a/resources/switcher/popup.svg +++ b/resources/switcher/popup.svg @@ -1,70 +1 @@ - -image/svg+xml + \ No newline at end of file diff --git a/resources/switcher/switch.svg b/resources/switcher/switch.svg index 4797699dc..db2b3d858 100644 --- a/resources/switcher/switch.svg +++ b/resources/switcher/switch.svg @@ -1,104 +1 @@ - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - + \ No newline at end of file diff --git a/resources/themes/Black.json b/resources/themes/Black.json new file mode 100644 index 000000000..20014cbed --- /dev/null +++ b/resources/themes/Black.json @@ -0,0 +1,114 @@ +{ + "$schema": "../../docs/ChatterinoTheme.schema.json", + "metadata": { + "iconTheme": "light" + }, + "colors": { + "accent": "#00aeef", + "messages": { + "backgrounds": { + "alternate": "#0a0a0a", + "regular": "#000000" + }, + "disabled": "#99000000", + "highlightAnimationEnd": "#00e6e6e6", + "highlightAnimationStart": "#6ee6e6e6", + "selection": "#40ffffff", + "textColors": { + "caret": "#ffffff", + "chatPlaceholder": "#5d5555", + "link": "#4286f4", + "regular": "#ffffff", + "system": "#8c7f7f" + } + }, + "scrollbars": { + "background": "#00000000", + "thumb": "#4d4d4d", + "thumbSelected": "#595959" + }, + "splits": { + "background": "#000000", + "dropPreview": "#300094ff", + "dropPreviewBorder": "#0094ff", + "dropTargetRect": "#000094ff", + "dropTargetRectBorder": "#000094ff", + "header": { + "background": "#050505", + "border": "#121212", + "focusedBackground": "#1a1a1a", + "focusedBorder": "#1c1c1c", + "focusedText": "#84c1ff", + "text": "#ffffff" + }, + "input": { + "background": "#080808", + "text": "#ffffff" + }, + "messageSeperator": "#3c3c3c", + "resizeHandle": "#700094ff", + "resizeHandleBackground": "#200094ff" + }, + "tabs": { + "liveIndicator": "#ff0000", + "rerunIndicator": "#c7c715", + "dividerLine": "#555555", + "highlighted": { + "backgrounds": { + "hover": "#0b0b0b", + "regular": "#0b0b0b", + "unfocused": "#0b0b0b" + }, + "line": { + "hover": "#ee6166", + "regular": "#ee6166", + "unfocused": "#ee6166" + }, + "text": "#eeeeee" + }, + "newMessage": { + "backgrounds": { + "hover": "#0b0b0b", + "regular": "#0b0b0b", + "unfocused": "#0b0b0b" + }, + "line": { + "hover": "#888888", + "regular": "#888888", + "unfocused": "#888888" + }, + "text": "#eeeeee" + }, + "regular": { + "backgrounds": { + "hover": "#0b0b0b", + "regular": "#0b0b0b", + "unfocused": "#0b0b0b" + }, + "line": { + "hover": "#444444", + "regular": "#444444", + "unfocused": "#444444" + }, + "text": "#aaaaaa" + }, + "selected": { + "backgrounds": { + "hover": "#333333", + "regular": "#333333", + "unfocused": "#333333" + }, + "line": { + "hover": "#00aeef", + "regular": "#00aeef", + "unfocused": "#00aeef" + }, + "text": "#ffffff" + } + }, + "window": { + "background": "#040404", + "text": "#eeeeee" + } + } +} diff --git a/resources/themes/Dark.json b/resources/themes/Dark.json new file mode 100644 index 000000000..cc0ff7f07 --- /dev/null +++ b/resources/themes/Dark.json @@ -0,0 +1,114 @@ +{ + "$schema": "../../docs/ChatterinoTheme.schema.json", + "metadata": { + "iconTheme": "light" + }, + "colors": { + "accent": "#00aeef", + "messages": { + "backgrounds": { + "alternate": "#222222", + "regular": "#191919" + }, + "disabled": "#99191919", + "highlightAnimationEnd": "#00e6e6e6", + "highlightAnimationStart": "#6ee6e6e6", + "selection": "#40ffffff", + "textColors": { + "caret": "#ffffff", + "chatPlaceholder": "#5d5555", + "link": "#4286f4", + "regular": "#ffffff", + "system": "#8c7f7f" + } + }, + "scrollbars": { + "background": "#00000000", + "thumb": "#575757", + "thumbSelected": "#616161" + }, + "splits": { + "background": "#191919", + "dropPreview": "#300094ff", + "dropPreviewBorder": "#0094ff", + "dropTargetRect": "#000094ff", + "dropTargetRectBorder": "#000094ff", + "header": { + "background": "#2e2e2e", + "border": "#383838", + "focusedBackground": "#444444", + "focusedBorder": "#464646", + "focusedText": "#84c1ff", + "text": "#ffffff" + }, + "input": { + "background": "#242424", + "text": "#ffffff" + }, + "messageSeperator": "#3c3c3c", + "resizeHandle": "#700094ff", + "resizeHandleBackground": "#200094ff" + }, + "tabs": { + "liveIndicator": "#ff0000", + "rerunIndicator": "#c7c715", + "dividerLine": "#555555", + "highlighted": { + "backgrounds": { + "hover": "#252525", + "regular": "#252525", + "unfocused": "#252525" + }, + "line": { + "hover": "#ee6166", + "regular": "#ee6166", + "unfocused": "#ee6166" + }, + "text": "#eeeeee" + }, + "newMessage": { + "backgrounds": { + "hover": "#252525", + "regular": "#252525", + "unfocused": "#252525" + }, + "line": { + "hover": "#888888", + "regular": "#888888", + "unfocused": "#888888" + }, + "text": "#eeeeee" + }, + "regular": { + "backgrounds": { + "hover": "#252525", + "regular": "#252525", + "unfocused": "#252525" + }, + "line": { + "hover": "#444444", + "regular": "#444444", + "unfocused": "#444444" + }, + "text": "#aaaaaa" + }, + "selected": { + "backgrounds": { + "hover": "#555555", + "regular": "#555555", + "unfocused": "#555555" + }, + "line": { + "hover": "#00aeef", + "regular": "#00aeef", + "unfocused": "#00aeef" + }, + "text": "#ffffff" + } + }, + "window": { + "background": "#111111", + "text": "#eeeeee" + } + } +} diff --git a/resources/themes/Light.json b/resources/themes/Light.json new file mode 100644 index 000000000..2097ff81b --- /dev/null +++ b/resources/themes/Light.json @@ -0,0 +1,114 @@ +{ + "$schema": "../../docs/ChatterinoTheme.schema.json", + "metadata": { + "iconTheme": "dark" + }, + "colors": { + "accent": "#00aeef", + "messages": { + "backgrounds": { + "alternate": "#dddddd", + "regular": "#e6e6e6" + }, + "disabled": "#99e6e6e6", + "highlightAnimationEnd": "#00141414", + "highlightAnimationStart": "#6e141414", + "selection": "#40000000", + "textColors": { + "caret": "#000000", + "chatPlaceholder": "#af9f9f", + "link": "#4286f4", + "regular": "#000000", + "system": "#8c7f7f" + } + }, + "scrollbars": { + "background": "#00000000", + "thumb": "#a8a8a8", + "thumbSelected": "#9e9e9e" + }, + "splits": { + "background": "#e6e6e6", + "dropPreview": "#300094ff", + "dropPreviewBorder": "#0094ff", + "dropTargetRect": "#00ffffff", + "dropTargetRectBorder": "#000094ff", + "header": { + "background": "#e6e6e6", + "border": "#e6e6e6", + "focusedBackground": "#dbdbdb", + "focusedBorder": "#d1d1d1", + "focusedText": "#0051a3", + "text": "#000000" + }, + "input": { + "background": "#dbdbdb", + "text": "#000000" + }, + "messageSeperator": "#7f7f7f", + "resizeHandle": "#0094ff", + "resizeHandleBackground": "#500094ff" + }, + "tabs": { + "liveIndicator": "#ff0000", + "rerunIndicator": "#c7c715", + "dividerLine": "#b4d7ff", + "highlighted": { + "backgrounds": { + "hover": "#eeeeee", + "regular": "#ffffff", + "unfocused": "#ffffff" + }, + "line": { + "hover": "#ff0000", + "regular": "#ff0000", + "unfocused": "#ff0000" + }, + "text": "#000000" + }, + "newMessage": { + "backgrounds": { + "hover": "#eeeeee", + "regular": "#ffffff", + "unfocused": "#ffffff" + }, + "line": { + "hover": "#bbbbbb", + "regular": "#bbbbbb", + "unfocused": "#bbbbbb" + }, + "text": "#222222" + }, + "regular": { + "backgrounds": { + "hover": "#eeeeee", + "regular": "#ffffff", + "unfocused": "#ffffff" + }, + "line": { + "hover": "#ffffff", + "regular": "#ffffff", + "unfocused": "#ffffff" + }, + "text": "#444444" + }, + "selected": { + "backgrounds": { + "hover": "#b4d7ff", + "regular": "#b4d7ff", + "unfocused": "#b4d7ff" + }, + "line": { + "hover": "#00aeef", + "regular": "#00aeef", + "unfocused": "#00aeef" + }, + "text": "#000000" + } + }, + "window": { + "background": "#ffffff", + "text": "#000000" + } + } +} diff --git a/resources/themes/White.json b/resources/themes/White.json new file mode 100644 index 000000000..950cfc6e0 --- /dev/null +++ b/resources/themes/White.json @@ -0,0 +1,114 @@ +{ + "$schema": "../../docs/ChatterinoTheme.schema.json", + "metadata": { + "iconTheme": "dark" + }, + "colors": { + "accent": "#00aeef", + "messages": { + "backgrounds": { + "alternate": "#f5f5f5", + "regular": "#ffffff" + }, + "disabled": "#99ffffff", + "highlightAnimationEnd": "#00141414", + "highlightAnimationStart": "#6e141414", + "selection": "#40000000", + "textColors": { + "caret": "#000000", + "chatPlaceholder": "#af9f9f", + "link": "#4286f4", + "regular": "#000000", + "system": "#8c7f7f" + } + }, + "scrollbars": { + "background": "#00000000", + "thumb": "#b3b3b3", + "thumbSelected": "#a6a6a6" + }, + "splits": { + "background": "#ffffff", + "dropPreview": "#300094ff", + "dropPreviewBorder": "#0094ff", + "dropTargetRect": "#00ffffff", + "dropTargetRectBorder": "#000094ff", + "header": { + "background": "#ffffff", + "border": "#ffffff", + "focusedBackground": "#f2f2f2", + "focusedBorder": "#e6e6e6", + "focusedText": "#0051a3", + "text": "#000000" + }, + "input": { + "background": "#f2f2f2", + "text": "#000000" + }, + "messageSeperator": "#7f7f7f", + "resizeHandle": "#0094ff", + "resizeHandleBackground": "#500094ff" + }, + "tabs": { + "liveIndicator": "#ff0000", + "rerunIndicator": "#c7c715", + "dividerLine": "#b4d7ff", + "highlighted": { + "backgrounds": { + "hover": "#eeeeee", + "regular": "#ffffff", + "unfocused": "#ffffff" + }, + "line": { + "hover": "#ff0000", + "regular": "#ff0000", + "unfocused": "#ff0000" + }, + "text": "#000000" + }, + "newMessage": { + "backgrounds": { + "hover": "#eeeeee", + "regular": "#ffffff", + "unfocused": "#ffffff" + }, + "line": { + "hover": "#bbbbbb", + "regular": "#bbbbbb", + "unfocused": "#bbbbbb" + }, + "text": "#222222" + }, + "regular": { + "backgrounds": { + "hover": "#eeeeee", + "regular": "#ffffff", + "unfocused": "#ffffff" + }, + "line": { + "hover": "#ffffff", + "regular": "#ffffff", + "unfocused": "#ffffff" + }, + "text": "#444444" + }, + "selected": { + "backgrounds": { + "hover": "#b4d7ff", + "regular": "#b4d7ff", + "unfocused": "#b4d7ff" + }, + "line": { + "hover": "#00aeef", + "regular": "#00aeef", + "unfocused": "#00aeef" + }, + "text": "#000000" + } + }, + "window": { + "background": "#ffffff", + "text": "#000000" + } + } +} diff --git a/resources/twitch-badges.json b/resources/twitch-badges.json new file mode 100644 index 000000000..b90fa1f25 --- /dev/null +++ b/resources/twitch-badges.json @@ -0,0 +1 @@ +{"data":[{"set_id":"1979-revolution_1","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/1979%20Revolution/details","description":"1979 Revolution","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/7833bb6e-d20d-48ff-a58d-67fe827a4f84/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/7833bb6e-d20d-48ff-a58d-67fe827a4f84/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/7833bb6e-d20d-48ff-a58d-67fe827a4f84/3","title":"1979 Revolution"}]},{"set_id":"60-seconds_1","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/60%20Seconds!/details","description":"60 Seconds!","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/1e7252f9-7e80-4d3d-ae42-319f030cca99/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/1e7252f9-7e80-4d3d-ae42-319f030cca99/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/1e7252f9-7e80-4d3d-ae42-319f030cca99/3","title":"60 Seconds!"}]},{"set_id":"60-seconds_2","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/60%20Seconds!/details","description":"60 Seconds!","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/64513f7d-21dd-4a05-a699-d73761945cf9/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/64513f7d-21dd-4a05-a699-d73761945cf9/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/64513f7d-21dd-4a05-a699-d73761945cf9/3","title":"60 Seconds!"}]},{"set_id":"60-seconds_3","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/60%20Seconds!/details","description":"60 Seconds!","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/f4306617-0f96-476f-994e-5304f81bcc6e/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/f4306617-0f96-476f-994e-5304f81bcc6e/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/f4306617-0f96-476f-994e-5304f81bcc6e/3","title":"60 Seconds!"}]},{"set_id":"H1Z1_1","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/H1Z1/details","description":"H1Z1","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/fc71386c-86cd-11e7-a55d-43f591dc0c71/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/fc71386c-86cd-11e7-a55d-43f591dc0c71/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/fc71386c-86cd-11e7-a55d-43f591dc0c71/3","title":"H1Z1"}]},{"set_id":"admin","versions":[{"click_action":null,"click_url":null,"description":"Twitch Admin","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/9ef7e029-4cdf-4d4d-a0d5-e2b3fb2583fe/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/9ef7e029-4cdf-4d4d-a0d5-e2b3fb2583fe/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/9ef7e029-4cdf-4d4d-a0d5-e2b3fb2583fe/3","title":"Admin"}]},{"set_id":"ambassador","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/team/ambassadors","description":"Twitch Ambassador","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/2cbc339f-34f4-488a-ae51-efdf74f4e323/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/2cbc339f-34f4-488a-ae51-efdf74f4e323/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/2cbc339f-34f4-488a-ae51-efdf74f4e323/3","title":"Twitch Ambassador"}]},{"set_id":"anomaly-2_1","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/Anomaly%202/details","description":"Anomaly 2","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/d1d1ad54-40a6-492b-882e-dcbdce5fa81e/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/d1d1ad54-40a6-492b-882e-dcbdce5fa81e/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/d1d1ad54-40a6-492b-882e-dcbdce5fa81e/3","title":"Anomaly 2"}]},{"set_id":"anomaly-warzone-earth_1","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/Anomaly:%20Warzone%20Earth/details","description":"Anomaly Warzone Earth","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/858be873-fb1f-47e5-ad34-657f40d3d156/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/858be873-fb1f-47e5-ad34-657f40d3d156/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/858be873-fb1f-47e5-ad34-657f40d3d156/3","title":"Anomaly Warzone Earth"}]},{"set_id":"anonymous-cheerer","versions":[{"click_action":null,"click_url":null,"description":"Anonymous Cheerer","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/ca3db7f7-18f5-487e-a329-cd0b538ee979/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/ca3db7f7-18f5-487e-a329-cd0b538ee979/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/ca3db7f7-18f5-487e-a329-cd0b538ee979/3","title":"Anonymous Cheerer"}]},{"set_id":"artist-badge","versions":[{"click_action":null,"click_url":null,"description":"Artist on this Channel","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/4300a897-03dc-4e83-8c0e-c332fee7057f/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/4300a897-03dc-4e83-8c0e-c332fee7057f/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/4300a897-03dc-4e83-8c0e-c332fee7057f/3","title":"Artist"}]},{"set_id":"axiom-verge_1","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/Axiom%20Verge/details","description":"Axiom Verge","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/f209b747-45ee-42f6-8baf-ea7542633d10/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/f209b747-45ee-42f6-8baf-ea7542633d10/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/f209b747-45ee-42f6-8baf-ea7542633d10/3","title":"Axiom Verge"}]},{"set_id":"battlechefbrigade_1","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/Battle%20Chef%20Brigade/details","description":"Battle Chef Brigade","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/24e32e67-33cd-4227-ad96-f0a7fc836107/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/24e32e67-33cd-4227-ad96-f0a7fc836107/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/24e32e67-33cd-4227-ad96-f0a7fc836107/3","title":"Battle Chef Brigade"}]},{"set_id":"battlechefbrigade_2","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/Battle%20Chef%20Brigade/details","description":"Battle Chef Brigade","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/ef1e96e8-a0f9-40b6-87af-2977d3c004bb/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/ef1e96e8-a0f9-40b6-87af-2977d3c004bb/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/ef1e96e8-a0f9-40b6-87af-2977d3c004bb/3","title":"Battle Chef Brigade"}]},{"set_id":"battlechefbrigade_3","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/Battle%20Chef%20Brigade/details","description":"Battle Chef Brigade","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/107ebb20-4fcd-449a-9931-cd3f81b84c70/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/107ebb20-4fcd-449a-9931-cd3f81b84c70/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/107ebb20-4fcd-449a-9931-cd3f81b84c70/3","title":"Battle Chef Brigade"}]},{"set_id":"battlerite_1","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/Battlerite/details","description":"Battlerite","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/484ebda9-f7fa-4c67-b12b-c80582f3cc61/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/484ebda9-f7fa-4c67-b12b-c80582f3cc61/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/484ebda9-f7fa-4c67-b12b-c80582f3cc61/3","title":"Battlerite"}]},{"set_id":"bits","versions":[{"click_action":"visit_url","click_url":"https://bits.twitch.tv","description":" ","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/73b5c3fb-24f9-4a82-a852-2f475b59411c/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/73b5c3fb-24f9-4a82-a852-2f475b59411c/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/73b5c3fb-24f9-4a82-a852-2f475b59411c/3","title":"cheer 1"},{"click_action":"visit_url","click_url":"https://bits.twitch.tv","description":" ","id":"100","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/09d93036-e7ce-431c-9a9e-7044297133f2/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/09d93036-e7ce-431c-9a9e-7044297133f2/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/09d93036-e7ce-431c-9a9e-7044297133f2/3","title":"cheer 100"},{"click_action":"visit_url","click_url":"https://bits.twitch.tv","description":" ","id":"1000","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/0d85a29e-79ad-4c63-a285-3acd2c66f2ba/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/0d85a29e-79ad-4c63-a285-3acd2c66f2ba/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/0d85a29e-79ad-4c63-a285-3acd2c66f2ba/3","title":"cheer 1000"},{"click_action":"visit_url","click_url":"https://bits.twitch.tv","description":" ","id":"10000","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/68af213b-a771-4124-b6e3-9bb6d98aa732/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/68af213b-a771-4124-b6e3-9bb6d98aa732/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/68af213b-a771-4124-b6e3-9bb6d98aa732/3","title":"cheer 10000"},{"click_action":"visit_url","click_url":"https://bits.twitch.tv","description":" ","id":"100000","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/96f0540f-aa63-49e1-a8b3-259ece3bd098/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/96f0540f-aa63-49e1-a8b3-259ece3bd098/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/96f0540f-aa63-49e1-a8b3-259ece3bd098/3","title":"cheer 100000"},{"click_action":"visit_url","click_url":"https://bits.twitch.tv","description":" ","id":"1000000","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/494d1c8e-c3b2-4d88-8528-baff57c9bd3f/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/494d1c8e-c3b2-4d88-8528-baff57c9bd3f/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/494d1c8e-c3b2-4d88-8528-baff57c9bd3f/3","title":"cheer 1000000"},{"click_action":"visit_url","click_url":"https://bits.twitch.tv","description":" ","id":"1250000","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/ce217209-4615-4bf8-81e3-57d06b8b9dc7/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/ce217209-4615-4bf8-81e3-57d06b8b9dc7/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/ce217209-4615-4bf8-81e3-57d06b8b9dc7/3","title":"cheer 1250000"},{"click_action":"visit_url","click_url":"https://bits.twitch.tv","description":" ","id":"1500000","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/c4eba5b4-17a7-40a1-a668-bc1972c1e24d/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/c4eba5b4-17a7-40a1-a668-bc1972c1e24d/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/c4eba5b4-17a7-40a1-a668-bc1972c1e24d/3","title":"cheer 1500000"},{"click_action":"visit_url","click_url":"https://bits.twitch.tv","description":" ","id":"1750000","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/183f1fd8-aaf4-450c-a413-e53f839f0f82/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/183f1fd8-aaf4-450c-a413-e53f839f0f82/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/183f1fd8-aaf4-450c-a413-e53f839f0f82/3","title":"cheer 1750000"},{"click_action":"visit_url","click_url":"https://bits.twitch.tv","description":" ","id":"200000","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/4a0b90c4-e4ef-407f-84fe-36b14aebdbb6/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/4a0b90c4-e4ef-407f-84fe-36b14aebdbb6/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/4a0b90c4-e4ef-407f-84fe-36b14aebdbb6/3","title":"cheer 200000"},{"click_action":"visit_url","click_url":"https://bits.twitch.tv","description":" ","id":"2000000","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/7ea89c53-1a3b-45f9-9223-d97ae19089f2/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/7ea89c53-1a3b-45f9-9223-d97ae19089f2/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/7ea89c53-1a3b-45f9-9223-d97ae19089f2/3","title":"cheer 2000000"},{"click_action":"visit_url","click_url":"https://bits.twitch.tv","description":" ","id":"25000","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/64ca5920-c663-4bd8-bfb1-751b4caea2dd/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/64ca5920-c663-4bd8-bfb1-751b4caea2dd/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/64ca5920-c663-4bd8-bfb1-751b4caea2dd/3","title":"cheer 25000"},{"click_action":"visit_url","click_url":"https://bits.twitch.tv","description":" ","id":"2500000","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/cf061daf-d571-4811-bcc2-c55c8792bc8f/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/cf061daf-d571-4811-bcc2-c55c8792bc8f/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/cf061daf-d571-4811-bcc2-c55c8792bc8f/3","title":"cheer 2500000"},{"click_action":"visit_url","click_url":"https://bits.twitch.tv","description":" ","id":"300000","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/ac13372d-2e94-41d1-ae11-ecd677f69bb6/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/ac13372d-2e94-41d1-ae11-ecd677f69bb6/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/ac13372d-2e94-41d1-ae11-ecd677f69bb6/3","title":"cheer 300000"},{"click_action":"visit_url","click_url":"https://bits.twitch.tv","description":" ","id":"3000000","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/5671797f-5e9f-478c-a2b5-eb086c8928cf/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/5671797f-5e9f-478c-a2b5-eb086c8928cf/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/5671797f-5e9f-478c-a2b5-eb086c8928cf/3","title":"cheer 3000000"},{"click_action":"visit_url","click_url":"https://bits.twitch.tv","description":" ","id":"3500000","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/c3d218f5-1e45-419d-9c11-033a1ae54d3a/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/c3d218f5-1e45-419d-9c11-033a1ae54d3a/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/c3d218f5-1e45-419d-9c11-033a1ae54d3a/3","title":"cheer 3500000"},{"click_action":"visit_url","click_url":"https://bits.twitch.tv","description":" ","id":"400000","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/a8f393af-76e6-4aa2-9dd0-7dcc1c34f036/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/a8f393af-76e6-4aa2-9dd0-7dcc1c34f036/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/a8f393af-76e6-4aa2-9dd0-7dcc1c34f036/3","title":"cheer 400000"},{"click_action":"visit_url","click_url":"https://bits.twitch.tv","description":" ","id":"4000000","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/79fe642a-87f3-40b1-892e-a341747b6e08/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/79fe642a-87f3-40b1-892e-a341747b6e08/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/79fe642a-87f3-40b1-892e-a341747b6e08/3","title":"cheer 4000000"},{"click_action":"visit_url","click_url":"https://bits.twitch.tv","description":" ","id":"4500000","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/736d4156-ac67-4256-a224-3e6e915436db/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/736d4156-ac67-4256-a224-3e6e915436db/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/736d4156-ac67-4256-a224-3e6e915436db/3","title":"cheer 4500000"},{"click_action":"visit_url","click_url":"https://bits.twitch.tv","description":" ","id":"5000","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/57cd97fc-3e9e-4c6d-9d41-60147137234e/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/57cd97fc-3e9e-4c6d-9d41-60147137234e/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/57cd97fc-3e9e-4c6d-9d41-60147137234e/3","title":"cheer 5000"},{"click_action":"visit_url","click_url":"https://bits.twitch.tv","description":" ","id":"50000","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/62310ba7-9916-4235-9eba-40110d67f85d/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/62310ba7-9916-4235-9eba-40110d67f85d/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/62310ba7-9916-4235-9eba-40110d67f85d/3","title":"cheer 50000"},{"click_action":"visit_url","click_url":"https://bits.twitch.tv","description":" ","id":"500000","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/f6932b57-6a6e-4062-a770-dfbd9f4302e5/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/f6932b57-6a6e-4062-a770-dfbd9f4302e5/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/f6932b57-6a6e-4062-a770-dfbd9f4302e5/3","title":"cheer 500000"},{"click_action":"visit_url","click_url":"https://bits.twitch.tv","description":" ","id":"5000000","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/3f085f85-8d15-4a03-a829-17fca7bf1bc2/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/3f085f85-8d15-4a03-a829-17fca7bf1bc2/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/3f085f85-8d15-4a03-a829-17fca7bf1bc2/3","title":"cheer 5000000"},{"click_action":"visit_url","click_url":"https://bits.twitch.tv","description":" ","id":"600000","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/4d908059-f91c-4aef-9acb-634434f4c32e/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/4d908059-f91c-4aef-9acb-634434f4c32e/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/4d908059-f91c-4aef-9acb-634434f4c32e/3","title":"cheer 600000"},{"click_action":"visit_url","click_url":"https://bits.twitch.tv","description":" ","id":"700000","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/a1d2a824-f216-4b9f-9642-3de8ed370957/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/a1d2a824-f216-4b9f-9642-3de8ed370957/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/a1d2a824-f216-4b9f-9642-3de8ed370957/3","title":"cheer 700000"},{"click_action":"visit_url","click_url":"https://bits.twitch.tv","description":" ","id":"75000","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/ce491fa4-b24f-4f3b-b6ff-44b080202792/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/ce491fa4-b24f-4f3b-b6ff-44b080202792/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/ce491fa4-b24f-4f3b-b6ff-44b080202792/3","title":"cheer 75000"},{"click_action":"visit_url","click_url":"https://bits.twitch.tv","description":" ","id":"800000","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/5ec2ee3e-5633-4c2a-8e77-77473fe409e6/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/5ec2ee3e-5633-4c2a-8e77-77473fe409e6/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/5ec2ee3e-5633-4c2a-8e77-77473fe409e6/3","title":"cheer 800000"},{"click_action":"visit_url","click_url":"https://bits.twitch.tv","description":" ","id":"900000","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/088c58c6-7c38-45ba-8f73-63ef24189b84/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/088c58c6-7c38-45ba-8f73-63ef24189b84/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/088c58c6-7c38-45ba-8f73-63ef24189b84/3","title":"cheer 900000"}]},{"set_id":"bits-charity","versions":[{"click_action":"visit_url","click_url":"https://link.twitch.tv/blizzardofbits","description":"Supported their favorite streamer during the 2018 Blizzard of Bits","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/a539dc18-ae19-49b0-98c4-8391a594332b/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/a539dc18-ae19-49b0-98c4-8391a594332b/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/a539dc18-ae19-49b0-98c4-8391a594332b/3","title":"Direct Relief - Charity 2018"}]},{"set_id":"bits-leader","versions":[{"click_action":"visit_url","click_url":"https://bits.twitch.tv","description":"Ranked as a top cheerer on this channel","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/8bedf8c3-7a6d-4df2-b62f-791b96a5dd31/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/8bedf8c3-7a6d-4df2-b62f-791b96a5dd31/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/8bedf8c3-7a6d-4df2-b62f-791b96a5dd31/3","title":"Bits Leader 1"},{"click_action":"visit_url","click_url":"https://bits.twitch.tv","description":"Ranked as a top cheerer on this channel","id":"2","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/f04baac7-9141-4456-a0e7-6301bcc34138/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/f04baac7-9141-4456-a0e7-6301bcc34138/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/f04baac7-9141-4456-a0e7-6301bcc34138/3","title":"Bits Leader 2"},{"click_action":"visit_url","click_url":"https://bits.twitch.tv","description":"Ranked as a top cheerer on this channel","id":"3","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/f1d2aab6-b647-47af-965b-84909cf303aa/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/f1d2aab6-b647-47af-965b-84909cf303aa/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/f1d2aab6-b647-47af-965b-84909cf303aa/3","title":"Bits Leader 3"}]},{"set_id":"brawlhalla_1","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/Brawlhalla/details","description":"Brawlhalla","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/bf6d6579-ab02-4f0a-9f64-a51c37040858/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/bf6d6579-ab02-4f0a-9f64-a51c37040858/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/bf6d6579-ab02-4f0a-9f64-a51c37040858/3","title":"Brawlhalla"}]},{"set_id":"broadcaster","versions":[{"click_action":null,"click_url":null,"description":"Broadcaster","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/5527c58c-fb7d-422d-b71b-f309dcb85cc1/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/5527c58c-fb7d-422d-b71b-f309dcb85cc1/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/5527c58c-fb7d-422d-b71b-f309dcb85cc1/3","title":"Broadcaster"}]},{"set_id":"broken-age_1","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/Broken%20Age/details","description":"Broken Age","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/56885ed2-9a09-4c8e-8131-3eb9ec15af94/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/56885ed2-9a09-4c8e-8131-3eb9ec15af94/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/56885ed2-9a09-4c8e-8131-3eb9ec15af94/3","title":"Broken Age"}]},{"set_id":"bubsy-the-woolies_1","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/Bubsy:%20The%20Woolies%20Strike%20Back/details","description":"Bubsy: The Woolies Strike Back","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/c8129382-1f4e-4d15-a8d2-48bdddba9b81/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/c8129382-1f4e-4d15-a8d2-48bdddba9b81/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/c8129382-1f4e-4d15-a8d2-48bdddba9b81/3","title":"Bubsy: The Woolies Strike Back"}]},{"set_id":"chatter-cs-go-2022","versions":[{"click_action":null,"click_url":null,"description":"Chatted during CS:GO Week Brazil 2022","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/57b6bd6b-a1b5-4204-9e6c-eb8ed5831603/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/57b6bd6b-a1b5-4204-9e6c-eb8ed5831603/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/57b6bd6b-a1b5-4204-9e6c-eb8ed5831603/3","title":"CS:GO Week Brazil 2022"}]},{"set_id":"clip-champ","versions":[{"click_action":"visit_url","click_url":"https://help.twitch.tv/customer/portal/articles/2918323-clip-champs-guide","description":"Power Clipper","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/f38976e0-ffc9-11e7-86d6-7f98b26a9d79/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/f38976e0-ffc9-11e7-86d6-7f98b26a9d79/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/f38976e0-ffc9-11e7-86d6-7f98b26a9d79/3","title":"Power Clipper"}]},{"set_id":"creator-cs-go-2022","versions":[{"click_action":null,"click_url":null,"description":"Streamed during CS:GO Week Brazil 2022","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/a2ea6df9-ac0a-4956-bfe9-e931f50b94fa/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/a2ea6df9-ac0a-4956-bfe9-e931f50b94fa/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/a2ea6df9-ac0a-4956-bfe9-e931f50b94fa/3","title":"CS:GO Week Brazil 2022"}]},{"set_id":"cuphead_1","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/Cuphead/details","description":"Cuphead","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/4384659a-a2e3-11e7-a564-87f6b1288bab/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/4384659a-a2e3-11e7-a564-87f6b1288bab/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/4384659a-a2e3-11e7-a564-87f6b1288bab/3","title":"Cuphead"}]},{"set_id":"darkest-dungeon_1","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/Darkest%20Dungeon/details","description":"Darkest Dungeon","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/52a98ddd-cc79-46a8-9fe3-30f8c719bc2d/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/52a98ddd-cc79-46a8-9fe3-30f8c719bc2d/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/52a98ddd-cc79-46a8-9fe3-30f8c719bc2d/3","title":"Darkest Dungeon"}]},{"set_id":"deceit_1","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/Deceit/details","description":"Deceit","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/b14fef48-4ff9-4063-abf6-579489234fe9/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/b14fef48-4ff9-4063-abf6-579489234fe9/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/b14fef48-4ff9-4063-abf6-579489234fe9/3","title":"Deceit"}]},{"set_id":"devil-may-cry-hd_1","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/Devil%20May%20Cry%20HD%20Collection/details","description":"Devil May Cry HD Collection","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/633877d4-a91c-4c36-b75b-803f82b1352f/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/633877d4-a91c-4c36-b75b-803f82b1352f/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/633877d4-a91c-4c36-b75b-803f82b1352f/3","title":"Devil May Cry HD Collection"}]},{"set_id":"devil-may-cry-hd_2","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/Devil%20May%20Cry%20HD%20Collection/details","description":"Devil May Cry HD Collection","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/408548fe-aa74-4d53-b5e9-960103d9b865/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/408548fe-aa74-4d53-b5e9-960103d9b865/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/408548fe-aa74-4d53-b5e9-960103d9b865/3","title":"Devil May Cry HD Collection"}]},{"set_id":"devil-may-cry-hd_3","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/Devil%20May%20Cry%20HD%20Collection/details","description":"Devil May Cry HD Collection","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/df84c5bf-8d66-48e2-b9fb-c014cc9b3945/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/df84c5bf-8d66-48e2-b9fb-c014cc9b3945/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/df84c5bf-8d66-48e2-b9fb-c014cc9b3945/3","title":"Devil May Cry HD Collection"}]},{"set_id":"devil-may-cry-hd_4","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/Devil%20May%20Cry%20HD%20Collection/details","description":"Devil May Cry HD Collection","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/af836b94-8ffd-4c0a-b7d8-a92fad5e3015/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/af836b94-8ffd-4c0a-b7d8-a92fad5e3015/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/af836b94-8ffd-4c0a-b7d8-a92fad5e3015/3","title":"Devil May Cry HD Collection"}]},{"set_id":"devilian_1","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/Devilian/details","description":"Devilian","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/3cb92b57-1eef-451c-ac23-4d748128b2c5/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/3cb92b57-1eef-451c-ac23-4d748128b2c5/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/3cb92b57-1eef-451c-ac23-4d748128b2c5/3","title":"Devilian"}]},{"set_id":"duelyst_1","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/Duelyst/details","description":"Duelyst","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/7d9c12f4-a2ac-4e88-8026-d1a330468282/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/7d9c12f4-a2ac-4e88-8026-d1a330468282/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/7d9c12f4-a2ac-4e88-8026-d1a330468282/3","title":"Duelyst"}]},{"set_id":"duelyst_2","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/Duelyst/details","description":"Duelyst","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/1938acd3-2d18-471d-b1af-44f2047c033c/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/1938acd3-2d18-471d-b1af-44f2047c033c/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/1938acd3-2d18-471d-b1af-44f2047c033c/3","title":"Duelyst"}]},{"set_id":"duelyst_3","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/Duelyst/details","description":"Duelyst","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/344c07fc-1632-47c6-9785-e62562a6b760/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/344c07fc-1632-47c6-9785-e62562a6b760/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/344c07fc-1632-47c6-9785-e62562a6b760/3","title":"Duelyst"}]},{"set_id":"duelyst_4","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/Duelyst/details","description":"Duelyst","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/39e717a8-00bc-49cc-b6d4-3ea91ee1be25/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/39e717a8-00bc-49cc-b6d4-3ea91ee1be25/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/39e717a8-00bc-49cc-b6d4-3ea91ee1be25/3","title":"Duelyst"}]},{"set_id":"duelyst_5","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/Duelyst/details","description":"Duelyst","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/290419b6-484a-47da-ad14-a99d6581f758/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/290419b6-484a-47da-ad14-a99d6581f758/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/290419b6-484a-47da-ad14-a99d6581f758/3","title":"Duelyst"}]},{"set_id":"duelyst_6","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/Duelyst/details","description":"Duelyst","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/c5e54a4b-0bf1-463a-874a-38524579aed0/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/c5e54a4b-0bf1-463a-874a-38524579aed0/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/c5e54a4b-0bf1-463a-874a-38524579aed0/3","title":"Duelyst"}]},{"set_id":"duelyst_7","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/Duelyst/details","description":"Duelyst","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/cf508179-3183-4987-97e0-56ca44babb9f/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/cf508179-3183-4987-97e0-56ca44babb9f/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/cf508179-3183-4987-97e0-56ca44babb9f/3","title":"Duelyst"}]},{"set_id":"enter-the-gungeon_1","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/Enter%20the%20Gungeon/details","description":"Enter The Gungeon","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/53c9af0b-84f6-4f9d-8c80-4bc51321a37d/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/53c9af0b-84f6-4f9d-8c80-4bc51321a37d/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/53c9af0b-84f6-4f9d-8c80-4bc51321a37d/3","title":"Enter The Gungeon"}]},{"set_id":"eso_1","versions":[{"click_action":null,"click_url":null,"description":"Elder Scrolls Online","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/18647a68-a35f-48d7-bf97-ae5deb6b277d/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/18647a68-a35f-48d7-bf97-ae5deb6b277d/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/18647a68-a35f-48d7-bf97-ae5deb6b277d/3","title":"Elder Scrolls Online"}]},{"set_id":"extension","versions":[{"click_action":null,"click_url":null,"description":"Extension","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/ea8b0f8c-aa27-11e8-ba0c-1370ffff3854/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/ea8b0f8c-aa27-11e8-ba0c-1370ffff3854/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/ea8b0f8c-aa27-11e8-ba0c-1370ffff3854/3","title":"Extension"}]},{"set_id":"firewatch_1","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/Firewatch/details","description":"Firewatch","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/b6bf4889-4902-49e2-9658-c0132e71c9c4/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/b6bf4889-4902-49e2-9658-c0132e71c9c4/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/b6bf4889-4902-49e2-9658-c0132e71c9c4/3","title":"Firewatch"}]},{"set_id":"founder","versions":[{"click_action":"visit_url","click_url":"https://help.twitch.tv/s/article/founders-badge","description":"Founder","id":"0","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/511b78a9-ab37-472f-9569-457753bbe7d3/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/511b78a9-ab37-472f-9569-457753bbe7d3/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/511b78a9-ab37-472f-9569-457753bbe7d3/3","title":"Founder"}]},{"set_id":"frozen-cortext_1","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/Frozen%20Cortex/details","description":"Frozen Cortext","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/2015f087-01b5-4a01-a2bb-ecb4d6be5240/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/2015f087-01b5-4a01-a2bb-ecb4d6be5240/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/2015f087-01b5-4a01-a2bb-ecb4d6be5240/3","title":"Frozen Cortext"}]},{"set_id":"frozen-synapse_1","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/Frozen%20Synapse/details","description":"Frozen Synapse","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/d4bd464d-55ea-4238-a11d-744f034e2375/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/d4bd464d-55ea-4238-a11d-744f034e2375/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/d4bd464d-55ea-4238-a11d-744f034e2375/3","title":"Frozen Synapse"}]},{"set_id":"game-developer","versions":[{"click_action":null,"click_url":null,"description":"Game Developer for:","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/85856a4a-eb7d-4e26-a43e-d204a977ade4/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/85856a4a-eb7d-4e26-a43e-d204a977ade4/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/85856a4a-eb7d-4e26-a43e-d204a977ade4/3","title":"Game Developer"}]},{"set_id":"getting-over-it_1","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/Getting%20Over%20It/details","description":"Getting Over It","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/8d4e178c-81ec-4c71-af68-745b40733984/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/8d4e178c-81ec-4c71-af68-745b40733984/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/8d4e178c-81ec-4c71-af68-745b40733984/3","title":"Getting Over It"}]},{"set_id":"getting-over-it_2","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/Getting%20Over%20It/details","description":"Getting Over It","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/bb620b42-e0e1-4373-928e-d4a732f99ccb/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/bb620b42-e0e1-4373-928e-d4a732f99ccb/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/bb620b42-e0e1-4373-928e-d4a732f99ccb/3","title":"Getting Over It"}]},{"set_id":"glhf-pledge","versions":[{"click_action":"visit_url","click_url":"https://www.anykey.org/pledge","description":"Signed the GLHF pledge in support for inclusive gaming communities","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/3158e758-3cb4-43c5-94b3-7639810451c5/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/3158e758-3cb4-43c5-94b3-7639810451c5/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/3158e758-3cb4-43c5-94b3-7639810451c5/3","title":"GLHF Pledge"}]},{"set_id":"glitchcon2020","versions":[{"click_action":"visit_url","click_url":"https://www.twitchcon.com/","description":"Earned for Watching Glitchcon 2020","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/1d4b03b9-51ea-42c9-8f29-698e3c85be3d/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/1d4b03b9-51ea-42c9-8f29-698e3c85be3d/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/1d4b03b9-51ea-42c9-8f29-698e3c85be3d/3","title":"GlitchCon 2020"}]},{"set_id":"global_mod","versions":[{"click_action":null,"click_url":null,"description":"Global Moderator","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/9384c43e-4ce7-4e94-b2a1-b93656896eba/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/9384c43e-4ce7-4e94-b2a1-b93656896eba/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/9384c43e-4ce7-4e94-b2a1-b93656896eba/3","title":"Global Moderator"}]},{"set_id":"gold-pixel-heart","versions":[{"click_action":"visit_url","click_url":"https://help.twitch.tv/s/article/twitch-charity","description":"Thank you for donating via the Twitch Charity tool during Twitch Together for Good 2023!","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/1687873b-cf38-412c-aad3-f9a4ce17f8b6/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/1687873b-cf38-412c-aad3-f9a4ce17f8b6/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/1687873b-cf38-412c-aad3-f9a4ce17f8b6/3","title":"Gold Pixel Heart"}]},{"set_id":"heavy-bullets_1","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/Heavy%20Bullets/details","description":"Heavy Bullets","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/fc83b76b-f8b2-4519-9f61-6faf84eef4cd/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/fc83b76b-f8b2-4519-9f61-6faf84eef4cd/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/fc83b76b-f8b2-4519-9f61-6faf84eef4cd/3","title":"Heavy Bullets"}]},{"set_id":"hello_neighbor_1","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/Hello%20Neighbor/details","description":"Hello Neighbor","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/030cab2c-5d14-11e7-8d91-43a5a4306286/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/030cab2c-5d14-11e7-8d91-43a5a4306286/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/030cab2c-5d14-11e7-8d91-43a5a4306286/3","title":"Hello Neighbor"}]},{"set_id":"hype-train","versions":[{"click_action":"visit_url","click_url":"https://help.twitch.tv/s/article/hype-train-guide","description":"Top supporter during the most recent hype train","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/fae4086c-3190-44d4-83c8-8ef0cbe1a515/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/fae4086c-3190-44d4-83c8-8ef0cbe1a515/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/fae4086c-3190-44d4-83c8-8ef0cbe1a515/3","title":"Current Hype Train Conductor"},{"click_action":"visit_url","click_url":"https://help.twitch.tv/s/article/hype-train-guide","description":"Top supporter during prior hype trains","id":"2","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/9c8d038a-3a29-45ea-96d4-5031fb1a7a81/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/9c8d038a-3a29-45ea-96d4-5031fb1a7a81/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/9c8d038a-3a29-45ea-96d4-5031fb1a7a81/3","title":"Former Hype Train Conductor"}]},{"set_id":"innerspace_1","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/Innerspace/details","description":"Innerspace","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/97532ccd-6a07-42b5-aecf-3458b6b3ebea/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/97532ccd-6a07-42b5-aecf-3458b6b3ebea/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/97532ccd-6a07-42b5-aecf-3458b6b3ebea/3","title":"Innerspace"}]},{"set_id":"innerspace_2","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/Innerspace/details","description":"Innerspace","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/fc7d6018-657a-40e4-9246-0acdc85886d1/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/fc7d6018-657a-40e4-9246-0acdc85886d1/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/fc7d6018-657a-40e4-9246-0acdc85886d1/3","title":"Innerspace"}]},{"set_id":"jackbox-party-pack_1","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/The%20Jackbox%20Party%20Pack/details","description":"Jackbox Party Pack","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/0f964fc1-f439-485f-a3c0-905294ee70e8/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/0f964fc1-f439-485f-a3c0-905294ee70e8/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/0f964fc1-f439-485f-a3c0-905294ee70e8/3","title":"Jackbox Party Pack"}]},{"set_id":"kingdom-new-lands_1","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/Kingdom:%20New%20Lands/details","description":"Kingdom: New Lands","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/e3c2a67e-ef80-4fe3-ae41-b933cd11788a/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/e3c2a67e-ef80-4fe3-ae41-b933cd11788a/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/e3c2a67e-ef80-4fe3-ae41-b933cd11788a/3","title":"Kingdom: New Lands"}]},{"set_id":"moderator","versions":[{"click_action":null,"click_url":null,"description":"Moderator","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/3267646d-33f0-4b17-b3df-f923a41db1d0/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/3267646d-33f0-4b17-b3df-f923a41db1d0/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/3267646d-33f0-4b17-b3df-f923a41db1d0/3","title":"Moderator"}]},{"set_id":"moments","versions":[{"click_action":null,"click_url":null,"description":"Earned for being a part of at least 1 moment on a channel","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/bf370830-d79a-497b-81c6-a365b2b60dda/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/bf370830-d79a-497b-81c6-a365b2b60dda/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/bf370830-d79a-497b-81c6-a365b2b60dda/3","title":"Moments Badge - Tier 1"},{"click_action":null,"click_url":null,"description":"Earned for being a part of at least 75 moments on a channel","id":"10","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/9c13f2b6-69cd-4537-91b4-4a8bd8b6b1fd/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/9c13f2b6-69cd-4537-91b4-4a8bd8b6b1fd/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/9c13f2b6-69cd-4537-91b4-4a8bd8b6b1fd/3","title":"Moments Badge - Tier 10"},{"click_action":null,"click_url":null,"description":"Earned for being a part of at least 90 moments on a channel","id":"11","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/7573e7a2-0f1f-4508-b833-d822567a1e03/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/7573e7a2-0f1f-4508-b833-d822567a1e03/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/7573e7a2-0f1f-4508-b833-d822567a1e03/3","title":"Moments Badge - Tier 11"},{"click_action":null,"click_url":null,"description":"Earned for being a part of at least 105 moments on a channel","id":"12","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/f2c91d14-85c8-434b-a6c0-6d7930091150/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/f2c91d14-85c8-434b-a6c0-6d7930091150/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/f2c91d14-85c8-434b-a6c0-6d7930091150/3","title":"Moments Badge - Tier 12"},{"click_action":null,"click_url":null,"description":"Earned for being a part of at least 120 moments on a channel","id":"13","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/35eb3395-a1d3-4170-969a-86402ecfb11a/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/35eb3395-a1d3-4170-969a-86402ecfb11a/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/35eb3395-a1d3-4170-969a-86402ecfb11a/3","title":"Moments Badge - Tier 13"},{"click_action":null,"click_url":null,"description":"Earned for being a part of at least 140 moments on a channel","id":"14","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/cb40eb03-1015-45ba-8793-51c66a24a3d5/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/cb40eb03-1015-45ba-8793-51c66a24a3d5/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/cb40eb03-1015-45ba-8793-51c66a24a3d5/3","title":"Moments Badge - Tier 14"},{"click_action":null,"click_url":null,"description":"Earned for being a part of at least 160 moments on a channel","id":"15","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/b241d667-280b-4183-96ae-2d0053631186/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/b241d667-280b-4183-96ae-2d0053631186/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/b241d667-280b-4183-96ae-2d0053631186/3","title":"Moments Badge - Tier 15"},{"click_action":null,"click_url":null,"description":"Earned for being a part of at least 180 moments on a channel","id":"16","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/5684d1bc-8132-4a4f-850c-18d3c5bd04f3/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/5684d1bc-8132-4a4f-850c-18d3c5bd04f3/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/5684d1bc-8132-4a4f-850c-18d3c5bd04f3/3","title":"Moments Badge - Tier 16"},{"click_action":null,"click_url":null,"description":"Earned for being a part of at least 200 moments on a channel","id":"17","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/3b08c1ee-0f77-451b-9226-b5b22d7f023c/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/3b08c1ee-0f77-451b-9226-b5b22d7f023c/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/3b08c1ee-0f77-451b-9226-b5b22d7f023c/3","title":"Moments Badge - Tier 17"},{"click_action":null,"click_url":null,"description":"Earned for being a part of at least 225 moments on a channel","id":"18","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/14057e75-080c-42da-a412-6232c6f33b68/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/14057e75-080c-42da-a412-6232c6f33b68/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/14057e75-080c-42da-a412-6232c6f33b68/3","title":"Moments Badge - Tier 18"},{"click_action":null,"click_url":null,"description":"Earned for being a part of at least 250 moments on a channel","id":"19","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/6100cc6f-6b4b-4a3d-a55b-a5b34edb5ea1/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/6100cc6f-6b4b-4a3d-a55b-a5b34edb5ea1/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/6100cc6f-6b4b-4a3d-a55b-a5b34edb5ea1/3","title":"Moments Badge - Tier 19"},{"click_action":null,"click_url":null,"description":"Earned for being a part of at least 5 moments on a channel","id":"2","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/fc46b10c-5b45-43fd-81ad-d5cb0de6d2f4/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/fc46b10c-5b45-43fd-81ad-d5cb0de6d2f4/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/fc46b10c-5b45-43fd-81ad-d5cb0de6d2f4/3","title":"Moments Badge - Tier 2"},{"click_action":null,"click_url":null,"description":"Earned for being a part of at least 275 moments on a channel","id":"20","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/43399796-e74c-4741-a975-56202f0af30e/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/43399796-e74c-4741-a975-56202f0af30e/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/43399796-e74c-4741-a975-56202f0af30e/3","title":"Moments Badge - Tier 20"},{"click_action":null,"click_url":null,"description":"Earned for being a part of at least 10 moments on a channel","id":"3","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/d08658d7-205f-4f75-ad44-8c6e0acd8ef6/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/d08658d7-205f-4f75-ad44-8c6e0acd8ef6/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/d08658d7-205f-4f75-ad44-8c6e0acd8ef6/3","title":"Moments Badge - Tier 3"},{"click_action":null,"click_url":null,"description":"Earned for being a part of at least 15 moments on a channel","id":"4","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/fe5b5ddc-93e7-4aaf-9b3e-799cd41808b1/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/fe5b5ddc-93e7-4aaf-9b3e-799cd41808b1/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/fe5b5ddc-93e7-4aaf-9b3e-799cd41808b1/3","title":"Moments Badge - Tier 4"},{"click_action":null,"click_url":null,"description":"Earned for being a part of at least 20 moments on a channel","id":"5","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/c8a0d95a-856e-4097-9fc0-7765300a4f58/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/c8a0d95a-856e-4097-9fc0-7765300a4f58/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/c8a0d95a-856e-4097-9fc0-7765300a4f58/3","title":"Moments Badge - Tier 5"},{"click_action":null,"click_url":null,"description":"Earned for being a part of at least 30 moments on a channel","id":"6","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/f9e3b4e4-200e-4045-bd71-3a6b480c23ae/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/f9e3b4e4-200e-4045-bd71-3a6b480c23ae/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/f9e3b4e4-200e-4045-bd71-3a6b480c23ae/3","title":"Moments Badge - Tier 6"},{"click_action":null,"click_url":null,"description":"Earned for being a part of at least 40 moments on a channel","id":"7","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/a90a26a4-fdf7-4ac3-a782-76a413da16c1/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/a90a26a4-fdf7-4ac3-a782-76a413da16c1/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/a90a26a4-fdf7-4ac3-a782-76a413da16c1/3","title":"Moments Badge - Tier 7"},{"click_action":null,"click_url":null,"description":"Earned for being a part of at least 50 moments on a channel","id":"8","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/f22286cd-6aa3-42ce-b3fb-10f5d18c4aa0/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/f22286cd-6aa3-42ce-b3fb-10f5d18c4aa0/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/f22286cd-6aa3-42ce-b3fb-10f5d18c4aa0/3","title":"Moments Badge - Tier 8"},{"click_action":null,"click_url":null,"description":"Earned for being a part of at least 60 moments on a channel","id":"9","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/5cb2e584-1efd-469b-ab1d-4d1b59a944e7/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/5cb2e584-1efd-469b-ab1d-4d1b59a944e7/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/5cb2e584-1efd-469b-ab1d-4d1b59a944e7/3","title":"Moments Badge - Tier 9"}]},{"set_id":"no_audio","versions":[{"click_action":null,"click_url":null,"description":"Individuals with unreliable or no sound can select this badge","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/aef2cd08-f29b-45a1-8c12-d44d7fd5e6f0/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/aef2cd08-f29b-45a1-8c12-d44d7fd5e6f0/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/aef2cd08-f29b-45a1-8c12-d44d7fd5e6f0/3","title":"Watching without audio"}]},{"set_id":"no_video","versions":[{"click_action":null,"click_url":null,"description":"Individuals with unreliable or no video can select this badge","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/199a0dba-58f3-494e-a7fc-1fa0a1001fb8/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/199a0dba-58f3-494e-a7fc-1fa0a1001fb8/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/199a0dba-58f3-494e-a7fc-1fa0a1001fb8/3","title":"Listening only"}]},{"set_id":"okhlos_1","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/Okhlos/details","description":"Okhlos","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/dc088bd6-8965-4907-a1a2-c0ba83874a7d/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/dc088bd6-8965-4907-a1a2-c0ba83874a7d/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/dc088bd6-8965-4907-a1a2-c0ba83874a7d/3","title":"Okhlos"}]},{"set_id":"overwatch-league-insider_1","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/overwatchleague","description":"OWL All-Access Pass 2018","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/51e9e0aa-12e3-48ce-b961-421af0787dad/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/51e9e0aa-12e3-48ce-b961-421af0787dad/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/51e9e0aa-12e3-48ce-b961-421af0787dad/3","title":"OWL All-Access Pass 2018"}]},{"set_id":"overwatch-league-insider_2018B","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/overwatchleague","description":"OWL All-Access Pass 2018","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/34ec1979-d9bb-4706-ad15-464de814a79d/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/34ec1979-d9bb-4706-ad15-464de814a79d/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/34ec1979-d9bb-4706-ad15-464de814a79d/3","title":"OWL All-Access Pass 2018"}]},{"set_id":"overwatch-league-insider_2019A","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/overwatchleague","description":"OWL All-Access Pass 2019","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/ca980da1-3639-48a6-95a3-a03b002eb0e5/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/ca980da1-3639-48a6-95a3-a03b002eb0e5/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/ca980da1-3639-48a6-95a3-a03b002eb0e5/3","title":"OWL All-Access Pass 2019"},{"click_action":"visit_url","click_url":"https://www.twitch.tv/overwatchleague","description":"OWL All-Access Pass 2019","id":"2","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/ab7fa7a7-c2d9-403f-9f33-215b29b43ce4/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/ab7fa7a7-c2d9-403f-9f33-215b29b43ce4/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/ab7fa7a7-c2d9-403f-9f33-215b29b43ce4/3","title":"OWL All-Access Pass 2019"}]},{"set_id":"overwatch-league-insider_2019B","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/overwatchleague","description":"OWL All-Access Pass 2019","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/c5860811-d714-4413-9433-d6b1c9fc803c/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/c5860811-d714-4413-9433-d6b1c9fc803c/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/c5860811-d714-4413-9433-d6b1c9fc803c/3","title":"OWL All-Access Pass 2019"},{"click_action":"visit_url","click_url":"https://www.twitch.tv/overwatchleague","description":"OWL All-Access Pass 2019","id":"2","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/75f05d4b-3042-415c-8b0b-e87620a24daf/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/75f05d4b-3042-415c-8b0b-e87620a24daf/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/75f05d4b-3042-415c-8b0b-e87620a24daf/3","title":"OWL All-Access Pass 2019"},{"click_action":"visit_url","click_url":"https://www.twitch.tv/overwatchleague","description":"OWL All-Access Pass 2019","id":"3","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/765a0dcf-2a94-43ff-9b9c-ef6c209b90cd/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/765a0dcf-2a94-43ff-9b9c-ef6c209b90cd/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/765a0dcf-2a94-43ff-9b9c-ef6c209b90cd/3","title":"OWL All-Access Pass 2019"},{"click_action":"visit_url","click_url":"https://www.twitch.tv/overwatchleague","description":"OWL All-Access Pass 2019","id":"4","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/a8ae0ccd-783d-460d-93ee-57c485c558a6/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/a8ae0ccd-783d-460d-93ee-57c485c558a6/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/a8ae0ccd-783d-460d-93ee-57c485c558a6/3","title":"OWL All-Access Pass 2019"},{"click_action":"visit_url","click_url":"https://www.twitch.tv/overwatchleague","description":"OWL All-Access Pass 2019","id":"5","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/be87fd6d-1560-4e33-9ba4-2401b58d901f/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/be87fd6d-1560-4e33-9ba4-2401b58d901f/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/be87fd6d-1560-4e33-9ba4-2401b58d901f/3","title":"OWL All-Access Pass 2019"}]},{"set_id":"partner","versions":[{"click_action":"visit_url","click_url":"https://blog.twitch.tv/2017/04/24/the-verified-badge-is-here-13381bc05735","description":"Verified","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/d12a2e27-16f6-41d0-ab77-b780518f00a3/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/d12a2e27-16f6-41d0-ab77-b780518f00a3/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/d12a2e27-16f6-41d0-ab77-b780518f00a3/3","title":"Verified"}]},{"set_id":"power-rangers","versions":[{"click_action":null,"click_url":null,"description":"Black Ranger","id":"0","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/9edf3e7f-62e4-40f5-86ab-7a646b10f1f0/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/9edf3e7f-62e4-40f5-86ab-7a646b10f1f0/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/9edf3e7f-62e4-40f5-86ab-7a646b10f1f0/3","title":"Black Ranger"},{"click_action":null,"click_url":null,"description":"Blue Ranger","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/1eeae8fe-5bc6-44ed-9c88-fb84d5e0df52/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/1eeae8fe-5bc6-44ed-9c88-fb84d5e0df52/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/1eeae8fe-5bc6-44ed-9c88-fb84d5e0df52/3","title":"Blue Ranger"},{"click_action":null,"click_url":null,"description":"Green Ranger","id":"2","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/21bbcd6d-1751-4d28-a0c3-0b72453dd823/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/21bbcd6d-1751-4d28-a0c3-0b72453dd823/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/21bbcd6d-1751-4d28-a0c3-0b72453dd823/3","title":"Green Ranger"},{"click_action":null,"click_url":null,"description":"Pink Ranger","id":"3","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/5c58cb40-9028-4d16-af67-5bc0c18b745e/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/5c58cb40-9028-4d16-af67-5bc0c18b745e/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/5c58cb40-9028-4d16-af67-5bc0c18b745e/3","title":"Pink Ranger"},{"click_action":null,"click_url":null,"description":"Red Ranger","id":"4","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/8843d2de-049f-47d5-9794-b6517903db61/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/8843d2de-049f-47d5-9794-b6517903db61/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/8843d2de-049f-47d5-9794-b6517903db61/3","title":"Red Ranger"},{"click_action":null,"click_url":null,"description":"White Ranger","id":"5","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/06c85e34-477e-4939-9537-fd9978976042/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/06c85e34-477e-4939-9537-fd9978976042/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/06c85e34-477e-4939-9537-fd9978976042/3","title":"White Ranger"},{"click_action":null,"click_url":null,"description":"Yellow Ranger","id":"6","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/d6dca630-1ca4-48de-94e7-55ed0a24d8d1/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/d6dca630-1ca4-48de-94e7-55ed0a24d8d1/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/d6dca630-1ca4-48de-94e7-55ed0a24d8d1/3","title":"Yellow Ranger"}]},{"set_id":"predictions","versions":[{"click_action":null,"click_url":null,"description":"Predicted Outcome One","id":"blue-1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/e33d8b46-f63b-4e67-996d-4a7dcec0ad33/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/e33d8b46-f63b-4e67-996d-4a7dcec0ad33/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/e33d8b46-f63b-4e67-996d-4a7dcec0ad33/3","title":"Predicted Blue (1)"},{"click_action":null,"click_url":null,"description":"Predicted Outcome Ten","id":"blue-10","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/072ae906-ecf7-44f1-ac69-a5b2261d8892/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/072ae906-ecf7-44f1-ac69-a5b2261d8892/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/072ae906-ecf7-44f1-ac69-a5b2261d8892/3","title":"Predicted Blue (10)"},{"click_action":null,"click_url":null,"description":"Predicted Outcome Two","id":"blue-2","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/ffdda3fe-8012-4db3-981e-7a131402b057/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/ffdda3fe-8012-4db3-981e-7a131402b057/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/ffdda3fe-8012-4db3-981e-7a131402b057/3","title":"Predicted Blue (2)"},{"click_action":null,"click_url":null,"description":"Predicted Outcome Three","id":"blue-3","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/f2ab9a19-8ef7-4f9f-bd5d-9cf4e603f845/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/f2ab9a19-8ef7-4f9f-bd5d-9cf4e603f845/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/f2ab9a19-8ef7-4f9f-bd5d-9cf4e603f845/3","title":"Predicted Blue (3)"},{"click_action":null,"click_url":null,"description":"Predicted Outcome Four","id":"blue-4","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/df95317d-9568-46de-a421-a8520edb9349/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/df95317d-9568-46de-a421-a8520edb9349/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/df95317d-9568-46de-a421-a8520edb9349/3","title":"Predicted Blue (4)"},{"click_action":null,"click_url":null,"description":"Predicted Outcome Five","id":"blue-5","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/88758be8-de09-479b-9383-e3bb6d9e6f06/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/88758be8-de09-479b-9383-e3bb6d9e6f06/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/88758be8-de09-479b-9383-e3bb6d9e6f06/3","title":"Predicted Blue (5)"},{"click_action":null,"click_url":null,"description":"Predicted Outcome Six","id":"blue-6","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/46b1537e-d8b0-4c0d-8fba-a652e57b9df0/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/46b1537e-d8b0-4c0d-8fba-a652e57b9df0/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/46b1537e-d8b0-4c0d-8fba-a652e57b9df0/3","title":"Predicted Blue (6)"},{"click_action":null,"click_url":null,"description":"Predicted Outcome Seven","id":"blue-7","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/07cd34b2-c6a1-45f5-8d8a-131e3c8b2279/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/07cd34b2-c6a1-45f5-8d8a-131e3c8b2279/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/07cd34b2-c6a1-45f5-8d8a-131e3c8b2279/3","title":"Predicted Blue (7)"},{"click_action":null,"click_url":null,"description":"Predicted Outcome Eight","id":"blue-8","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/4416dfd7-db97-44a0-98e7-40b4e250615e/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/4416dfd7-db97-44a0-98e7-40b4e250615e/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/4416dfd7-db97-44a0-98e7-40b4e250615e/3","title":"Predicted Blue (8)"},{"click_action":null,"click_url":null,"description":"Predicted Outcome Nine","id":"blue-9","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/fc74bd90-2b74-4f56-8e42-04d405e10fae/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/fc74bd90-2b74-4f56-8e42-04d405e10fae/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/fc74bd90-2b74-4f56-8e42-04d405e10fae/3","title":"Predicted Blue (9)"},{"click_action":null,"click_url":null,"description":"Predicted Gray (1)","id":"gray-1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/144f77a2-e324-4a6b-9c17-9304fa193a27/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/144f77a2-e324-4a6b-9c17-9304fa193a27/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/144f77a2-e324-4a6b-9c17-9304fa193a27/3","title":"Predicted Gray (1)"},{"click_action":null,"click_url":null,"description":"Predicted Gray (2)","id":"gray-2","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/097a4b14-b458-47eb-91b6-fe74d3dbb3f5/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/097a4b14-b458-47eb-91b6-fe74d3dbb3f5/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/097a4b14-b458-47eb-91b6-fe74d3dbb3f5/3","title":"Predicted Gray (2)"},{"click_action":null,"click_url":null,"description":"Predicted Outcome One","id":"pink-1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/75e27613-caf7-4585-98f1-cb7363a69a4a/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/75e27613-caf7-4585-98f1-cb7363a69a4a/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/75e27613-caf7-4585-98f1-cb7363a69a4a/3","title":"Predicted Pink (1)"},{"click_action":null,"click_url":null,"description":"Predicted Outcome Two","id":"pink-2","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/4b76d5f2-91cc-4400-adf2-908a1e6cfd1e/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/4b76d5f2-91cc-4400-adf2-908a1e6cfd1e/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/4b76d5f2-91cc-4400-adf2-908a1e6cfd1e/3","title":"Predicted Pink (2)"}]},{"set_id":"premium","versions":[{"click_action":"visit_url","click_url":"https://gaming.amazon.com","description":"Prime Gaming","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/bbbe0db0-a598-423e-86d0-f9fb98ca1933/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/bbbe0db0-a598-423e-86d0-f9fb98ca1933/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/bbbe0db0-a598-423e-86d0-f9fb98ca1933/3","title":"Prime Gaming"}]},{"set_id":"psychonauts_1","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/Psychonauts/details","description":"Psychonauts","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/a9811799-dce3-475f-8feb-3745ad12b7ea/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/a9811799-dce3-475f-8feb-3745ad12b7ea/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/a9811799-dce3-475f-8feb-3745ad12b7ea/3","title":"Psychonauts"}]},{"set_id":"raiden-v-directors-cut_1","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/Raiden%20V/details","description":"Raiden V","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/441b50ae-a2e3-11e7-8a3e-6bff0c840878/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/441b50ae-a2e3-11e7-8a3e-6bff0c840878/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/441b50ae-a2e3-11e7-8a3e-6bff0c840878/3","title":"Raiden V"}]},{"set_id":"rift_1","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/Rift/details","description":"RIFT","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/f939686b-2892-46a4-9f0d-5f582578173e/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/f939686b-2892-46a4-9f0d-5f582578173e/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/f939686b-2892-46a4-9f0d-5f582578173e/3","title":"RIFT"}]},{"set_id":"rplace-2023","versions":[{"click_action":"visit_url","click_url":"https://www.reddit.com/r/place/","description":"A very delicious badge earned by watching Reddit's r/place 2023 event on Twitch Rivals or other participating channels.","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/e33e0c67-c380-4241-828a-099c46e51c66/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/e33e0c67-c380-4241-828a-099c46e51c66/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/e33e0c67-c380-4241-828a-099c46e51c66/3","title":"r/place 2023 Cake"}]},{"set_id":"samusoffer_beta","versions":[{"click_action":"visit_url","click_url":"https://twitch.amazon.com/prime","description":"beta_title1","id":"0","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/aa960159-a7b8-417e-83c1-035e4bc2deb5/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/aa960159-a7b8-417e-83c1-035e4bc2deb5/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/aa960159-a7b8-417e-83c1-035e4bc2deb5/3","title":"beta_title1"}]},{"set_id":"staff","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/jobs?ref=chat_badge","description":"Twitch Staff","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/d97c37bd-a6f5-4c38-8f57-4e4bef88af34/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/d97c37bd-a6f5-4c38-8f57-4e4bef88af34/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/d97c37bd-a6f5-4c38-8f57-4e4bef88af34/3","title":"Staff"}]},{"set_id":"starbound_1","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/Starbound/details","description":"Starbound","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/e838e742-0025-4646-9772-18a87ba99358/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/e838e742-0025-4646-9772-18a87ba99358/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/e838e742-0025-4646-9772-18a87ba99358/3","title":"Starbound"}]},{"set_id":"strafe_1","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/strafe/details","description":"Strafe","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/0051508d-2d42-4e4b-a328-c86b04510ca4/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/0051508d-2d42-4e4b-a328-c86b04510ca4/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/0051508d-2d42-4e4b-a328-c86b04510ca4/3","title":"Strafe"}]},{"set_id":"sub-gift-leader","versions":[{"click_action":null,"click_url":null,"description":"Ranked as a top subscription gifter in this community","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/21656088-7da2-4467-acd2-55220e1f45ad/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/21656088-7da2-4467-acd2-55220e1f45ad/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/21656088-7da2-4467-acd2-55220e1f45ad/3","title":"Gifter Leader 1"},{"click_action":null,"click_url":null,"description":"Ranked as a top subscription gifter in this community","id":"2","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/0d9fe96b-97b7-4215-b5f3-5328ebad271c/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/0d9fe96b-97b7-4215-b5f3-5328ebad271c/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/0d9fe96b-97b7-4215-b5f3-5328ebad271c/3","title":"Gifter Leader 2"},{"click_action":null,"click_url":null,"description":"Ranked as a top subscription gifter in this community","id":"3","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/4c6e4497-eed9-4dd3-ac64-e0599d0a63e5/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/4c6e4497-eed9-4dd3-ac64-e0599d0a63e5/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/4c6e4497-eed9-4dd3-ac64-e0599d0a63e5/3","title":"Gifter Leader 3"}]},{"set_id":"sub-gifter","versions":[{"click_action":null,"click_url":null,"description":"Has gifted a subscription to another viewer in this community","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/a5ef6c17-2e5b-4d8f-9b80-2779fd722414/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/a5ef6c17-2e5b-4d8f-9b80-2779fd722414/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/a5ef6c17-2e5b-4d8f-9b80-2779fd722414/3","title":"Sub Gifter"},{"click_action":null,"click_url":null,"description":"Has gifted a subscription to another viewer in this community","id":"10","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/d333288c-65d7-4c7b-b691-cdd7b3484bf8/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/d333288c-65d7-4c7b-b691-cdd7b3484bf8/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/d333288c-65d7-4c7b-b691-cdd7b3484bf8/3","title":"10 Gift Subs"},{"click_action":null,"click_url":null,"description":"Has gifted a subscription to another viewer in this community","id":"100","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/8343ada7-3451-434e-91c4-e82bdcf54460/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/8343ada7-3451-434e-91c4-e82bdcf54460/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/8343ada7-3451-434e-91c4-e82bdcf54460/3","title":"100 Gift Subs"},{"click_action":null,"click_url":null,"description":"Has gifted a subscription to another viewer in this community","id":"1000","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/bfb7399a-c632-42f7-8d5f-154610dede81/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/bfb7399a-c632-42f7-8d5f-154610dede81/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/bfb7399a-c632-42f7-8d5f-154610dede81/3","title":"1000 Gift Subs"},{"click_action":null,"click_url":null,"description":"Has gifted a subscription to another viewer in this community","id":"150","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/514845ba-0fc3-4771-bce1-14d57e91e621/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/514845ba-0fc3-4771-bce1-14d57e91e621/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/514845ba-0fc3-4771-bce1-14d57e91e621/3","title":"150 Gift Subs"},{"click_action":null,"click_url":null,"description":"Has gifted a subscription to another viewer in this community","id":"200","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/c6b1893e-8059-4024-b93c-39c84b601732/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/c6b1893e-8059-4024-b93c-39c84b601732/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/c6b1893e-8059-4024-b93c-39c84b601732/3","title":"200 Gift Subs"},{"click_action":null,"click_url":null,"description":"Has gifted a subscription to another viewer in this community","id":"2000","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/4e8b3a32-1513-44ad-8a12-6c90232c77f9/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/4e8b3a32-1513-44ad-8a12-6c90232c77f9/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/4e8b3a32-1513-44ad-8a12-6c90232c77f9/3","title":"2000 Gift Subs"},{"click_action":null,"click_url":null,"description":"Has gifted a subscription to another viewer in this community","id":"25","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/052a5d41-f1cc-455c-bc7b-fe841ffaf17f/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/052a5d41-f1cc-455c-bc7b-fe841ffaf17f/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/052a5d41-f1cc-455c-bc7b-fe841ffaf17f/3","title":"25 Gift Subs"},{"click_action":null,"click_url":null,"description":"Has gifted a subscription to another viewer in this community","id":"250","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/cd479dc0-4a15-407d-891f-9fd2740bddda/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/cd479dc0-4a15-407d-891f-9fd2740bddda/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/cd479dc0-4a15-407d-891f-9fd2740bddda/3","title":"250 Gift Subs"},{"click_action":null,"click_url":null,"description":"Has gifted a subscription to another viewer in this community","id":"300","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/9e1bb24f-d238-4078-871a-ac401b76ecf2/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/9e1bb24f-d238-4078-871a-ac401b76ecf2/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/9e1bb24f-d238-4078-871a-ac401b76ecf2/3","title":"300 Gift Subs"},{"click_action":null,"click_url":null,"description":"Has gifted a subscription to another viewer in this community","id":"3000","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/b18852ba-65d2-4b84-97d2-aeb6c44a0956/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/b18852ba-65d2-4b84-97d2-aeb6c44a0956/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/b18852ba-65d2-4b84-97d2-aeb6c44a0956/3","title":"3000 Gift Subs"},{"click_action":null,"click_url":null,"description":"Has gifted a subscription to another viewer in this community","id":"350","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/6c4783cd-0aba-4e75-a7a4-f48a70b665b0/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/6c4783cd-0aba-4e75-a7a4-f48a70b665b0/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/6c4783cd-0aba-4e75-a7a4-f48a70b665b0/3","title":"350 Gift Subs"},{"click_action":null,"click_url":null,"description":"Has gifted a subscription to another viewer in this community","id":"400","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/6f4cab6b-def9-4d99-ad06-90b0013b28c8/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/6f4cab6b-def9-4d99-ad06-90b0013b28c8/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/6f4cab6b-def9-4d99-ad06-90b0013b28c8/3","title":"400 Gift Subs"},{"click_action":null,"click_url":null,"description":"Has gifted a subscription to another viewer in this community","id":"4000","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/efbf3c93-ecfa-4b67-8d0a-1f732fb07397/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/efbf3c93-ecfa-4b67-8d0a-1f732fb07397/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/efbf3c93-ecfa-4b67-8d0a-1f732fb07397/3","title":"4000 Gift Subs"},{"click_action":null,"click_url":null,"description":"Has gifted a subscription to another viewer in this community","id":"450","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/b593d68a-f8fb-4516-a09a-18cce955402c/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/b593d68a-f8fb-4516-a09a-18cce955402c/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/b593d68a-f8fb-4516-a09a-18cce955402c/3","title":"450 Gift Subs"},{"click_action":null,"click_url":null,"description":"Has gifted a subscription to another viewer in this community","id":"5","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/ee113e59-c839-4472-969a-1e16d20f3962/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/ee113e59-c839-4472-969a-1e16d20f3962/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/ee113e59-c839-4472-969a-1e16d20f3962/3","title":"5 Gift Subs"},{"click_action":null,"click_url":null,"description":"Has gifted a subscription to another viewer in this community","id":"50","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/c4a29737-e8a5-4420-917a-314a447f083e/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/c4a29737-e8a5-4420-917a-314a447f083e/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/c4a29737-e8a5-4420-917a-314a447f083e/3","title":"50 Gift Subs"},{"click_action":null,"click_url":null,"description":"Has gifted a subscription to another viewer in this community","id":"500","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/60e9504c-8c3d-489f-8a74-314fb195ad8d/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/60e9504c-8c3d-489f-8a74-314fb195ad8d/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/60e9504c-8c3d-489f-8a74-314fb195ad8d/3","title":"500 Gift Subs"},{"click_action":null,"click_url":null,"description":"Has gifted a subscription to another viewer in this community","id":"5000","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/d775275d-fd19-4914-b63a-7928a22135c3/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/d775275d-fd19-4914-b63a-7928a22135c3/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/d775275d-fd19-4914-b63a-7928a22135c3/3","title":"5000 Gift Subs"},{"click_action":null,"click_url":null,"description":"Has gifted a subscription to another viewer in this community","id":"550","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/024d2563-1794-43ed-b8dc-33df3efae900/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/024d2563-1794-43ed-b8dc-33df3efae900/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/024d2563-1794-43ed-b8dc-33df3efae900/3","title":"550 Gift Subs"},{"click_action":null,"click_url":null,"description":"Has gifted a subscription to another viewer in this community","id":"600","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/3ecc3aab-09bf-4823-905e-3a4647171fc1/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/3ecc3aab-09bf-4823-905e-3a4647171fc1/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/3ecc3aab-09bf-4823-905e-3a4647171fc1/3","title":"600 Gift Subs"},{"click_action":null,"click_url":null,"description":"Has gifted a subscription to another viewer in this community","id":"650","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/eeabf43c-8e4c-448d-9790-4c2172c57944/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/eeabf43c-8e4c-448d-9790-4c2172c57944/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/eeabf43c-8e4c-448d-9790-4c2172c57944/3","title":"650 Gift Subs"},{"click_action":null,"click_url":null,"description":"Has gifted a subscription to another viewer in this community","id":"700","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/4a9acdc7-30be-4dd1-9898-fc9e42b3d304/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/4a9acdc7-30be-4dd1-9898-fc9e42b3d304/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/4a9acdc7-30be-4dd1-9898-fc9e42b3d304/3","title":"700 Gift Subs"},{"click_action":null,"click_url":null,"description":"Has gifted a subscription to another viewer in this community","id":"750","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/ca17277c-53e5-422b-8bb4-7c5dcdb0ac67/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/ca17277c-53e5-422b-8bb4-7c5dcdb0ac67/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/ca17277c-53e5-422b-8bb4-7c5dcdb0ac67/3","title":"750 Gift Subs"},{"click_action":null,"click_url":null,"description":"Has gifted a subscription to another viewer in this community","id":"800","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/9c1fb96d-0579-43d7-ba94-94672eaef63a/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/9c1fb96d-0579-43d7-ba94-94672eaef63a/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/9c1fb96d-0579-43d7-ba94-94672eaef63a/3","title":"800 Gift Subs"},{"click_action":null,"click_url":null,"description":"Has gifted a subscription to another viewer in this community","id":"850","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/cc924aaf-dfd4-4f3f-822a-f5a87eb24069/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/cc924aaf-dfd4-4f3f-822a-f5a87eb24069/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/cc924aaf-dfd4-4f3f-822a-f5a87eb24069/3","title":"850 Gift Subs"},{"click_action":null,"click_url":null,"description":"Has gifted a subscription to another viewer in this community","id":"900","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/193d86f6-83e1-428c-9638-d6ca9e408166/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/193d86f6-83e1-428c-9638-d6ca9e408166/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/193d86f6-83e1-428c-9638-d6ca9e408166/3","title":"900 Gift Subs"},{"click_action":null,"click_url":null,"description":"Has gifted a subscription to another viewer in this community","id":"950","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/7ce130bd-6f55-40cc-9231-e2a4cb712962/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/7ce130bd-6f55-40cc-9231-e2a4cb712962/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/7ce130bd-6f55-40cc-9231-e2a4cb712962/3","title":"950 Gift Subs"}]},{"set_id":"subscriber","versions":[{"click_action":"subscribe_to_channel","click_url":null,"description":"Subscriber","id":"0","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/5d9f2208-5dd8-11e7-8513-2ff4adfae661/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/5d9f2208-5dd8-11e7-8513-2ff4adfae661/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/5d9f2208-5dd8-11e7-8513-2ff4adfae661/3","title":"Subscriber"},{"click_action":"subscribe_to_channel","click_url":null,"description":"Subscriber","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/5d9f2208-5dd8-11e7-8513-2ff4adfae661/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/5d9f2208-5dd8-11e7-8513-2ff4adfae661/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/5d9f2208-5dd8-11e7-8513-2ff4adfae661/3","title":"Subscriber"},{"click_action":"subscribe_to_channel","click_url":null,"description":"2-Month Subscriber","id":"2","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/25a03e36-2bb2-4625-bd37-d6d9d406238d/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/25a03e36-2bb2-4625-bd37-d6d9d406238d/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/25a03e36-2bb2-4625-bd37-d6d9d406238d/3","title":"2-Month Subscriber"},{"click_action":"subscribe_to_channel","click_url":null,"description":"3-Month Subscriber","id":"3","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/e8984705-d091-4e54-8241-e53b30a84b0e/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/e8984705-d091-4e54-8241-e53b30a84b0e/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/e8984705-d091-4e54-8241-e53b30a84b0e/3","title":"3-Month Subscriber"},{"click_action":"subscribe_to_channel","click_url":null,"description":"6-Month Subscriber","id":"4","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/2d2485f6-d19b-4daa-8393-9493b019156b/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/2d2485f6-d19b-4daa-8393-9493b019156b/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/2d2485f6-d19b-4daa-8393-9493b019156b/3","title":"6-Month Subscriber"},{"click_action":"subscribe_to_channel","click_url":null,"description":"9-Month Subscriber","id":"5","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/b4e6b13a-a76f-4c56-87e1-9375a7aaa610/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/b4e6b13a-a76f-4c56-87e1-9375a7aaa610/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/b4e6b13a-a76f-4c56-87e1-9375a7aaa610/3","title":"9-Month Subscriber"},{"click_action":"subscribe_to_channel","click_url":null,"description":"1-Year Subscriber","id":"6","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/ed51a614-2c44-4a60-80b6-62908436b43a/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/ed51a614-2c44-4a60-80b6-62908436b43a/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/ed51a614-2c44-4a60-80b6-62908436b43a/3","title":"6-Month Subscriber"}]},{"set_id":"superhot_1","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/superhot/details","description":"Superhot","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/c5a06922-83b5-40cb-885f-bcffd3cd6c68/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/c5a06922-83b5-40cb-885f-bcffd3cd6c68/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/c5a06922-83b5-40cb-885f-bcffd3cd6c68/3","title":"Superhot"}]},{"set_id":"superultracombo-2023","versions":[{"click_action":null,"click_url":null,"description":"This user joined Twitch's SuperUltraCombo 2023","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/5864739a-5e58-4623-9450-a2c0555ef90b/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/5864739a-5e58-4623-9450-a2c0555ef90b/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/5864739a-5e58-4623-9450-a2c0555ef90b/3","title":"SuperUltraCombo 2023"}]},{"set_id":"the-game-awards-2023","versions":[{"click_action":"visit_url","click_url":"https://blog.twitch.tv/2023/11/30/the-2023-game-awards-is-live-on-twitch-december-7th/","description":"You’ve completed all categories of the 2023 Twitch Predicts: The Game Awards extension!","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/10cf46de-61e7-4a42-807a-7898408ce352/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/10cf46de-61e7-4a42-807a-7898408ce352/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/10cf46de-61e7-4a42-807a-7898408ce352/3","title":"The Game Awards 2023"}]},{"set_id":"the-golden-predictor-of-the-game-awards-2023","versions":[{"click_action":"visit_url","click_url":"https://blog.twitch.tv/2023/11/30/the-2023-game-awards-is-live-on-twitch-december-7th/","description":"You've predicted the entire 2023 Game Awards perfectly, here is a special gift for your work. Go ahead, show it off!","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/c84c4dd7-9318-4e8b-9f01-1612d3f83dae/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/c84c4dd7-9318-4e8b-9f01-1612d3f83dae/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/c84c4dd7-9318-4e8b-9f01-1612d3f83dae/3","title":"The Golden Predictor of the Game Awards 2023"}]},{"set_id":"the-surge_1","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/The%20Surge/details","description":"The Surge","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/c9f69d89-31c8-41aa-843b-fee956dfbe23/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/c9f69d89-31c8-41aa-843b-fee956dfbe23/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/c9f69d89-31c8-41aa-843b-fee956dfbe23/3","title":"The Surge"}]},{"set_id":"the-surge_2","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/The%20Surge/details","description":"The Surge","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/2c4d7e95-e138-4dde-a783-7956a8ecc408/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/2c4d7e95-e138-4dde-a783-7956a8ecc408/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/2c4d7e95-e138-4dde-a783-7956a8ecc408/3","title":"The Surge"}]},{"set_id":"the-surge_3","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/The%20Surge/details","description":"The Surge","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/0a8fc2d4-3125-4ccb-88db-e970dfbee189/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/0a8fc2d4-3125-4ccb-88db-e970dfbee189/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/0a8fc2d4-3125-4ccb-88db-e970dfbee189/3","title":"The Surge"}]},{"set_id":"this-war-of-mine_1","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/This%20War%20of%20Mine/details","description":"This War of Mine","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/6a20f814-cb2c-414e-89cc-f8dd483e1785/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/6a20f814-cb2c-414e-89cc-f8dd483e1785/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/6a20f814-cb2c-414e-89cc-f8dd483e1785/3","title":"This War of Mine"}]},{"set_id":"titan-souls_1","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/Titan%20Souls/details","description":"Titan Souls","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/092a7ce2-709c-434f-8df4-a6b075ef867d/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/092a7ce2-709c-434f-8df4-a6b075ef867d/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/092a7ce2-709c-434f-8df4-a6b075ef867d/3","title":"Titan Souls"}]},{"set_id":"treasure-adventure-world_1","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/Treasure%20Adventure%20World/details","description":"Treasure Adventure World","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/59810027-2988-4b0d-b88d-fc414c751305/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/59810027-2988-4b0d-b88d-fc414c751305/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/59810027-2988-4b0d-b88d-fc414c751305/3","title":"Treasure Adventure World"}]},{"set_id":"turbo","versions":[{"click_action":"turbo","click_url":null,"description":"A subscriber of Twitch's monthly premium user service","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/bd444ec6-8f34-4bf9-91f4-af1e3428d80f/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/bd444ec6-8f34-4bf9-91f4-af1e3428d80f/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/bd444ec6-8f34-4bf9-91f4-af1e3428d80f/3","title":"Turbo"}]},{"set_id":"twitch-intern-2023","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/jobs/early-career/","description":"This user was an intern at Twitch for the summer of 2023","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/e239e7e0-e373-4fdf-b95e-3469aec28485/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/e239e7e0-e373-4fdf-b95e-3469aec28485/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/e239e7e0-e373-4fdf-b95e-3469aec28485/3","title":"Twitch Intern 2023"}]},{"set_id":"twitch-recap-2023","versions":[{"click_action":"visit_url","click_url":"https://twitch-web.app.link/e/twitch-recap","description":"This user bled purple like it was their job, and was one of the most engaged members of Twitch in 2023!","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/4d9e9812-ba9b-48a6-8690-13f3f338ee65/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/4d9e9812-ba9b-48a6-8690-13f3f338ee65/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/4d9e9812-ba9b-48a6-8690-13f3f338ee65/3","title":"Twitch Recap 2023"}]},{"set_id":"twitchbot","versions":[{"click_action":"visit_url","click_url":"http://link.twitch.tv/automod_blog","description":"AutoMod","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/df9095f6-a8a0-4cc2-bb33-d908c0adffb8/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/df9095f6-a8a0-4cc2-bb33-d908c0adffb8/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/df9095f6-a8a0-4cc2-bb33-d908c0adffb8/3","title":"AutoMod"}]},{"set_id":"twitchcon2017","versions":[{"click_action":"visit_url","click_url":"https://www.twitchcon.com/","description":"Attended TwitchCon Long Beach 2017","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/0964bed0-5c31-11e7-a90b-0739918f1d9b/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/0964bed0-5c31-11e7-a90b-0739918f1d9b/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/0964bed0-5c31-11e7-a90b-0739918f1d9b/3","title":"TwitchCon 2017 - Long Beach"}]},{"set_id":"twitchcon2018","versions":[{"click_action":"visit_url","click_url":"https://www.twitchcon.com/?utm_source=twitch-chat&utm_medium=badge&utm_campaign=tc18","description":"Attended TwitchCon San Jose 2018","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/e68164e4-087d-4f62-81da-d3557efae3cb/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/e68164e4-087d-4f62-81da-d3557efae3cb/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/e68164e4-087d-4f62-81da-d3557efae3cb/3","title":"TwitchCon 2018 - San Jose"}]},{"set_id":"twitchconAmsterdam2020","versions":[{"click_action":"visit_url","click_url":"https://www.twitchcon.com/amsterdam/?utm_source=twitch-chat&utm_medium=badge&utm_campaign=tcamsterdam20","description":"Registered for TwitchCon Amsterdam 2020","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/ed917c9a-1a45-4340-9c64-ca8be4348c51/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/ed917c9a-1a45-4340-9c64-ca8be4348c51/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/ed917c9a-1a45-4340-9c64-ca8be4348c51/3","title":"TwitchCon 2020 - Amsterdam"}]},{"set_id":"twitchconEU2019","versions":[{"click_action":"visit_url","click_url":"https://europe.twitchcon.com/?utm_source=twitch-chat&utm_medium=badge&utm_campaign=tceu19","description":"Attended TwitchCon Berlin 2019","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/590eee9e-f04d-474c-90e7-b304d9e74b32/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/590eee9e-f04d-474c-90e7-b304d9e74b32/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/590eee9e-f04d-474c-90e7-b304d9e74b32/3","title":"TwitchCon 2019 - Berlin"}]},{"set_id":"twitchconEU2022","versions":[{"click_action":"visit_url","click_url":"https://www.twitchcon.com/amsterdam-2022/?utm_source=twitch-chat&utm_medium=badge&utm_campaign=tceu22","description":"Attended TwitchCon Amsterdam 2022","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/e4744003-50b7-4eb8-9b47-a7b1616a30c6/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/e4744003-50b7-4eb8-9b47-a7b1616a30c6/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/e4744003-50b7-4eb8-9b47-a7b1616a30c6/3","title":"TwitchCon 2022 - Amsterdam"}]},{"set_id":"twitchconEU2023","versions":[{"click_action":"visit_url","click_url":"https://www.twitchcon.com/paris-2023/?utm_source=chat_badge","description":"TwitchCon 2023 - Paris","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/a8f2084e-46b9-4bb9-ae5e-00d594aafc64/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/a8f2084e-46b9-4bb9-ae5e-00d594aafc64/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/a8f2084e-46b9-4bb9-ae5e-00d594aafc64/3","title":"TwitchCon 2023 - Paris"}]},{"set_id":"twitchconNA2019","versions":[{"click_action":"visit_url","click_url":"https://www.twitchcon.com/?utm_source=twitch-chat&utm_medium=badge&utm_campaign=tcna19","description":"Attended TwitchCon San Diego 2019","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/569c829d-c216-4f56-a191-3db257ed657c/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/569c829d-c216-4f56-a191-3db257ed657c/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/569c829d-c216-4f56-a191-3db257ed657c/3","title":"TwitchCon 2019 - San Diego"}]},{"set_id":"twitchconNA2020","versions":[{"click_action":"visit_url","click_url":"https://www.twitchcon.com/?utm_source=twitch-chat&utm_medium=badge&utm_campaign=tcna20","description":"Registered for TwitchCon North America 2020","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/ed917c9a-1a45-4340-9c64-ca8be4348c51/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/ed917c9a-1a45-4340-9c64-ca8be4348c51/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/ed917c9a-1a45-4340-9c64-ca8be4348c51/3","title":"TwitchCon 2020 - North America"}]},{"set_id":"twitchconNA2022","versions":[{"click_action":"visit_url","click_url":"https://www.twitchcon.com/san-diego-2022/?utm_source=twitch-chat&utm_medium=badge&utm_campaign=tcna22","description":"Attended TwitchCon San Diego 2022","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/344d429a-0b34-48e5-a84c-14a1b5772a3a/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/344d429a-0b34-48e5-a84c-14a1b5772a3a/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/344d429a-0b34-48e5-a84c-14a1b5772a3a/3","title":"TwitchCon 2022 - San Diego"}]},{"set_id":"twitchconNA2023","versions":[{"click_action":"visit_url","click_url":"https://www.twitchcon.com/en/las-vegas-2023/","description":"Attended TwitchCon Las Vegas 2023","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/c90a753f-ab20-41bc-9c42-ede062485d2c/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/c90a753f-ab20-41bc-9c42-ede062485d2c/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/c90a753f-ab20-41bc-9c42-ede062485d2c/3","title":"TwitchCon 2023 - Las Vegas"}]},{"set_id":"tyranny_1","versions":[{"click_action":"visit_url","click_url":"https://www.twitch.tv/directory/game/Tyranny/details","description":"Tyranny","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/0c79afdf-28ce-4b0b-9e25-4f221c30bfde/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/0c79afdf-28ce-4b0b-9e25-4f221c30bfde/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/0c79afdf-28ce-4b0b-9e25-4f221c30bfde/3","title":"Tyranny"}]},{"set_id":"user-anniversary","versions":[{"click_action":null,"click_url":null,"description":"Staff badge celebrating Twitch tenure","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/ccbbedaa-f4db-4d0b-9c2a-375de7ad947c/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/ccbbedaa-f4db-4d0b-9c2a-375de7ad947c/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/ccbbedaa-f4db-4d0b-9c2a-375de7ad947c/3","title":"Twitchiversary Badge"}]},{"set_id":"vga-champ-2017","versions":[{"click_action":"visit_url","click_url":"https://blog.twitch.tv/watch-and-co-stream-the-game-awards-this-thursday-on-twitch-3d8e34d2345d","description":"2017 VGA Champ","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/03dca92e-dc69-11e7-ac5b-9f942d292dc7/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/03dca92e-dc69-11e7-ac5b-9f942d292dc7/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/03dca92e-dc69-11e7-ac5b-9f942d292dc7/3","title":"2017 VGA Champ"}]},{"set_id":"vip","versions":[{"click_action":"visit_url","click_url":"https://help.twitch.tv/customer/en/portal/articles/659115-twitch-chat-badges-guide","description":"VIP","id":"1","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/b817aba4-fad8-49e2-b88a-7cc744dfa6ec/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/b817aba4-fad8-49e2-b88a-7cc744dfa6ec/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/b817aba4-fad8-49e2-b88a-7cc744dfa6ec/3","title":"VIP"}]},{"set_id":"warcraft","versions":[{"click_action":"visit_url","click_url":"http://warcraftontwitch.tv/","description":"For Lordaeron!","id":"alliance","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/c4816339-bad4-4645-ae69-d1ab2076a6b0/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/c4816339-bad4-4645-ae69-d1ab2076a6b0/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/c4816339-bad4-4645-ae69-d1ab2076a6b0/3","title":"Alliance"},{"click_action":"visit_url","click_url":"http://warcraftontwitch.tv/","description":"For the Horde!","id":"horde","image_url_1x":"https://static-cdn.jtvnw.net/badges/v1/de8b26b6-fd28-4e6c-bc89-3d597343800d/1","image_url_2x":"https://static-cdn.jtvnw.net/badges/v1/de8b26b6-fd28-4e6c-bc89-3d597343800d/2","image_url_4x":"https://static-cdn.jtvnw.net/badges/v1/de8b26b6-fd28-4e6c-bc89-3d597343800d/3","title":"Horde"}]}]} diff --git a/resources/twitch/admin.png b/resources/twitch/admin.png deleted file mode 100644 index a5dcb13b5..000000000 Binary files a/resources/twitch/admin.png and /dev/null differ diff --git a/resources/twitch/automod.png b/resources/twitch/automod.png index 32eabac97..e1f468b74 100644 Binary files a/resources/twitch/automod.png and b/resources/twitch/automod.png differ diff --git a/resources/twitch/broadcaster.png b/resources/twitch/broadcaster.png deleted file mode 100644 index ee1c18c73..000000000 Binary files a/resources/twitch/broadcaster.png and /dev/null differ diff --git a/resources/twitch/cheer1.png b/resources/twitch/cheer1.png deleted file mode 100644 index f1ed9fd29..000000000 Binary files a/resources/twitch/cheer1.png and /dev/null differ diff --git a/resources/twitch/globalmod.png b/resources/twitch/globalmod.png deleted file mode 100644 index 9353fc8a2..000000000 Binary files a/resources/twitch/globalmod.png and /dev/null differ diff --git a/resources/twitch/moderator.png b/resources/twitch/moderator.png deleted file mode 100644 index b418088d1..000000000 Binary files a/resources/twitch/moderator.png and /dev/null differ diff --git a/resources/twitch/prime.png b/resources/twitch/prime.png deleted file mode 100644 index 21e442bbf..000000000 Binary files a/resources/twitch/prime.png and /dev/null differ diff --git a/resources/twitch/staff.png b/resources/twitch/staff.png deleted file mode 100644 index d2d257428..000000000 Binary files a/resources/twitch/staff.png and /dev/null differ diff --git a/resources/twitch/subscriber.png b/resources/twitch/subscriber.png deleted file mode 100644 index 9054ef08a..000000000 Binary files a/resources/twitch/subscriber.png and /dev/null differ diff --git a/resources/twitch/turbo.png b/resources/twitch/turbo.png deleted file mode 100644 index 12bc1bdb3..000000000 Binary files a/resources/twitch/turbo.png and /dev/null differ diff --git a/resources/twitch/verified.png b/resources/twitch/verified.png deleted file mode 100644 index 0b7019471..000000000 Binary files a/resources/twitch/verified.png and /dev/null differ diff --git a/resources/twitch/vip.png b/resources/twitch/vip.png deleted file mode 100644 index 768e59493..000000000 Binary files a/resources/twitch/vip.png and /dev/null differ diff --git a/resources/windows.rc b/resources/windows.rc deleted file mode 100644 index 8f9d9ca03..000000000 --- a/resources/windows.rc +++ /dev/null @@ -1 +0,0 @@ -IDI_ICON1 ICON DISCARDABLE "icon.ico" diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 000000000..92192ef35 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,3 @@ +# scripts + +This directory contains scripts that may be useful for a contributor to run while working on Chatterino diff --git a/tools/build-docker-images.sh b/scripts/build-docker-images.sh similarity index 100% rename from tools/build-docker-images.sh rename to scripts/build-docker-images.sh diff --git a/scripts/check-clang-tidy.sh b/scripts/check-clang-tidy.sh new file mode 100755 index 000000000..62965189b --- /dev/null +++ b/scripts/check-clang-tidy.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +set -eu + +clang-tidy --version + +find \ + src/ \ + tests/src/ \ + benchmarks/src/ \ + mocks/include/ \ + -type f \( -name "*.hpp" -o -name "*.cpp" \) -print0 | parallel -0 -j16 -I {} clang-tidy --quiet "$@" "{}" diff --git a/tools/check-format.sh b/scripts/check-format.sh similarity index 78% rename from tools/check-format.sh rename to scripts/check-format.sh index e7722ed6f..1d786901c 100755 --- a/tools/check-format.sh +++ b/scripts/check-format.sh @@ -11,7 +11,7 @@ while read -r file; do echo "$file differs!!!!!!!" fail="1" fi -done < <(find src/ -type f \( -iname "*.hpp" -o -iname "*.cpp" \)) +done < <(find src/ tests/src benchmarks/src mocks/include -type f \( -iname "*.hpp" -o -iname "*.cpp" \)) if [ "$fail" = "1" ]; then echo "At least one file is poorly formatted - check the output above" diff --git a/tools/check-line-endings.sh b/scripts/check-line-endings.sh similarity index 100% rename from tools/check-line-endings.sh rename to scripts/check-line-endings.sh diff --git a/scripts/clang-format-all.sh b/scripts/clang-format-all.sh new file mode 100755 index 000000000..7e1e94eb5 --- /dev/null +++ b/scripts/clang-format-all.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +read -p "Are you sure you want to run clang-format on all source files? (y/n) " -n 1 -r +echo +if [[ $REPLY =~ ^[Yy]$ ]]; then + find src/ \( -iname "*.hpp" -o -iname "*.cpp" \) -exec clang-format -i {} \; + find tests/src/ \( -iname "*.hpp" -o -iname "*.cpp" \) -exec clang-format -i {} \; + find benchmarks/src/ \( -iname "*.hpp" -o -iname "*.cpp" \) -exec clang-format -i {} \; + find mocks/include/ \( -iname "*.hpp" -o -iname "*.cpp" \) -exec clang-format -i {} \; +fi diff --git a/tools/get-tlds-update.sh b/scripts/get-tlds-update.sh old mode 100644 new mode 100755 similarity index 100% rename from tools/get-tlds-update.sh rename to scripts/get-tlds-update.sh diff --git a/scripts/get-vcpkg-package-versions.sh b/scripts/get-vcpkg-package-versions.sh new file mode 100755 index 000000000..f726a6cbb --- /dev/null +++ b/scripts/get-vcpkg-package-versions.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +dependencies="$(jq -r -c '.dependencies[] | if type == "string" then . else .name end' vcpkg.json)" +dependencies+=" openssl" +baseline="$(jq -r -c '."builtin-baseline"' vcpkg.json)" + +for dependency_name in $dependencies; do + dependency_url="https://raw.githubusercontent.com/microsoft/vcpkg/${baseline}/ports/${dependency_name}/vcpkg.json" + dependency_version="$(curl -s "$dependency_url" | jq -rc '.version')" + echo "Dependency $dependency_name is at version '$dependency_version' in baseline $baseline" +done diff --git a/scripts/make_luals_meta.py b/scripts/make_luals_meta.py new file mode 100644 index 000000000..f9f44e7ed --- /dev/null +++ b/scripts/make_luals_meta.py @@ -0,0 +1,328 @@ +""" +This script generates docs/plugin-meta.lua. It accepts no arguments + +It assumes comments look like: +/** + * Thing + * + * @lua@param thing boolean + * @lua@returns boolean + * @exposed name + */ +- Do not have any useful info on '/**' and '*/' lines. +- Class members are not allowed to have non-@command lines and commands different from @lua@field + +Only entire comment blocks are used. One comment block can describe at most one +entity (function/class/enum). Blocks without commands are ignored. + +Valid commands are: +1. @exposeenum [dotted.name.in_lua.last_part] + Define a table with keys of the enum. Values behind those keys aren't + written on purpose. +2. @exposed [c2.name] + Generates a function definition line from the last `@lua@param`s. +3. @lua[@command] + Writes [@command] to the file as a comment, usually this is @class, @param, @return, ... + @lua@class and @lua@field have special treatment when it comes to generation of spacing new lines + +Non-command lines of comments are written with a space after '---' +""" + +from io import TextIOWrapper +from pathlib import Path +import re +from typing import Optional + +BOILERPLATE = """ +---@meta Chatterino2 + +-- This file is automatically generated from src/controllers/plugins/LuaAPI.hpp by the scripts/make_luals_meta.py script +-- This file is intended to be used with LuaLS (https://luals.github.io/). +-- Add the folder this file is in to "Lua.workspace.library". + +c2 = {} +""" + +repo_root = Path(__file__).parent.parent +lua_api_file = repo_root / "src" / "controllers" / "plugins" / "LuaAPI.hpp" +lua_meta = repo_root / "docs" / "plugin-meta.lua" + +print("Writing to", lua_meta.relative_to(repo_root)) + + +def strip_line(line: str): + return re.sub(r"^/\*\*|^\*|\*/$", "", line).strip() + + +def is_comment_start(line: str): + return line.startswith("/**") + + +def is_enum_class(line: str): + return line.startswith("enum class") + + +def is_class(line: str): + return line.startswith(("class", "struct")) + + +class Reader: + lines: list[str] + line_idx: int + + def __init__(self, lines: list[str]) -> None: + self.lines = lines + self.line_idx = 0 + + def line_no(self) -> int: + """Returns the current line number (starting from 1)""" + return self.line_idx + 1 + + def has_next(self) -> bool: + """Returns true if there are lines left to read""" + return self.line_idx < len(self.lines) + + def peek_line(self) -> Optional[str]: + """Reads the line the cursor is at""" + if self.has_next(): + return self.lines[self.line_idx].strip() + return None + + def next_line(self) -> Optional[str]: + """Consumes and returns one line""" + if self.has_next(): + self.line_idx += 1 + return self.lines[self.line_idx - 1].strip() + return None + + def next_doc_comment(self) -> Optional[list[str]]: + """Reads a documentation comment (/** ... */) and advances the cursor""" + lines = [] + # find the start + while (line := self.next_line()) is not None and not is_comment_start(line): + pass + if line is None: + return None + + stripped = strip_line(line) + if stripped: + lines.append(stripped) + + if stripped.endswith("*/"): + return lines if lines else None + + while (line := self.next_line()) is not None: + if line.startswith("*/"): + break + + stripped = strip_line(line) + if not stripped: + continue + + if stripped.startswith("@"): + lines.append(stripped) + continue + + if not lines: + lines.append(stripped) + else: + lines[-1] += "\n--- " + stripped + + return lines if lines else None + + def read_class_body(self) -> list[list[str]]: + """The reader must be at the first line of the class/struct body. All comments inside the class are returned.""" + items = [] + nesting = -1 # for the opening brace + while (line := self.peek_line()) is not None: + if line.startswith("};") and nesting == 0: + self.next_line() + break + if not is_comment_start(line): + nesting += line.count("{") - line.count("}") + self.next_line() + continue + doc = self.next_doc_comment() + if not doc: + break + items.append(doc) + return items + + def read_enum_variants(self) -> list[str]: + """The reader must be before an enum class definition (possibly with some comments before). It returns all variants.""" + items = [] + is_comment = False + while (line := self.peek_line()) is not None and not line.startswith("};"): + self.next_line() + if is_comment: + if line.endswith("*/"): + is_comment = False + continue + if line.startswith("/*"): + is_comment = True + continue + if line.startswith("//"): + continue + if line.endswith("};"): # oneline declaration + opener = line.find("{") + 1 + closer = line.find("}") + items = [ + line.split("=", 1)[0].strip() + for line in line[opener:closer].split(",") + ] + break + if line.startswith("enum class"): + continue + + items.append(line.rstrip(",")) + + return items + + +def finish_class(out, name): + out.write(f"{name} = {{}}\n") + + +def printmsg(path: Path, line: int, message: str): + print(f"{path.relative_to(repo_root)}:{line} {message}") + + +def panic(path: Path, line: int, message: str): + printmsg(path, line, message) + exit(1) + + +def write_func(path: Path, line: int, comments: list[str], out: TextIOWrapper): + if not comments[0].startswith("@"): + out.write(f"--- {comments[0]}\n---\n") + comments = comments[1:] + params = [] + for comment in comments[:-1]: + if not comment.startswith("@lua"): + panic(path, line, f"Invalid function specification - got '{comment}'") + if comment.startswith("@lua@param"): + params.append(comment.split(" ", 2)[1]) + + out.write(f"---{comment.removeprefix('@lua')}\n") + + if not comments[-1].startswith("@exposed "): + panic(path, line, f"Invalid function exposure - got '{comments[-1]}'") + name = comments[-1].split(" ", 1)[1] + printmsg(path, line, f"function {name}") + lua_params = ", ".join(params) + out.write(f"function {name}({lua_params}) end\n\n") + + +def read_file(path: Path, out: TextIOWrapper): + print("Reading", path.relative_to(repo_root)) + with path.open("r") as f: + lines = f.read().splitlines() + + reader = Reader(lines) + while reader.has_next(): + doc_comment = reader.next_doc_comment() + if not doc_comment: + break + header_comment = None + if not doc_comment[0].startswith("@"): + if len(doc_comment) == 1: + continue + header_comment = doc_comment[0] + header = doc_comment[1:] + else: + header = doc_comment + + # enum + if header[0].startswith("@exposeenum "): + if len(header) > 1: + panic( + path, + reader.line_no(), + f"Invalid enum exposure - one command expected, got {len(header)}", + ) + name = header[0].split(" ", 1)[1] + printmsg(path, reader.line_no(), f"enum {name}") + out.write(f"---@alias {name} integer\n") + if header_comment: + out.write(f"--- {header_comment}\n") + out.write("---@type { ") + out.write( + ", ".join( + [f"{variant}: {name}" for variant in reader.read_enum_variants()] + ) + ) + out.write(" }\n") + out.write(f"{name} = {{}}\n\n") + continue + + # class + elif header[0].startswith("@lua@class "): + name = header[0].split(" ", 1)[1] + classname = name.split(":")[0].strip() + printmsg(path, reader.line_no(), f"class {classname}") + + if header_comment: + out.write(f"--- {header_comment}\n") + out.write(f"---@class {name}\n") + # inline class + if len(header) > 1: + for field in header[1:]: + if not field.startswith("@lua@field "): + panic( + path, + reader.line_no(), + f"Invalid inline class exposure - all lines must be fields, got '{field}'", + ) + out.write(f"---{field.removeprefix('@lua')}\n") + out.write("\n") + continue + + # class definition + # save functions for later (print fields first) + funcs = [] + for comment in reader.read_class_body(): + if comment[-1].startswith("@exposed "): + funcs.append(comment) + continue + if len(comment) > 1 or not comment[0].startswith("@lua"): + continue + out.write(f"---{comment[0].removeprefix('@lua')}\n") + + if funcs: + # only define global if there are functions on the class + out.write(f"{classname} = {{}}\n\n") + else: + out.write("\n") + + for func in funcs: + write_func(path, reader.line_no(), func, out) + continue + # global function + elif header[-1].startswith("@exposed "): + write_func(path, reader.line_no(), doc_comment, out) + continue + else: + for comment in header: + inline_command(path, reader.line_no(), comment, out) + + +def inline_command(path: Path, line: int, comment: str, out: TextIOWrapper): + if comment.startswith("@includefile "): + filename = comment.split(" ", 1)[1] + out.write(f"-- Begin src/{filename}\n\n") + read_file(repo_root / "src" / filename, out) + out.write(f"-- End src/{filename}\n\n") + elif comment.startswith("@lua@class"): + panic( + path, + line, + "Unexpected @lua@class command. @lua@class must be placed at the start of the comment block!", + ) + elif comment.startswith("@lua@"): + out.write(f'---{comment.replace("@lua", "", 1)}\n') + + +if __name__ == "__main__": + with lua_meta.open("w") as output: + output.write(BOILERPLATE[1:]) # skip the newline after triple quote + read_file(lua_api_file, output) diff --git a/tools/update-emoji-data.sh b/scripts/update-emoji-data.sh similarity index 100% rename from tools/update-emoji-data.sh rename to scripts/update-emoji-data.sh diff --git a/tools/windows-fix-directory-case-sensitivity.sh b/scripts/windows-fix-directory-case-sensitivity.sh old mode 100644 new mode 100755 similarity index 100% rename from tools/windows-fix-directory-case-sensitivity.sh rename to scripts/windows-fix-directory-case-sensitivity.sh diff --git a/src/.clang-format b/src/.clang-format deleted file mode 100644 index f34c1465b..000000000 --- a/src/.clang-format +++ /dev/null @@ -1,35 +0,0 @@ -Language: Cpp - -AccessModifierOffset: -4 -AlignEscapedNewlinesLeft: true -AllowShortFunctionsOnASingleLine: false -AllowShortIfStatementsOnASingleLine: false -AllowShortLambdasOnASingleLine: Empty -AllowShortLoopsOnASingleLine: false -AlwaysBreakAfterDefinitionReturnType: false -AlwaysBreakBeforeMultilineStrings: false -BasedOnStyle: Google -BraceWrapping: { - AfterClass: 'true' - AfterControlStatement: 'true' - AfterFunction: 'true' - AfterNamespace: 'false' - BeforeCatch: 'true' - BeforeElse: 'true' -} -BreakBeforeBraces: Custom -BreakConstructorInitializersBeforeComma: true -ColumnLimit: 80 -ConstructorInitializerAllOnOneLineOrOnePerLine: false -DerivePointerBinding: false -FixNamespaceComments: true -IndentCaseLabels: true -IndentWidth: 4 -IndentWrappedFunctionNames: true -IndentPPDirectives: AfterHash -IncludeBlocks: Preserve -NamespaceIndentation: Inner -PointerBindsToType: false -SpacesBeforeTrailingComments: 2 -Standard: Auto -ReflowComments: false diff --git a/src/Application.cpp b/src/Application.cpp index a8963384c..735c226ea 100644 --- a/src/Application.cpp +++ b/src/Application.cpp @@ -1,99 +1,206 @@ #include "Application.hpp" -#include - #include "common/Args.hpp" +#include "common/Channel.hpp" #include "common/QLogging.hpp" #include "common/Version.hpp" #include "controllers/accounts/AccountController.hpp" +#include "controllers/commands/Command.hpp" #include "controllers/commands/CommandController.hpp" #include "controllers/highlights/HighlightController.hpp" #include "controllers/hotkeys/HotkeyController.hpp" #include "controllers/ignores/IgnoreController.hpp" #include "controllers/notifications/NotificationController.hpp" -#include "debug/AssertInGuiThread.hpp" -#include "messages/MessageBuilder.hpp" +#include "controllers/sound/ISoundController.hpp" #include "providers/bttv/BttvEmotes.hpp" +#include "providers/ffz/FfzEmotes.hpp" +#include "providers/links/LinkResolver.hpp" +#include "providers/seventv/SeventvAPI.hpp" +#include "providers/seventv/SeventvEmotes.hpp" +#include "providers/twitch/TwitchBadges.hpp" +#include "singletons/ImageUploader.hpp" +#ifdef CHATTERINO_HAVE_PLUGINS +# include "controllers/plugins/PluginController.hpp" +#endif +#include "controllers/sound/MiniaudioBackend.hpp" +#include "controllers/sound/NullBackend.hpp" +#include "controllers/twitch/LiveController.hpp" +#include "controllers/userdata/UserDataController.hpp" +#include "debug/AssertInGuiThread.hpp" +#include "messages/Message.hpp" +#include "messages/MessageBuilder.hpp" +#include "providers/bttv/BttvLiveUpdates.hpp" #include "providers/chatterino/ChatterinoBadges.hpp" #include "providers/ffz/FfzBadges.hpp" -#include "providers/ffz/FfzEmotes.hpp" -#include "providers/irc/Irc2.hpp" +#include "providers/seventv/eventapi/Dispatch.hpp" +#include "providers/seventv/eventapi/Subscription.hpp" +#include "providers/seventv/SeventvBadges.hpp" +#include "providers/seventv/SeventvEventAPI.hpp" +#include "providers/twitch/ChannelPointReward.hpp" +#include "providers/twitch/PubSubActions.hpp" #include "providers/twitch/PubSubManager.hpp" +#include "providers/twitch/PubSubMessages.hpp" +#include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchIrcServer.hpp" -#include "providers/twitch/TwitchMessageBuilder.hpp" +#include "singletons/CrashHandler.hpp" #include "singletons/Emotes.hpp" #include "singletons/Fonts.hpp" +#include "singletons/helper/LoggingChannel.hpp" #include "singletons/Logging.hpp" -#include "singletons/NativeMessaging.hpp" #include "singletons/Paths.hpp" -#include "singletons/Resources.hpp" #include "singletons/Settings.hpp" +#include "singletons/StreamerMode.hpp" #include "singletons/Theme.hpp" #include "singletons/Toasts.hpp" #include "singletons/Updates.hpp" #include "singletons/WindowManager.hpp" #include "util/Helpers.hpp" #include "util/PostToThread.hpp" -#include "util/RapidjsonHelpers.hpp" #include "widgets/Notebook.hpp" -#include "widgets/Window.hpp" #include "widgets/splits/Split.hpp" +#include "widgets/Window.hpp" +#include +#include #include +namespace { + +using namespace chatterino; + +const QString BTTV_LIVE_UPDATES_URL = "wss://sockets.betterttv.net/ws"; +const QString SEVENTV_EVENTAPI_URL = "wss://events.7tv.io/v3"; + +ISoundController *makeSoundController(Settings &settings) +{ + SoundBackend soundBackend = settings.soundBackend; + switch (soundBackend) + { + case SoundBackend::Miniaudio: { + return new MiniaudioBackend(); + } + break; + + case SoundBackend::Null: { + return new NullBackend(); + } + break; + + default: { + return new MiniaudioBackend(); + } + break; + } +} + +BttvLiveUpdates *makeBttvLiveUpdates(Settings &settings) +{ + bool enabled = + settings.enableBTTVLiveUpdates && settings.enableBTTVChannelEmotes; + + if (enabled) + { + return new BttvLiveUpdates(BTTV_LIVE_UPDATES_URL); + } + + return nullptr; +} + +SeventvEventAPI *makeSeventvEventAPI(Settings &settings) +{ + bool enabled = settings.enableSevenTVEventAPI; + + if (enabled) + { + return new SeventvEventAPI(SEVENTV_EVENTAPI_URL); + } + + return nullptr; +} + +const QString TWITCH_PUBSUB_URL = "wss://pubsub-edge.twitch.tv"; + +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +IApplication *INSTANCE = nullptr; + +} // namespace + namespace chatterino { -static std::atomic isAppInitialized{false}; - -Application *Application::instance = nullptr; -IApplication *IApplication::instance = nullptr; - IApplication::IApplication() { - IApplication::instance = this; + INSTANCE = this; +} + +IApplication::~IApplication() +{ + INSTANCE = nullptr; } // this class is responsible for handling the workflow of Chatterino // It will create the instances of the major classes, and connect their signals // to each other -Application::Application(Settings &_settings, Paths &_paths) - : themes(&this->emplace()) - , fonts(&this->emplace()) - , emotes(&this->emplace()) - , accounts(&this->emplace()) - , hotkeys(&this->emplace()) - , windows(&this->emplace()) - , toasts(&this->emplace()) +Application::Application(Settings &_settings, const Paths &paths, + const Args &_args, Updates &_updates) + : paths_(paths) + , args_(_args) + , themes(new Theme(paths)) + , fonts(new Fonts(_settings)) + , emotes(new Emotes) + , accounts(new AccountController) + , hotkeys(new HotkeyController) + , windows(new WindowManager(paths, _settings, *this->themes, *this->fonts)) + , toasts(new Toasts) + , imageUploader(new ImageUploader) + , seventvAPI(new SeventvAPI) + , crashHandler(new CrashHandler(paths)) - , commands(&this->emplace()) - , notifications(&this->emplace()) - , highlights(&this->emplace()) - , twitch(&this->emplace()) - , chatterinoBadges(&this->emplace()) - , ffzBadges(&this->emplace()) - , logging(&this->emplace()) + , commands(new CommandController(paths)) + , notifications(new NotificationController) + , highlights(new HighlightController(_settings, this->accounts.get())) + , twitch(new TwitchIrcServer) + , ffzBadges(new FfzBadges) + , seventvBadges(new SeventvBadges) + , userData(new UserDataController(paths)) + , sound(makeSoundController(_settings)) + , twitchLiveController(new TwitchLiveController) + , twitchPubSub(new PubSub(TWITCH_PUBSUB_URL)) + , twitchBadges(new TwitchBadges) + , chatterinoBadges(new ChatterinoBadges) + , bttvEmotes(new BttvEmotes) + , bttvLiveUpdates(makeBttvLiveUpdates(_settings)) + , ffzEmotes(new FfzEmotes) + , seventvEmotes(new SeventvEmotes) + , seventvEventAPI(makeSeventvEventAPI(_settings)) + , logging(new Logging(_settings)) + , linkResolver(new LinkResolver) + , streamerMode(new StreamerMode) +#ifdef CHATTERINO_HAVE_PLUGINS + , plugins(new PluginController(paths)) +#endif + , updates(_updates) { - this->instance = this; - - this->fonts->fontChanged.connect([this]() { - this->windows->layoutChannelViews(); - }); } -void Application::initialize(Settings &settings, Paths &paths) +Application::~Application() { - assert(isAppInitialized == false); - isAppInitialized = true; + // we do this early to ensure getApp isn't used in any dtors + INSTANCE = nullptr; +} + +void Application::initialize(Settings &settings, const Paths &paths) +{ + assert(!this->initialized); // Show changelog - if (!getArgs().isFramelessEmbed && + if (!this->args_.isFramelessEmbed && getSettings()->currentVersion.getValue() != "" && getSettings()->currentVersion.getValue() != CHATTERINO_VERSION) { - auto box = new QMessageBox(QMessageBox::Information, "Chatterino 2", - "Show changelog?", - QMessageBox::Yes | QMessageBox::No); + auto *box = new QMessageBox(QMessageBox::Information, "Chatterino 2", + "Show changelog?", + QMessageBox::Yes | QMessageBox::No); box->setAttribute(Qt::WA_DeleteOnClose); if (box->exec() == QMessageBox::Yes) { @@ -102,457 +209,472 @@ void Application::initialize(Settings &settings, Paths &paths) } } - if (!getArgs().isFramelessEmbed) + if (!this->args_.isFramelessEmbed) { getSettings()->currentVersion.setValue(CHATTERINO_VERSION); - - if (getSettings()->enableExperimentalIrc) - { - Irc::instance().load(); - } } - for (auto &singleton : this->singletons_) - { - singleton->initialize(settings, paths); - } + this->accounts->load(); - // add crash message - if (!getArgs().isFramelessEmbed && getArgs().crashRecovery) + this->windows->initialize(); + + this->ffzBadges->load(); + + // Load global emotes + this->bttvEmotes->loadEmotes(); + this->ffzEmotes->loadEmotes(); + this->seventvEmotes->loadGlobalEmotes(); + + this->twitch->initialize(); + + // Load live status + this->notifications->initialize(); + + // XXX: Loading Twitch badges after Helix has been initialized, which only happens after + // the AccountController initialize has been called + this->twitchBadges->loadTwitchBadges(); + +#ifdef CHATTERINO_HAVE_PLUGINS + this->plugins->initialize(settings); +#endif + + // Show crash message. + // On Windows, the crash message was already shown. +#ifndef Q_OS_WIN + if (!this->args_.isFramelessEmbed && this->args_.crashRecovery) { - if (auto selected = + if (auto *selected = this->windows->getMainWindow().getNotebook().getSelectedPage()) { - if (auto container = dynamic_cast(selected)) + if (auto *container = dynamic_cast(selected)) { for (auto &&split : container->getSplits()) { if (auto channel = split->getChannel(); !channel->isEmpty()) { - channel->addMessage(makeSystemMessage( + channel->addSystemMessage( "Chatterino unexpectedly crashed and restarted. " "You can disable automatic restarts in the " - "settings.")); + "settings."); } } } } } +#endif - this->windows->updateWordTypeMask(); - - if (!getArgs().isFramelessEmbed) + if (!this->args_.isFramelessEmbed) { this->initNm(paths); } - this->initPubSub(); + this->twitchPubSub->initialize(); + + this->initBttvLiveUpdates(); + this->initSeventvEventAPI(); + + this->streamerMode->start(); + + this->initialized = true; } -int Application::run(QApplication &qtApp) +int Application::run() { - assert(isAppInitialized); + assert(this->initialized); this->twitch->connect(); - if (!getArgs().isFramelessEmbed) + if (!this->args_.isFramelessEmbed) { this->windows->getMainWindow().show(); } - getSettings()->betaUpdates.connect( - [] { - Updates::instance().checkForUpdates(); + getSettings()->enableBTTVChannelEmotes.connect( + [this] { + this->twitch->reloadAllBTTVChannelEmotes(); + }, + false); + getSettings()->enableFFZChannelEmotes.connect( + [this] { + this->twitch->reloadAllFFZChannelEmotes(); + }, + false); + getSettings()->enableSevenTVChannelEmotes.connect( + [this] { + this->twitch->reloadAllSevenTVChannelEmotes(); }, false); - getSettings()->moderationActions.delayedItemsChanged.connect([this] { - this->windows->forceLayoutChannelViews(); - }); - getSettings()->highlightedMessages.delayedItemsChanged.connect([this] { - this->windows->forceLayoutChannelViews(); - }); - getSettings()->highlightedUsers.delayedItemsChanged.connect([this] { - this->windows->forceLayoutChannelViews(); - }); + return QApplication::exec(); +} - getSettings()->removeSpacesBetweenEmotes.connect([this] { - this->windows->forceLayoutChannelViews(); - }); +Theme *Application::getThemes() +{ + assertInGuiThread(); + assert(this->themes); - getSettings()->enableBTTVGlobalEmotes.connect([this] { - this->twitch->reloadBTTVGlobalEmotes(); - }); - getSettings()->enableBTTVChannelEmotes.connect([this] { - this->twitch->reloadAllBTTVChannelEmotes(); - }); - getSettings()->enableFFZGlobalEmotes.connect([this] { - this->twitch->reloadFFZGlobalEmotes(); - }); - getSettings()->enableFFZChannelEmotes.connect([this] { - this->twitch->reloadAllFFZChannelEmotes(); - }); + return this->themes.get(); +} - return qtApp.exec(); +Fonts *Application::getFonts() +{ + assertInGuiThread(); + assert(this->fonts); + + return this->fonts.get(); +} + +IEmotes *Application::getEmotes() +{ + assertInGuiThread(); + assert(this->emotes); + + return this->emotes.get(); +} + +AccountController *Application::getAccounts() +{ + assertInGuiThread(); + assert(this->accounts); + + return this->accounts.get(); +} + +HotkeyController *Application::getHotkeys() +{ + assertInGuiThread(); + assert(this->hotkeys); + + return this->hotkeys.get(); +} + +WindowManager *Application::getWindows() +{ + assertInGuiThread(); + assert(this->windows); + + return this->windows.get(); +} + +Toasts *Application::getToasts() +{ + assertInGuiThread(); + assert(this->toasts); + + return this->toasts.get(); +} + +CrashHandler *Application::getCrashHandler() +{ + assertInGuiThread(); + assert(this->crashHandler); + + return this->crashHandler.get(); +} + +CommandController *Application::getCommands() +{ + assertInGuiThread(); + assert(this->commands); + + return this->commands.get(); +} + +NotificationController *Application::getNotifications() +{ + assertInGuiThread(); + assert(this->notifications); + + return this->notifications.get(); +} + +HighlightController *Application::getHighlights() +{ + assertInGuiThread(); + assert(this->highlights); + + return this->highlights.get(); +} + +FfzBadges *Application::getFfzBadges() +{ + assertInGuiThread(); + assert(this->ffzBadges); + + return this->ffzBadges.get(); +} + +SeventvBadges *Application::getSeventvBadges() +{ + // SeventvBadges handles its own locks, so we don't need to assert that this is called in the GUI thread + assert(this->seventvBadges); + + return this->seventvBadges.get(); +} + +IUserDataController *Application::getUserData() +{ + assertInGuiThread(); + + return this->userData.get(); +} + +ISoundController *Application::getSound() +{ + assertInGuiThread(); + + return this->sound.get(); +} + +ITwitchLiveController *Application::getTwitchLiveController() +{ + assertInGuiThread(); + assert(this->twitchLiveController); + + return this->twitchLiveController.get(); +} + +TwitchBadges *Application::getTwitchBadges() +{ + assertInGuiThread(); + assert(this->twitchBadges); + + return this->twitchBadges.get(); +} + +IChatterinoBadges *Application::getChatterinoBadges() +{ + assertInGuiThread(); + assert(this->chatterinoBadges); + + return this->chatterinoBadges.get(); +} + +ImageUploader *Application::getImageUploader() +{ + assertInGuiThread(); + assert(this->imageUploader); + + return this->imageUploader.get(); +} + +SeventvAPI *Application::getSeventvAPI() +{ + assertInGuiThread(); + assert(this->seventvAPI); + + return this->seventvAPI.get(); +} + +#ifdef CHATTERINO_HAVE_PLUGINS +PluginController *Application::getPlugins() +{ + assertInGuiThread(); + assert(this->plugins); + + return this->plugins.get(); +} +#endif + +Updates &Application::getUpdates() +{ + assertInGuiThread(); + + return this->updates; +} + +ITwitchIrcServer *Application::getTwitch() +{ + assertInGuiThread(); + + return this->twitch.get(); +} + +PubSub *Application::getTwitchPubSub() +{ + assertInGuiThread(); + + return this->twitchPubSub.get(); +} + +ILogging *Application::getChatLogger() +{ + assertInGuiThread(); + + return this->logging.get(); +} + +ILinkResolver *Application::getLinkResolver() +{ + assertInGuiThread(); + + return this->linkResolver.get(); +} + +IStreamerMode *Application::getStreamerMode() +{ + return this->streamerMode.get(); +} + +BttvEmotes *Application::getBttvEmotes() +{ + assertInGuiThread(); + assert(this->bttvEmotes); + + return this->bttvEmotes.get(); +} + +BttvLiveUpdates *Application::getBttvLiveUpdates() +{ + assertInGuiThread(); + // bttvLiveUpdates may be nullptr if it's not enabled + + return this->bttvLiveUpdates.get(); +} + +FfzEmotes *Application::getFfzEmotes() +{ + assertInGuiThread(); + assert(this->ffzEmotes); + + return this->ffzEmotes.get(); +} + +SeventvEmotes *Application::getSeventvEmotes() +{ + assertInGuiThread(); + assert(this->seventvEmotes); + + return this->seventvEmotes.get(); +} + +SeventvEventAPI *Application::getSeventvEventAPI() +{ + assertInGuiThread(); + // seventvEventAPI may be nullptr if it's not enabled + + return this->seventvEventAPI.get(); } void Application::save() { - for (auto &singleton : this->singletons_) - { - singleton->save(); - } + this->commands->save(); + this->hotkeys->save(); + this->windows->save(); } -void Application::initNm(Paths &paths) +void Application::initNm(const Paths &paths) { (void)paths; #ifdef Q_OS_WIN -# if defined QT_NO_DEBUG || defined C_DEBUG_NM +# if defined QT_NO_DEBUG || defined CHATTERINO_DEBUG_NM registerNmHost(paths); this->nmServer.start(); # endif #endif } -void Application::initPubSub() +void Application::initBttvLiveUpdates() { - this->twitch->pubsub->signals_.moderation.chatCleared.connect( - [this](const auto &action) { - auto chan = this->twitch->getChannelOrEmptyByID(action.roomID); - if (chan->isEmpty()) - { - return; - } + if (!this->bttvLiveUpdates) + { + qCDebug(chatterinoBttv) + << "Skipping initialization of Live Updates as it's disabled"; + return; + } - QString text = - QString("%1 cleared the chat").arg(action.source.login); + // We can safely ignore these signal connections since the twitch object will always + // be destroyed before the Application + std::ignore = this->bttvLiveUpdates->signals_.emoteAdded.connect( + [&](const auto &data) { + auto chan = this->twitch->getChannelOrEmptyByID(data.channelID); - auto msg = makeSystemMessage(text); - postToThread([chan, msg] { - chan->addMessage(msg); - }); - }); - - this->twitch->pubsub->signals_.moderation.modeChanged.connect( - [this](const auto &action) { - auto chan = this->twitch->getChannelOrEmptyByID(action.roomID); - if (chan->isEmpty()) - { - return; - } - - QString text = - QString("%1 turned %2 %3 mode") - .arg(action.source.login) - .arg(action.state == ModeChangedAction::State::On ? "on" - : "off") - .arg(action.getModeName()); - - if (action.duration > 0) - { - text += QString(" (%1 seconds)").arg(action.duration); - } - - auto msg = makeSystemMessage(text); - postToThread([chan, msg] { - chan->addMessage(msg); - }); - }); - - this->twitch->pubsub->signals_.moderation.moderationStateChanged.connect( - [this](const auto &action) { - auto chan = this->twitch->getChannelOrEmptyByID(action.roomID); - if (chan->isEmpty()) - { - return; - } - - QString text; - - text = QString("%1 %2 %3") - .arg(action.source.login, - (action.modded ? "modded" : "unmodded"), - action.target.login); - - auto msg = makeSystemMessage(text); - postToThread([chan, msg] { - chan->addMessage(msg); - }); - }); - - this->twitch->pubsub->signals_.moderation.userBanned.connect( - [&](const auto &action) { - auto chan = this->twitch->getChannelOrEmptyByID(action.roomID); - - if (chan->isEmpty()) - { - return; - } - - postToThread([chan, action] { - MessageBuilder msg(action); - msg->flags.set(MessageFlag::PubSub); - chan->addOrReplaceTimeout(msg.release()); - }); - }); - this->twitch->pubsub->signals_.moderation.messageDeleted.connect( - [&](const auto &action) { - auto chan = this->twitch->getChannelOrEmptyByID(action.roomID); - - if (chan->isEmpty() || getSettings()->hideDeletionActions) - { - return; - } - - MessageBuilder msg; - TwitchMessageBuilder::deletionMessage(action, &msg); - msg->flags.set(MessageFlag::PubSub); - - postToThread([chan, msg = msg.release()] { - auto replaced = false; - LimitedQueueSnapshot snapshot = - chan->getMessageSnapshot(); - int snapshotLength = snapshot.size(); - - // without parens it doesn't build on windows - int end = (std::max)(0, snapshotLength - 200); - - for (int i = snapshotLength - 1; i >= end; --i) + postToThread([chan, data] { + if (auto *channel = dynamic_cast(chan.get())) { - auto &s = snapshot[i]; - if (!s->flags.has(MessageFlag::PubSub) && - s->timeoutUser == msg->timeoutUser) - { - chan->replaceMessage(s, msg); - replaced = true; - break; - } + channel->addBttvEmote(data); } - if (!replaced) + }); + }); + std::ignore = this->bttvLiveUpdates->signals_.emoteUpdated.connect( + [&](const auto &data) { + auto chan = this->twitch->getChannelOrEmptyByID(data.channelID); + + postToThread([chan, data] { + if (auto *channel = dynamic_cast(chan.get())) { - chan->addMessage(msg); + channel->updateBttvEmote(data); } }); }); + std::ignore = this->bttvLiveUpdates->signals_.emoteRemoved.connect( + [&](const auto &data) { + auto chan = this->twitch->getChannelOrEmptyByID(data.channelID); - this->twitch->pubsub->signals_.moderation.userUnbanned.connect( - [&](const auto &action) { - auto chan = this->twitch->getChannelOrEmptyByID(action.roomID); - - if (chan->isEmpty()) - { - return; - } - - auto msg = MessageBuilder(action).release(); - - postToThread([chan, msg] { - chan->addMessage(msg); - }); - }); - - this->twitch->pubsub->signals_.moderation.autoModMessageCaught.connect( - [&](const auto &msg, const QString &channelID) { - auto chan = this->twitch->getChannelOrEmptyByID(channelID); - if (chan->isEmpty()) - { - return; - } - - switch (msg.type) - { - case PubSubAutoModQueueMessage::Type::AutoModCaughtMessage: { - if (msg.status == "PENDING") - { - AutomodAction action(msg.data, channelID); - action.reason = QString("%1 level %2") - .arg(msg.contentCategory) - .arg(msg.contentLevel); - - action.msgID = msg.messageID; - action.message = msg.messageText; - - // this message also contains per-word automod data, which could be implemented - - // extract sender data manually because Twitch loves not being consistent - QString senderDisplayName = - msg.senderUserDisplayName; // Might be transformed later - bool hasLocalizedName = false; - if (!msg.senderUserDisplayName.isEmpty()) - { - // check for non-ascii display names - if (QString::compare(msg.senderUserDisplayName, - msg.senderUserLogin, - Qt::CaseInsensitive) != 0) - { - hasLocalizedName = true; - } - } - QColor senderColor = msg.senderUserChatColor; - QString senderColor_; - if (!senderColor.isValid() && - getSettings()->colorizeNicknames) - { - // color may be not present if user is a grey-name - senderColor = getRandomColor(msg.senderUserID); - } - - // handle username style based on prefered setting - switch (getSettings()->usernameDisplayMode.getValue()) - { - case UsernameDisplayMode::Username: { - if (hasLocalizedName) - { - senderDisplayName = msg.senderUserLogin; - } - break; - } - case UsernameDisplayMode::LocalizedName: { - break; - } - case UsernameDisplayMode:: - UsernameAndLocalizedName: { - if (hasLocalizedName) - { - senderDisplayName = QString("%1(%2)").arg( - msg.senderUserLogin, - msg.senderUserDisplayName); - } - break; - } - } - - action.target = - ActionUser{msg.senderUserID, msg.senderUserLogin, - senderDisplayName, senderColor}; - postToThread([chan, action] { - const auto p = makeAutomodMessage(action); - chan->addMessage(p.first); - chan->addMessage(p.second); - }); - } - // "ALLOWED" and "DENIED" statuses remain unimplemented - // They are versions of automod_message_(denied|approved) but for mods. - } - break; - - case PubSubAutoModQueueMessage::Type::INVALID: - default: { - } - break; - } - }); - - this->twitch->pubsub->signals_.moderation.autoModMessageBlocked.connect( - [&](const auto &action) { - auto chan = this->twitch->getChannelOrEmptyByID(action.roomID); - if (chan->isEmpty()) - { - return; - } - - postToThread([chan, action] { - const auto p = makeAutomodMessage(action); - chan->addMessage(p.first); - chan->addMessage(p.second); - }); - }); - - this->twitch->pubsub->signals_.moderation.automodUserMessage.connect( - [&](const auto &action) { - auto chan = this->twitch->getChannelOrEmptyByID(action.roomID); - - if (chan->isEmpty()) - { - return; - } - - auto msg = MessageBuilder(action).release(); - - postToThread([chan, msg] { - chan->addMessage(msg); - }); - chan->deleteMessage(msg->id); - }); - - this->twitch->pubsub->signals_.moderation.automodInfoMessage.connect( - [&](const auto &action) { - auto chan = this->twitch->getChannelOrEmptyByID(action.roomID); - - if (chan->isEmpty()) - { - return; - } - - postToThread([chan, action] { - const auto p = makeAutomodInfoMessage(action); - chan->addMessage(p); - }); - }); - - this->twitch->pubsub->signals_.pointReward.redeemed.connect( - [&](auto &data) { - QString channelId = data.value("channel_id").toString(); - if (channelId.isEmpty()) - { - qCDebug(chatterinoApp) - << "Couldn't find channel id of point reward"; - return; - } - - auto chan = this->twitch->getChannelOrEmptyByID(channelId); - - auto reward = ChannelPointReward(data); - - postToThread([chan, reward] { - if (auto channel = dynamic_cast(chan.get())) + postToThread([chan, data] { + if (auto *channel = dynamic_cast(chan.get())) { - channel->addChannelPointReward(reward); + channel->removeBttvEmote(data); } }); }); - - this->twitch->pubsub->start(); - - auto RequestModerationActions = [=]() { - this->twitch->pubsub->setAccount( - getApp()->accounts->twitch.getCurrent()); - // TODO(pajlada): Unlisten to all authed topics instead of only - // moderation topics this->twitch->pubsub->UnlistenAllAuthedTopics(); - - this->twitch->pubsub->listenToWhispers(); - }; - - this->accounts->twitch.currentUserChanged.connect( - [=] { - this->twitch->pubsub->unlistenAllModerationActions(); - this->twitch->pubsub->unlistenAutomod(); - this->twitch->pubsub->unlistenWhispers(); - }, - boost::signals2::at_front); - - this->accounts->twitch.currentUserChanged.connect(RequestModerationActions); - - RequestModerationActions(); + this->bttvLiveUpdates->start(); } -Application *getApp() +void Application::initSeventvEventAPI() { - assert(Application::instance != nullptr); + if (!this->seventvEventAPI) + { + qCDebug(chatterinoSeventvEventAPI) + << "Skipping initialization as the EventAPI is disabled"; + return; + } - assertInGuiThread(); + // We can safely ignore these signal connections since the twitch object will always + // be destroyed before the Application + std::ignore = this->seventvEventAPI->signals_.emoteAdded.connect( + [&](const auto &data) { + postToThread([this, data] { + this->twitch->forEachSeventvEmoteSet( + data.emoteSetID, [data](TwitchChannel &chan) { + chan.addSeventvEmote(data); + }); + }); + }); + std::ignore = this->seventvEventAPI->signals_.emoteUpdated.connect( + [&](const auto &data) { + postToThread([this, data] { + this->twitch->forEachSeventvEmoteSet( + data.emoteSetID, [data](TwitchChannel &chan) { + chan.updateSeventvEmote(data); + }); + }); + }); + std::ignore = this->seventvEventAPI->signals_.emoteRemoved.connect( + [&](const auto &data) { + postToThread([this, data] { + this->twitch->forEachSeventvEmoteSet( + data.emoteSetID, [data](TwitchChannel &chan) { + chan.removeSeventvEmote(data); + }); + }); + }); + std::ignore = this->seventvEventAPI->signals_.userUpdated.connect( + [&](const auto &data) { + this->twitch->forEachSeventvUser(data.userID, + [data](TwitchChannel &chan) { + chan.updateSeventvUser(data); + }); + }); - return Application::instance; + this->seventvEventAPI->start(); } -IApplication *getIApp() +IApplication *getApp() { - assert(IApplication::instance != nullptr); + assert(INSTANCE != nullptr); - assertInGuiThread(); - - return IApplication::instance; + return INSTANCE; } } // namespace chatterino diff --git a/src/Application.hpp b/src/Application.hpp index ee0d1417c..c44e1c63e 100644 --- a/src/Application.hpp +++ b/src/Application.hpp @@ -1,167 +1,232 @@ #pragma once -#include -#include - -#include "common/SignalVector.hpp" -#include "common/Singleton.hpp" #include "singletons/NativeMessaging.hpp" +#include +#include + namespace chatterino { +class Args; class TwitchIrcServer; +class ITwitchIrcServer; class PubSub; +class Updates; class CommandController; class AccountController; class NotificationController; class HighlightController; class HotkeyController; +class IUserDataController; +class UserDataController; +class ISoundController; +class SoundController; +class ITwitchLiveController; +class TwitchLiveController; +class TwitchBadges; +#ifdef CHATTERINO_HAVE_PLUGINS +class PluginController; +#endif class Theme; class WindowManager; +class ILogging; class Logging; class Paths; -class AccountManager; class Emotes; +class IEmotes; class Settings; class Fonts; class Toasts; +class IChatterinoBadges; class ChatterinoBadges; class FfzBadges; +class SeventvBadges; +class ImageUploader; +class SeventvAPI; +class CrashHandler; +class BttvEmotes; +class BttvLiveUpdates; +class FfzEmotes; +class SeventvEmotes; +class SeventvEventAPI; +class ILinkResolver; +class IStreamerMode; class IApplication { public: IApplication(); - virtual ~IApplication() = default; + virtual ~IApplication(); - static IApplication *instance; + IApplication(const IApplication &) = delete; + IApplication(IApplication &&) = delete; + IApplication &operator=(const IApplication &) = delete; + IApplication &operator=(IApplication &&) = delete; + virtual bool isTest() const = 0; + + virtual const Paths &getPaths() = 0; + virtual const Args &getArgs() = 0; virtual Theme *getThemes() = 0; virtual Fonts *getFonts() = 0; - virtual Emotes *getEmotes() = 0; + virtual IEmotes *getEmotes() = 0; virtual AccountController *getAccounts() = 0; virtual HotkeyController *getHotkeys() = 0; virtual WindowManager *getWindows() = 0; virtual Toasts *getToasts() = 0; + virtual CrashHandler *getCrashHandler() = 0; virtual CommandController *getCommands() = 0; virtual HighlightController *getHighlights() = 0; virtual NotificationController *getNotifications() = 0; - virtual TwitchIrcServer *getTwitch() = 0; - virtual ChatterinoBadges *getChatterinoBadges() = 0; + virtual ITwitchIrcServer *getTwitch() = 0; + virtual PubSub *getTwitchPubSub() = 0; + virtual ILogging *getChatLogger() = 0; + virtual IChatterinoBadges *getChatterinoBadges() = 0; virtual FfzBadges *getFfzBadges() = 0; + virtual SeventvBadges *getSeventvBadges() = 0; + virtual IUserDataController *getUserData() = 0; + virtual ISoundController *getSound() = 0; + virtual ITwitchLiveController *getTwitchLiveController() = 0; + virtual TwitchBadges *getTwitchBadges() = 0; + virtual ImageUploader *getImageUploader() = 0; + virtual SeventvAPI *getSeventvAPI() = 0; +#ifdef CHATTERINO_HAVE_PLUGINS + virtual PluginController *getPlugins() = 0; +#endif + virtual Updates &getUpdates() = 0; + virtual BttvEmotes *getBttvEmotes() = 0; + virtual BttvLiveUpdates *getBttvLiveUpdates() = 0; + virtual FfzEmotes *getFfzEmotes() = 0; + virtual SeventvEmotes *getSeventvEmotes() = 0; + virtual SeventvEventAPI *getSeventvEventAPI() = 0; + virtual ILinkResolver *getLinkResolver() = 0; + virtual IStreamerMode *getStreamerMode() = 0; }; class Application : public IApplication { - std::vector> singletons_; - int argc_; - char **argv_; + const Paths &paths_; + const Args &args_; + int argc_{}; + char **argv_{}; public: - static Application *instance; + Application(Settings &_settings, const Paths &paths, const Args &_args, + Updates &_updates); + ~Application() override; - Application(Settings &settings, Paths &paths); + Application(const Application &) = delete; + Application(Application &&) = delete; + Application &operator=(const Application &) = delete; + Application &operator=(Application &&) = delete; - void initialize(Settings &settings, Paths &paths); + bool isTest() const override + { + return false; + } + + void initialize(Settings &settings, const Paths &paths); void load(); void save(); - int run(QApplication &qtApp); + int run(); friend void test(); - Theme *const themes{}; - Fonts *const fonts{}; - Emotes *const emotes{}; - AccountController *const accounts{}; - HotkeyController *const hotkeys{}; - WindowManager *const windows{}; - Toasts *const toasts{}; +private: + std::unique_ptr themes; + std::unique_ptr fonts; + std::unique_ptr emotes; + std::unique_ptr accounts; + std::unique_ptr hotkeys; + std::unique_ptr windows; + std::unique_ptr toasts; + std::unique_ptr imageUploader; + std::unique_ptr seventvAPI; + std::unique_ptr crashHandler; + std::unique_ptr commands; + std::unique_ptr notifications; + std::unique_ptr highlights; + std::unique_ptr twitch; + std::unique_ptr ffzBadges; + std::unique_ptr seventvBadges; + std::unique_ptr userData; + std::unique_ptr sound; + std::unique_ptr twitchLiveController; + std::unique_ptr twitchPubSub; + std::unique_ptr twitchBadges; + std::unique_ptr chatterinoBadges; + std::unique_ptr bttvEmotes; + std::unique_ptr bttvLiveUpdates; + std::unique_ptr ffzEmotes; + std::unique_ptr seventvEmotes; + std::unique_ptr seventvEventAPI; + const std::unique_ptr logging; + std::unique_ptr linkResolver; + std::unique_ptr streamerMode; +#ifdef CHATTERINO_HAVE_PLUGINS + std::unique_ptr plugins; +#endif - CommandController *const commands{}; - NotificationController *const notifications{}; - HighlightController *const highlights{}; - TwitchIrcServer *const twitch{}; - ChatterinoBadges *const chatterinoBadges{}; - FfzBadges *const ffzBadges{}; +public: + const Paths &getPaths() override + { + return this->paths_; + } + const Args &getArgs() override + { + return this->args_; + } + Theme *getThemes() override; + Fonts *getFonts() override; + IEmotes *getEmotes() override; + AccountController *getAccounts() override; + HotkeyController *getHotkeys() override; + WindowManager *getWindows() override; + Toasts *getToasts() override; + CrashHandler *getCrashHandler() override; + CommandController *getCommands() override; + NotificationController *getNotifications() override; + HighlightController *getHighlights() override; + ITwitchIrcServer *getTwitch() override; + PubSub *getTwitchPubSub() override; + ILogging *getChatLogger() override; + FfzBadges *getFfzBadges() override; + SeventvBadges *getSeventvBadges() override; + IUserDataController *getUserData() override; + ISoundController *getSound() override; + ITwitchLiveController *getTwitchLiveController() override; + TwitchBadges *getTwitchBadges() override; + IChatterinoBadges *getChatterinoBadges() override; + ImageUploader *getImageUploader() override; + SeventvAPI *getSeventvAPI() override; +#ifdef CHATTERINO_HAVE_PLUGINS + PluginController *getPlugins() override; +#endif + Updates &getUpdates() override; - /*[[deprecated]]*/ Logging *const logging{}; + BttvEmotes *getBttvEmotes() override; + BttvLiveUpdates *getBttvLiveUpdates() override; + FfzEmotes *getFfzEmotes() override; + SeventvEmotes *getSeventvEmotes() override; + SeventvEventAPI *getSeventvEventAPI() override; - Theme *getThemes() override - { - return this->themes; - } - Fonts *getFonts() override - { - return this->fonts; - } - Emotes *getEmotes() override - { - return this->emotes; - } - AccountController *getAccounts() override - { - return this->accounts; - } - HotkeyController *getHotkeys() override - { - return this->hotkeys; - } - WindowManager *getWindows() override - { - return this->windows; - } - Toasts *getToasts() override - { - return this->toasts; - } - CommandController *getCommands() override - { - return this->commands; - } - NotificationController *getNotifications() override - { - return this->notifications; - } - HighlightController *getHighlights() override - { - return this->highlights; - } - TwitchIrcServer *getTwitch() override - { - return this->twitch; - } - ChatterinoBadges *getChatterinoBadges() override - { - return this->chatterinoBadges; - } - FfzBadges *getFfzBadges() override - { - return this->ffzBadges; - } + ILinkResolver *getLinkResolver() override; + IStreamerMode *getStreamerMode() override; private: - void addSingleton(Singleton *singleton); - void initPubSub(); - void initNm(Paths &paths); + void initBttvLiveUpdates(); + void initSeventvEventAPI(); + void initNm(const Paths &paths); - template ::value>> - T &emplace() - { - auto t = new T; - this->singletons_.push_back(std::unique_ptr(t)); - return *t; - } + NativeMessagingServer nmServer; + Updates &updates; - NativeMessagingServer nmServer{}; + bool initialized{false}; }; -Application *getApp(); - -// Get an interface version of the Application class - should be preferred when possible for new code -IApplication *getIApp(); +IApplication *getApp(); } // namespace chatterino diff --git a/src/BaseSettings.cpp b/src/BaseSettings.cpp deleted file mode 100644 index 141d66997..000000000 --- a/src/BaseSettings.cpp +++ /dev/null @@ -1,128 +0,0 @@ -#include "BaseSettings.hpp" - -#include - -#include "util/Clamp.hpp" - -namespace chatterino { - -std::vector> _settings; - -AB_SETTINGS_CLASS *AB_SETTINGS_CLASS::instance = nullptr; - -void _actuallyRegisterSetting( - std::weak_ptr setting) -{ - _settings.push_back(std::move(setting)); -} - -AB_SETTINGS_CLASS::AB_SETTINGS_CLASS(const QString &settingsDirectory) -{ - AB_SETTINGS_CLASS::instance = this; - - QString settingsPath = settingsDirectory + "/settings.json"; - - // get global instance of the settings library - auto settingsInstance = pajlada::Settings::SettingManager::getInstance(); - - settingsInstance->load(qPrintable(settingsPath)); - - settingsInstance->setBackupEnabled(true); - settingsInstance->setBackupSlots(9); - settingsInstance->saveMethod = - pajlada::Settings::SettingManager::SaveMethod::SaveOnExit; -} - -void AB_SETTINGS_CLASS::saveSnapshot() -{ - rapidjson::Document *d = new rapidjson::Document(rapidjson::kObjectType); - rapidjson::Document::AllocatorType &a = d->GetAllocator(); - - for (const auto &weakSetting : _settings) - { - auto setting = weakSetting.lock(); - if (!setting) - { - continue; - } - - rapidjson::Value key(setting->getPath().c_str(), a); - auto curVal = setting->unmarshalJSON(); - if (curVal == nullptr) - { - continue; - } - - rapidjson::Value val; - val.CopyFrom(*curVal, a); - d->AddMember(key.Move(), val.Move(), a); - } - - // log("Snapshot state: {}", rj::stringify(*d)); - - this->snapshot_.reset(d); -} - -void AB_SETTINGS_CLASS::restoreSnapshot() -{ - if (!this->snapshot_) - { - return; - } - - const auto &snapshot = *(this->snapshot_.get()); - - if (!snapshot.IsObject()) - { - return; - } - - for (const auto &weakSetting : _settings) - { - auto setting = weakSetting.lock(); - if (!setting) - { - continue; - } - - const char *path = setting->getPath().c_str(); - - if (!snapshot.HasMember(path)) - { - continue; - } - - setting->marshalJSON(snapshot[path]); - } -} - -float AB_SETTINGS_CLASS::getClampedUiScale() const -{ - return clamp(this->uiScale.getValue(), 0.2f, 10); -} - -void AB_SETTINGS_CLASS::setClampedUiScale(float value) -{ - this->uiScale.setValue(clamp(value, 0.2f, 10)); -} - -#ifndef AB_CUSTOM_SETTINGS -Settings *getSettings() -{ - static_assert(std::is_same_v, - "`AB_SETTINGS_CLASS` must be the same as `Settings`"); - - assert(AB_SETTINGS_CLASS::instance != nullptr); - - return AB_SETTINGS_CLASS::instance; -} -#endif - -AB_SETTINGS_CLASS *getABSettings() -{ - assert(AB_SETTINGS_CLASS::instance); - - return AB_SETTINGS_CLASS::instance; -} - -} // namespace chatterino diff --git a/src/BaseSettings.hpp b/src/BaseSettings.hpp deleted file mode 100644 index a747b67c6..000000000 --- a/src/BaseSettings.hpp +++ /dev/null @@ -1,53 +0,0 @@ -#ifndef AB_SETTINGS_H -#define AB_SETTINGS_H - -#include "common/ChatterinoSetting.hpp" - -#include -#include -#include - -#include - -#ifdef AB_CUSTOM_SETTINGS -# define AB_SETTINGS_CLASS ABSettings -#else -# define AB_SETTINGS_CLASS Settings -#endif - -namespace chatterino { - -class Settings; - -void _actuallyRegisterSetting( - std::weak_ptr setting); - -class AB_SETTINGS_CLASS -{ -public: - AB_SETTINGS_CLASS(const QString &settingsDirectory); - - void saveSnapshot(); - void restoreSnapshot(); - - static AB_SETTINGS_CLASS *instance; - - FloatSetting uiScale = {"/appearance/uiScale2", 1}; - BoolSetting windowTopMost = {"/appearance/windowAlwaysOnTop", false}; - - float getClampedUiScale() const; - void setClampedUiScale(float value); - -private: - std::unique_ptr snapshot_; -}; - -Settings *getSettings(); -AB_SETTINGS_CLASS *getABSettings(); - -} // namespace chatterino - -#ifdef CHATTERINO -# include "singletons/Settings.hpp" -#endif -#endif diff --git a/src/BaseTheme.cpp b/src/BaseTheme.cpp deleted file mode 100644 index 4c58ccba2..000000000 --- a/src/BaseTheme.cpp +++ /dev/null @@ -1,210 +0,0 @@ -#include "BaseTheme.hpp" - -namespace chatterino { -namespace { - double getMultiplierByTheme(const QString &themeName) - { - if (themeName == "Light") - { - return 0.8; - } - else if (themeName == "White") - { - return 1.0; - } - else if (themeName == "Black") - { - return -1.0; - } - else if (themeName == "Dark") - { - return -0.8; - } - /* - else if (themeName == "Custom") - { - return getSettings()->customThemeMultiplier.getValue(); - } - */ - - return -0.8; - } -} // namespace - -bool AB_THEME_CLASS::isLightTheme() const -{ - return this->isLight_; -} - -void AB_THEME_CLASS::update() -{ - this->actuallyUpdate(this->themeHue, - getMultiplierByTheme(this->themeName.getValue())); - - this->updated.invoke(); -} - -void AB_THEME_CLASS::actuallyUpdate(double hue, double multiplier) -{ - this->isLight_ = multiplier > 0; - bool lightWin = isLight_; - - // QColor themeColor = QColor::fromHslF(hue, 0.43, 0.5); - QColor themeColor = QColor::fromHslF(hue, 0.8, 0.5); - QColor themeColorNoSat = QColor::fromHslF(hue, 0, 0.5); - - qreal sat = 0; - // 0.05; - - auto getColor = [multiplier](double h, double s, double l, double a = 1.0) { - return QColor::fromHslF(h, s, ((l - 0.5) * multiplier) + 0.5, a); - }; - - /// WINDOW - { -#ifdef Q_OS_LINUX - this->window.background = lightWin ? "#fff" : QColor(61, 60, 56); -#else - this->window.background = lightWin ? "#fff" : "#111"; -#endif - - QColor fg = this->window.text = lightWin ? "#000" : "#eee"; - this->window.borderFocused = lightWin ? "#ccc" : themeColor; - this->window.borderUnfocused = lightWin ? "#ccc" : themeColorNoSat; - - // Ubuntu style - // TODO: add setting for this - // TabText = QColor(210, 210, 210); - // TabBackground = QColor(61, 60, 56); - // TabHoverText = QColor(210, 210, 210); - // TabHoverBackground = QColor(73, 72, 68); - - // message (referenced later) - this->messages.textColors.caret = // - this->messages.textColors.regular = isLight_ ? "#000" : "#fff"; - - QColor highlighted = lightWin ? QColor("#ff0000") : QColor("#ee6166"); - - /// TABS - if (lightWin) - { - this->tabs.regular = { - QColor("#444"), - {QColor("#fff"), QColor("#eee"), QColor("#fff")}, - {QColor("#fff"), QColor("#fff"), QColor("#fff")}}; - this->tabs.newMessage = { - QColor("#222"), - {QColor("#fff"), QColor("#eee"), QColor("#fff")}, - {QColor("#bbb"), QColor("#bbb"), QColor("#bbb")}}; - this->tabs.highlighted = { - fg, - {QColor("#fff"), QColor("#eee"), QColor("#fff")}, - {highlighted, highlighted, highlighted}}; - this->tabs.selected = { - QColor("#000"), - {QColor("#b4d7ff"), QColor("#b4d7ff"), QColor("#b4d7ff")}, - {this->accent, this->accent, this->accent}}; - } - else - { - this->tabs.regular = { - QColor("#aaa"), - {QColor("#252525"), QColor("#252525"), QColor("#252525")}, - {QColor("#444"), QColor("#444"), QColor("#444")}}; - this->tabs.newMessage = { - fg, - {QColor("#252525"), QColor("#252525"), QColor("#252525")}, - {QColor("#888"), QColor("#888"), QColor("#888")}}; - this->tabs.highlighted = { - fg, - {QColor("#252525"), QColor("#252525"), QColor("#252525")}, - {highlighted, highlighted, highlighted}}; - - this->tabs.selected = { - QColor("#fff"), - {QColor("#555555"), QColor("#555555"), QColor("#555555")}, - {this->accent, this->accent, this->accent}}; - } - - // scrollbar - this->scrollbars.highlights.highlight = QColor("#ee6166"); - this->scrollbars.highlights.subscription = QColor("#C466FF"); - - // this->tabs.newMessage = { - // fg, - // {QBrush(blendColors(themeColor, "#ccc", 0.9), Qt::FDiagPattern), - // QBrush(blendColors(themeColor, "#ccc", 0.9), Qt::FDiagPattern), - // QBrush(blendColors(themeColorNoSat, "#ccc", 0.9), - // Qt::FDiagPattern)}}; - - // this->tabs.newMessage = { - // fg, - // {QBrush(blendColors(themeColor, "#666", 0.7), - // Qt::FDiagPattern), - // QBrush(blendColors(themeColor, "#666", 0.5), - // Qt::FDiagPattern), - // QBrush(blendColors(themeColorNoSat, "#666", 0.7), - // Qt::FDiagPattern)}}; - // this->tabs.highlighted = {fg, {QColor("#777"), - // QColor("#777"), QColor("#666")}}; - - this->tabs.dividerLine = - this->tabs.selected.backgrounds.regular.color(); - } - - // Message - this->messages.textColors.link = - isLight_ ? QColor(66, 134, 244) : QColor(66, 134, 244); - this->messages.textColors.system = QColor(140, 127, 127); - this->messages.textColors.chatPlaceholder = - isLight_ ? QColor(175, 159, 159) : QColor(93, 85, 85); - - this->messages.backgrounds.regular = getColor(0, sat, 1); - this->messages.backgrounds.alternate = getColor(0, sat, 0.96); - - // this->messages.backgrounds.resub - // this->messages.backgrounds.whisper - this->messages.disabled = getColor(0, sat, 1, 0.6); - // this->messages.seperator = - // this->messages.seperatorInner = - - // Scrollbar - this->scrollbars.background = QColor(0, 0, 0, 0); - // this->scrollbars.background = splits.background; - // this->scrollbars.background.setAlphaF(qreal(0.2)); - this->scrollbars.thumb = getColor(0, sat, 0.70); - this->scrollbars.thumbSelected = getColor(0, sat, 0.65); - - // tooltip - this->tooltip.background = QColor(0, 0, 0); - this->tooltip.text = QColor(255, 255, 255); - - // Selection - this->messages.selection = - isLightTheme() ? QColor(0, 0, 0, 64) : QColor(255, 255, 255, 64); -} - -QColor AB_THEME_CLASS::blendColors(const QColor &color1, const QColor &color2, - qreal ratio) -{ - int r = int(color1.red() * (1 - ratio) + color2.red() * ratio); - int g = int(color1.green() * (1 - ratio) + color2.green() * ratio); - int b = int(color1.blue() * (1 - ratio) + color2.blue() * ratio); - - return QColor(r, g, b, 255); -} - -#ifndef AB_CUSTOM_THEME -Theme *getTheme() -{ - static auto theme = [] { - auto theme = new Theme(); - theme->update(); - return theme; - }(); - - return theme; -} -#endif - -} // namespace chatterino diff --git a/src/BaseTheme.hpp b/src/BaseTheme.hpp deleted file mode 100644 index 2d6ee5cdc..000000000 --- a/src/BaseTheme.hpp +++ /dev/null @@ -1,118 +0,0 @@ -#ifndef AB_THEME_H -#define AB_THEME_H - -#include -#include -#include - -#ifdef AB_CUSTOM_THEME -# define AB_THEME_CLASS BaseTheme -#else -# define AB_THEME_CLASS Theme -#endif - -namespace chatterino { - -class Theme; - -class AB_THEME_CLASS -{ -public: - bool isLightTheme() const; - - struct TabColors { - QColor text; - struct { - QBrush regular; - QBrush hover; - QBrush unfocused; - } backgrounds; - struct { - QColor regular; - QColor hover; - QColor unfocused; - } line; - }; - - QColor accent{"#00aeef"}; - - /// WINDOW - struct { - QColor background; - QColor text; - QColor borderUnfocused; - QColor borderFocused; - } window; - - /// TABS - struct { - TabColors regular; - TabColors newMessage; - TabColors highlighted; - TabColors selected; - QColor border; - QColor dividerLine; - } tabs; - - /// MESSAGES - struct { - struct { - QColor regular; - QColor caret; - QColor link; - QColor system; - QColor chatPlaceholder; - } textColors; - - struct { - QColor regular; - QColor alternate; - // QColor whisper; - } backgrounds; - - QColor disabled; - // QColor seperator; - // QColor seperatorInner; - QColor selection; - } messages; - - /// SCROLLBAR - struct { - QColor background; - QColor thumb; - QColor thumbSelected; - struct { - QColor highlight; - QColor subscription; - } highlights; - } scrollbars; - - /// TOOLTIP - struct { - QColor text; - QColor background; - } tooltip; - - void update(); - virtual void actuallyUpdate(double hue, double multiplier); - QColor blendColors(const QColor &color1, const QColor &color2, qreal ratio); - - pajlada::Signals::NoArgSignal updated; - - QStringSetting themeName{"/appearance/theme/name", "Dark"}; - DoubleSetting themeHue{"/appearance/theme/hue", 0.0}; - -private: - bool isLight_ = false; -}; - -// Implemented in parent project if AB_CUSTOM_THEME is set. -// Otherwise implemented in BaseThemecpp -Theme *getTheme(); - -} // namespace chatterino - -#ifdef CHATTERINO -# include "singletons/Theme.hpp" -#endif -#endif diff --git a/src/BrowserExtension.cpp b/src/BrowserExtension.cpp index dad0ac2af..4b0f7e69a 100644 --- a/src/BrowserExtension.cpp +++ b/src/BrowserExtension.cpp @@ -1,14 +1,8 @@ #include "BrowserExtension.hpp" #include "singletons/NativeMessaging.hpp" +#include "util/RenameThread.hpp" -#include -#include -#include -#include - -#include -#include #include #include #include @@ -16,75 +10,94 @@ #ifdef Q_OS_WIN # include # include -# include -#endif -namespace chatterino { +# include + +#endif namespace { - void initFileMode() - { + +using namespace chatterino; + +void initFileMode() +{ #ifdef Q_OS_WIN - _setmode(_fileno(stdin), _O_BINARY); - _setmode(_fileno(stdout), _O_BINARY); + _setmode(_fileno(stdin), _O_BINARY); + _setmode(_fileno(stdout), _O_BINARY); #endif +} + +// TODO(Qt6): Use QUtf8String +void sendToBrowser(QLatin1String str) +{ + auto len = static_cast(str.size()); + std::cout.write(reinterpret_cast(&len), sizeof(len)); + std::cout.write(str.data(), str.size()); + std::cout.flush(); +} + +QByteArray receiveFromBrowser() +{ + uint32_t size = 0; + std::cin.read(reinterpret_cast(&size), sizeof(size)); + + if (std::cin.eof()) + { + return {}; } - void runLoop(NativeMessagingClient &client) - { - auto received_message = std::make_shared(true); + QByteArray buffer{static_cast(size), + Qt::Uninitialized}; + std::cin.read(buffer.data(), size); - auto thread = std::thread([=]() { - while (true) - { - using namespace std::chrono_literals; - if (!received_message->exchange(false)) - { - _Exit(1); - } - std::this_thread::sleep_for(5s); - } - }); + return buffer; +} +void runLoop() +{ + auto receivedMessage = std::make_shared(true); + + auto thread = std::thread([=]() { while (true) { - char size_c[4]; - std::cin.read(size_c, 4); - - if (std::cin.eof()) + using namespace std::chrono_literals; + if (!receivedMessage->exchange(false)) { - break; + sendToBrowser(QLatin1String{ + R"({"type":"status","status":"exiting-host","reason":"no message was received in 10s"})"}); + _Exit(1); } - - auto size = *reinterpret_cast(size_c); - - std::unique_ptr buffer(new char[size + 1]); - std::cin.read(buffer.get(), size); - *(buffer.get() + size) = '\0'; - - auto data = QByteArray::fromRawData(buffer.get(), - static_cast(size)); - auto doc = QJsonDocument(); - - if (doc.object().value("type") == "nm_pong") - { - received_message->store(true); - } - - received_message->store(true); - - client.sendMessage(data); + std::this_thread::sleep_for(10s); } + }); + renameThread(thread, "BrowserPingCheck"); + + while (true) + { + auto buffer = receiveFromBrowser(); + if (buffer.isNull()) + { + break; + } + + receivedMessage->store(true); + + nm::client::sendMessage(buffer); } + + sendToBrowser(QLatin1String{ + R"({"type":"status","status":"exiting-host","reason":"received EOF"})"}); + _Exit(0); +} } // namespace +namespace chatterino { + void runBrowserExtensionHost() { initFileMode(); - NativeMessagingClient client; - - runLoop(client); + runLoop(); } } // namespace chatterino diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 00d16c3f5..589e924bb 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,13 +1,15 @@ set(LIBRARY_PROJECT "${PROJECT_NAME}-lib") +set(VERSION_PROJECT "${LIBRARY_PROJECT}-version") set(EXECUTABLE_PROJECT "${PROJECT_NAME}") +add_compile_definitions(QT_DISABLE_DEPRECATED_BEFORE=0x050F00) + +# registers the native messageing host +option(CHATTERINO_DEBUG_NATIVE_MESSAGES "Debug native messages" OFF) +option(CHATTERINO_STATIC_QT_BUILD "Static link Qt" OFF) set(SOURCE_FILES Application.cpp Application.hpp - BaseSettings.cpp - BaseSettings.hpp - BaseTheme.cpp - BaseTheme.hpp BrowserExtension.cpp BrowserExtension.hpp RunGui.cpp @@ -23,35 +25,35 @@ set(SOURCE_FILES common/ChatterinoSetting.hpp common/ChatterSet.cpp common/ChatterSet.hpp - common/CompletionModel.cpp - common/CompletionModel.hpp common/Credentials.cpp common/Credentials.hpp - common/DownloadManager.cpp - common/DownloadManager.hpp common/Env.cpp common/Env.hpp common/LinkParser.cpp common/LinkParser.hpp + common/Literals.hpp common/Modes.cpp common/Modes.hpp - common/NetworkCommon.cpp - common/NetworkCommon.hpp - common/NetworkManager.cpp - common/NetworkManager.hpp - common/NetworkPrivate.cpp - common/NetworkPrivate.hpp - common/NetworkRequest.cpp - common/NetworkRequest.hpp - common/NetworkResult.cpp - common/NetworkResult.hpp common/QLogging.cpp common/QLogging.hpp - common/Version.cpp - common/Version.hpp common/WindowDescriptors.cpp common/WindowDescriptors.hpp + common/enums/MessageOverflow.hpp + + common/network/NetworkCommon.cpp + common/network/NetworkCommon.hpp + common/network/NetworkManager.cpp + common/network/NetworkManager.hpp + common/network/NetworkPrivate.cpp + common/network/NetworkPrivate.hpp + common/network/NetworkRequest.cpp + common/network/NetworkRequest.hpp + common/network/NetworkResult.cpp + common/network/NetworkResult.hpp + common/network/NetworkTask.cpp + common/network/NetworkTask.hpp + controllers/accounts/Account.cpp controllers/accounts/Account.hpp controllers/accounts/AccountController.cpp @@ -59,21 +61,114 @@ set(SOURCE_FILES controllers/accounts/AccountModel.cpp controllers/accounts/AccountModel.hpp - controllers/commands/Command.cpp - controllers/commands/Command.hpp + controllers/commands/builtin/chatterino/Debugging.cpp + controllers/commands/builtin/chatterino/Debugging.hpp + controllers/commands/builtin/Misc.cpp + controllers/commands/builtin/Misc.hpp + controllers/commands/builtin/twitch/AddModerator.cpp + controllers/commands/builtin/twitch/AddModerator.hpp + controllers/commands/builtin/twitch/AddVIP.cpp + controllers/commands/builtin/twitch/AddVIP.hpp + controllers/commands/builtin/twitch/Announce.cpp + controllers/commands/builtin/twitch/Announce.hpp + controllers/commands/builtin/twitch/Ban.cpp + controllers/commands/builtin/twitch/Ban.hpp + controllers/commands/builtin/twitch/Block.cpp + controllers/commands/builtin/twitch/Block.hpp + controllers/commands/builtin/twitch/ChatSettings.cpp + controllers/commands/builtin/twitch/ChatSettings.hpp + controllers/commands/builtin/twitch/Chatters.cpp + controllers/commands/builtin/twitch/Chatters.hpp + controllers/commands/builtin/twitch/DeleteMessages.cpp + controllers/commands/builtin/twitch/DeleteMessages.hpp + controllers/commands/builtin/twitch/GetModerators.cpp + controllers/commands/builtin/twitch/GetModerators.hpp + controllers/commands/builtin/twitch/GetVIPs.cpp + controllers/commands/builtin/twitch/GetVIPs.hpp + controllers/commands/builtin/twitch/Raid.cpp + controllers/commands/builtin/twitch/Raid.hpp + controllers/commands/builtin/twitch/RemoveModerator.cpp + controllers/commands/builtin/twitch/RemoveModerator.hpp + controllers/commands/builtin/twitch/RemoveVIP.cpp + controllers/commands/builtin/twitch/RemoveVIP.hpp + controllers/commands/builtin/twitch/SendReply.cpp + controllers/commands/builtin/twitch/SendReply.hpp + controllers/commands/builtin/twitch/SendWhisper.cpp + controllers/commands/builtin/twitch/SendWhisper.hpp + controllers/commands/builtin/twitch/ShieldMode.cpp + controllers/commands/builtin/twitch/ShieldMode.hpp + controllers/commands/builtin/twitch/Shoutout.cpp + controllers/commands/builtin/twitch/Shoutout.hpp + controllers/commands/builtin/twitch/StartCommercial.cpp + controllers/commands/builtin/twitch/StartCommercial.hpp + controllers/commands/builtin/twitch/Unban.cpp + controllers/commands/builtin/twitch/Unban.hpp + controllers/commands/builtin/twitch/UpdateChannel.cpp + controllers/commands/builtin/twitch/UpdateChannel.hpp + controllers/commands/builtin/twitch/UpdateColor.cpp + controllers/commands/builtin/twitch/UpdateColor.hpp + controllers/commands/builtin/twitch/Warn.cpp + controllers/commands/builtin/twitch/Warn.hpp + controllers/commands/common/ChannelAction.cpp + controllers/commands/common/ChannelAction.hpp + controllers/commands/CommandContext.hpp controllers/commands/CommandController.cpp controllers/commands/CommandController.hpp + controllers/commands/Command.cpp + controllers/commands/Command.hpp controllers/commands/CommandModel.cpp controllers/commands/CommandModel.hpp + controllers/completion/CompletionModel.cpp + controllers/completion/CompletionModel.hpp + controllers/completion/sources/Source.hpp + controllers/completion/sources/CommandSource.cpp + controllers/completion/sources/CommandSource.hpp + controllers/completion/sources/EmoteSource.cpp + controllers/completion/sources/EmoteSource.hpp + controllers/completion/sources/Helpers.hpp + controllers/completion/sources/UnifiedSource.cpp + controllers/completion/sources/UnifiedSource.hpp + controllers/completion/sources/UserSource.cpp + controllers/completion/sources/UserSource.hpp + controllers/completion/strategies/ClassicEmoteStrategy.cpp + controllers/completion/strategies/ClassicEmoteStrategy.hpp + controllers/completion/strategies/ClassicUserStrategy.cpp + controllers/completion/strategies/ClassicUserStrategy.hpp + controllers/completion/strategies/CommandStrategy.cpp + controllers/completion/strategies/CommandStrategy.hpp + controllers/completion/strategies/SmartEmoteStrategy.cpp + controllers/completion/strategies/SmartEmoteStrategy.cpp + controllers/completion/strategies/Strategy.hpp + controllers/completion/TabCompletionModel.cpp + controllers/completion/TabCompletionModel.hpp + controllers/filters/FilterModel.cpp controllers/filters/FilterModel.hpp - controllers/filters/parser/FilterParser.cpp - controllers/filters/parser/FilterParser.hpp - controllers/filters/parser/Tokenizer.cpp - controllers/filters/parser/Tokenizer.hpp - controllers/filters/parser/Types.cpp - controllers/filters/parser/Types.hpp + controllers/filters/FilterRecord.cpp + controllers/filters/FilterRecord.hpp + controllers/filters/FilterSet.cpp + controllers/filters/FilterSet.hpp + controllers/filters/lang/expressions/Expression.cpp + controllers/filters/lang/expressions/Expression.hpp + controllers/filters/lang/expressions/BinaryOperation.cpp + controllers/filters/lang/expressions/BinaryOperation.hpp + controllers/filters/lang/expressions/ListExpression.cpp + controllers/filters/lang/expressions/ListExpression.hpp + controllers/filters/lang/expressions/RegexExpression.cpp + controllers/filters/lang/expressions/RegexExpression.hpp + controllers/filters/lang/expressions/UnaryOperation.hpp + controllers/filters/lang/expressions/UnaryOperation.cpp + controllers/filters/lang/expressions/ValueExpression.cpp + controllers/filters/lang/expressions/ValueExpression.hpp + controllers/filters/lang/Filter.cpp + controllers/filters/lang/Filter.hpp + controllers/filters/lang/FilterParser.cpp + controllers/filters/lang/FilterParser.hpp + controllers/filters/lang/Tokenizer.cpp + controllers/filters/lang/Tokenizer.hpp + controllers/filters/lang/Types.cpp + controllers/filters/lang/Types.hpp controllers/highlights/BadgeHighlightModel.cpp controllers/highlights/BadgeHighlightModel.hpp @@ -105,12 +200,19 @@ set(SOURCE_FILES controllers/ignores/IgnoreController.hpp controllers/ignores/IgnoreModel.cpp controllers/ignores/IgnoreModel.hpp + controllers/ignores/IgnorePhrase.cpp + controllers/ignores/IgnorePhrase.hpp controllers/moderationactions/ModerationAction.cpp controllers/moderationactions/ModerationAction.hpp controllers/moderationactions/ModerationActionModel.cpp controllers/moderationactions/ModerationActionModel.hpp + controllers/logging/ChannelLog.cpp + controllers/logging/ChannelLog.hpp + controllers/logging/ChannelLoggingModel.cpp + controllers/logging/ChannelLoggingModel.hpp + controllers/nicknames/NicknamesModel.cpp controllers/nicknames/NicknamesModel.hpp controllers/nicknames/Nickname.hpp @@ -123,6 +225,38 @@ set(SOURCE_FILES controllers/pings/MutedChannelModel.cpp controllers/pings/MutedChannelModel.hpp + controllers/plugins/api/ChannelRef.cpp + controllers/plugins/api/ChannelRef.hpp + controllers/plugins/api/IOWrapper.cpp + controllers/plugins/api/IOWrapper.hpp + controllers/plugins/api/HTTPRequest.cpp + controllers/plugins/api/HTTPRequest.hpp + controllers/plugins/api/HTTPResponse.cpp + controllers/plugins/api/HTTPResponse.hpp + controllers/plugins/LuaAPI.cpp + controllers/plugins/LuaAPI.hpp + controllers/plugins/PluginPermission.cpp + controllers/plugins/PluginPermission.hpp + controllers/plugins/Plugin.cpp + controllers/plugins/Plugin.hpp + controllers/plugins/PluginController.hpp + controllers/plugins/PluginController.cpp + controllers/plugins/LuaUtilities.cpp + controllers/plugins/LuaUtilities.hpp + + controllers/sound/ISoundController.hpp + controllers/sound/MiniaudioBackend.cpp + controllers/sound/MiniaudioBackend.hpp + controllers/sound/NullBackend.cpp + controllers/sound/NullBackend.hpp + + controllers/twitch/LiveController.cpp + controllers/twitch/LiveController.hpp + + controllers/userdata/UserDataController.cpp + controllers/userdata/UserDataController.hpp + controllers/userdata/UserData.hpp + debug/Benchmark.cpp debug/Benchmark.hpp @@ -142,20 +276,22 @@ set(SOURCE_FILES messages/MessageColor.hpp messages/MessageElement.cpp messages/MessageElement.hpp + messages/MessageFlag.hpp messages/MessageThread.cpp messages/MessageThread.hpp - messages/SharedMessageBuilder.cpp - messages/SharedMessageBuilder.hpp - messages/layouts/MessageLayout.cpp messages/layouts/MessageLayout.hpp messages/layouts/MessageLayoutContainer.cpp messages/layouts/MessageLayoutContainer.hpp + messages/layouts/MessageLayoutContext.cpp + messages/layouts/MessageLayoutContext.hpp messages/layouts/MessageLayoutElement.cpp messages/layouts/MessageLayoutElement.hpp messages/search/AuthorPredicate.cpp messages/search/AuthorPredicate.hpp + messages/search/BadgePredicate.cpp + messages/search/BadgePredicate.hpp messages/search/ChannelPredicate.cpp messages/search/ChannelPredicate.hpp messages/search/LinkPredicate.cpp @@ -166,18 +302,23 @@ set(SOURCE_FILES messages/search/RegexPredicate.hpp messages/search/SubstringPredicate.cpp messages/search/SubstringPredicate.hpp + messages/search/SubtierPredicate.cpp + messages/search/SubtierPredicate.hpp providers/IvrApi.cpp providers/IvrApi.hpp - providers/LinkResolver.cpp - providers/LinkResolver.hpp - providers/RecentMessagesApi.cpp - providers/RecentMessagesApi.hpp + providers/NetworkConfigurationProvider.cpp + providers/NetworkConfigurationProvider.hpp providers/bttv/BttvEmotes.cpp providers/bttv/BttvEmotes.hpp - providers/bttv/LoadBttvChannelEmote.cpp - providers/bttv/LoadBttvChannelEmote.hpp + providers/bttv/BttvLiveUpdates.cpp + providers/bttv/BttvLiveUpdates.hpp + + providers/bttv/liveupdates/BttvLiveUpdateMessages.cpp + providers/bttv/liveupdates/BttvLiveUpdateMessages.hpp + providers/bttv/liveupdates/BttvLiveUpdateSubscription.cpp + providers/bttv/liveupdates/BttvLiveUpdateSubscription.hpp providers/chatterino/ChatterinoBadges.cpp providers/chatterino/ChatterinoBadges.hpp @@ -192,23 +333,44 @@ set(SOURCE_FILES providers/ffz/FfzBadges.hpp providers/ffz/FfzEmotes.cpp providers/ffz/FfzEmotes.hpp + providers/ffz/FfzUtil.cpp + providers/ffz/FfzUtil.hpp - providers/irc/AbstractIrcServer.cpp - providers/irc/AbstractIrcServer.hpp - providers/irc/Irc2.cpp - providers/irc/Irc2.hpp - providers/irc/IrcAccount.cpp - providers/irc/IrcAccount.hpp - providers/irc/IrcChannel2.cpp - providers/irc/IrcChannel2.hpp - providers/irc/IrcCommands.cpp - providers/irc/IrcCommands.hpp providers/irc/IrcConnection2.cpp providers/irc/IrcConnection2.hpp - providers/irc/IrcMessageBuilder.cpp - providers/irc/IrcMessageBuilder.hpp - providers/irc/IrcServer.cpp - providers/irc/IrcServer.hpp + + providers/links/LinkInfo.cpp + providers/links/LinkInfo.hpp + providers/links/LinkResolver.cpp + providers/links/LinkResolver.hpp + + providers/liveupdates/BasicPubSubClient.hpp + providers/liveupdates/BasicPubSubManager.hpp + providers/liveupdates/BasicPubSubWebsocket.hpp + + providers/recentmessages/Api.cpp + providers/recentmessages/Api.hpp + providers/recentmessages/Impl.cpp + providers/recentmessages/Impl.hpp + + providers/seventv/SeventvAPI.cpp + providers/seventv/SeventvAPI.hpp + providers/seventv/SeventvBadges.cpp + providers/seventv/SeventvBadges.hpp + providers/seventv/SeventvCosmetics.hpp + providers/seventv/SeventvEmotes.cpp + providers/seventv/SeventvEmotes.hpp + providers/seventv/SeventvEventAPI.cpp + providers/seventv/SeventvEventAPI.hpp + + providers/seventv/eventapi/Client.cpp + providers/seventv/eventapi/Client.hpp + providers/seventv/eventapi/Dispatch.cpp + providers/seventv/eventapi/Dispatch.hpp + providers/seventv/eventapi/Message.cpp + providers/seventv/eventapi/Message.hpp + providers/seventv/eventapi/Subscription.cpp + providers/seventv/eventapi/Subscription.hpp providers/twitch/ChannelPointReward.cpp providers/twitch/ChannelPointReward.hpp @@ -240,8 +402,6 @@ set(SOURCE_FILES providers/twitch/TwitchHelpers.hpp providers/twitch/TwitchIrcServer.cpp providers/twitch/TwitchIrcServer.hpp - providers/twitch/TwitchMessageBuilder.cpp - providers/twitch/TwitchMessageBuilder.hpp providers/twitch/TwitchUser.cpp providers/twitch/TwitchUser.hpp @@ -255,6 +415,8 @@ set(SOURCE_FILES providers/twitch/pubsubmessages/ChatModeratorAction.hpp providers/twitch/pubsubmessages/Listen.cpp providers/twitch/pubsubmessages/Listen.hpp + providers/twitch/pubsubmessages/LowTrustUsers.cpp + providers/twitch/pubsubmessages/LowTrustUsers.hpp providers/twitch/pubsubmessages/Message.hpp providers/twitch/pubsubmessages/Unlisten.cpp providers/twitch/pubsubmessages/Unlisten.hpp @@ -264,12 +426,14 @@ set(SOURCE_FILES providers/twitch/api/Helix.cpp providers/twitch/api/Helix.hpp - singletons/Badges.cpp - singletons/Badges.hpp + singletons/CrashHandler.cpp + singletons/CrashHandler.hpp singletons/Emotes.cpp singletons/Emotes.hpp singletons/Fonts.cpp singletons/Fonts.hpp + singletons/ImageUploader.cpp + singletons/ImageUploader.hpp singletons/Logging.cpp singletons/Logging.hpp singletons/NativeMessaging.cpp @@ -280,12 +444,12 @@ set(SOURCE_FILES singletons/Resources.hpp singletons/Settings.cpp singletons/Settings.hpp + singletons/StreamerMode.cpp + singletons/StreamerMode.hpp singletons/Theme.cpp singletons/Theme.hpp singletons/Toasts.cpp singletons/Toasts.hpp - singletons/TooltipPreviewImage.cpp - singletons/TooltipPreviewImage.hpp singletons/Updates.cpp singletons/Updates.hpp singletons/WindowManager.cpp @@ -296,8 +460,11 @@ set(SOURCE_FILES singletons/helper/LoggingChannel.cpp singletons/helper/LoggingChannel.hpp + util/AbandonObject.hpp util/AttachToConsole.cpp util/AttachToConsole.hpp + util/CancellationToken.hpp + util/ChannelHelpers.hpp util/Clipboard.cpp util/Clipboard.hpp util/DebugCount.cpp @@ -316,27 +483,41 @@ set(SOURCE_FILES util/IncognitoBrowser.hpp util/InitUpdateButton.cpp util/InitUpdateButton.hpp + util/IpcQueue.cpp + util/IpcQueue.hpp util/LayoutHelper.cpp util/LayoutHelper.hpp - util/NuulsUploader.cpp - util/NuulsUploader.hpp + util/LoadPixmap.cpp + util/LoadPixmap.hpp util/RapidjsonHelpers.cpp util/RapidjsonHelpers.hpp util/RatelimitBucket.cpp util/RatelimitBucket.hpp + util/RenameThread.cpp + util/RenameThread.hpp util/SampleData.cpp util/SampleData.hpp - util/SplitCommand.cpp - util/SplitCommand.hpp + util/SharedPtrElementLess.hpp + util/SignalListener.hpp util/StreamLink.cpp util/StreamLink.hpp - util/StreamerMode.cpp - util/StreamerMode.hpp + util/ThreadGuard.hpp util/Twitch.cpp util/Twitch.hpp util/TypeName.hpp + util/Variant.hpp + util/WidgetHelpers.cpp + util/WidgetHelpers.hpp util/WindowsHelper.cpp util/WindowsHelper.hpp + util/XDGDesktopFile.cpp + util/XDGDesktopFile.hpp + util/XDGDirectory.cpp + util/XDGDirectory.hpp + util/XDGHelper.cpp + util/XDGHelper.hpp + + util/serialize/Container.hpp widgets/AccountSwitchPopup.cpp widgets/AccountSwitchPopup.hpp @@ -360,8 +541,8 @@ set(SOURCE_FILES widgets/Notebook.hpp widgets/Scrollbar.cpp widgets/Scrollbar.hpp - widgets/StreamView.cpp - widgets/StreamView.hpp + widgets/TooltipEntryWidget.cpp + widgets/TooltipEntryWidget.hpp widgets/TooltipWidget.cpp widgets/TooltipWidget.hpp widgets/Window.cpp @@ -377,15 +558,10 @@ set(SOURCE_FILES widgets/dialogs/EditHotkeyDialog.hpp widgets/dialogs/EmotePopup.cpp widgets/dialogs/EmotePopup.hpp - widgets/dialogs/IrcConnectionEditor.cpp - widgets/dialogs/IrcConnectionEditor.hpp - widgets/dialogs/IrcConnectionEditor.ui widgets/dialogs/LastRunCrashDialog.cpp widgets/dialogs/LastRunCrashDialog.hpp widgets/dialogs/LoginDialog.cpp widgets/dialogs/LoginDialog.hpp - widgets/dialogs/NotificationPopup.cpp - widgets/dialogs/NotificationPopup.hpp widgets/dialogs/QualityPopup.cpp widgets/dialogs/QualityPopup.hpp widgets/dialogs/ReplyThreadPopup.cpp @@ -413,12 +589,25 @@ set(SOURCE_FILES widgets/dialogs/switcher/SwitchSplitItem.cpp widgets/dialogs/switcher/SwitchSplitItem.hpp + widgets/helper/color/AlphaSlider.cpp + widgets/helper/color/AlphaSlider.hpp + widgets/helper/color/Checkerboard.cpp + widgets/helper/color/Checkerboard.hpp + widgets/helper/color/ColorButton.cpp + widgets/helper/color/ColorButton.hpp + widgets/helper/color/ColorInput.cpp + widgets/helper/color/ColorInput.hpp + widgets/helper/color/ColorItemDelegate.cpp + widgets/helper/color/ColorItemDelegate.hpp + widgets/helper/color/HueSlider.cpp + widgets/helper/color/HueSlider.hpp + widgets/helper/color/SBCanvas.cpp + widgets/helper/color/SBCanvas.hpp + widgets/helper/Button.cpp widgets/helper/Button.hpp widgets/helper/ChannelView.cpp widgets/helper/ChannelView.hpp - widgets/helper/ColorButton.cpp - widgets/helper/ColorButton.hpp widgets/helper/ComboBoxItemDelegate.cpp widgets/helper/ComboBoxItemDelegate.hpp widgets/helper/DebugPopup.cpp @@ -427,16 +616,18 @@ set(SOURCE_FILES widgets/helper/EditableModelView.hpp widgets/helper/EffectLabel.cpp widgets/helper/EffectLabel.hpp + widgets/helper/IconDelegate.cpp + widgets/helper/IconDelegate.hpp + widgets/helper/InvisibleSizeGrip.cpp + widgets/helper/InvisibleSizeGrip.hpp + widgets/helper/MessageView.cpp + widgets/helper/MessageView.hpp widgets/helper/NotebookButton.cpp widgets/helper/NotebookButton.hpp widgets/helper/NotebookTab.cpp widgets/helper/NotebookTab.hpp - widgets/helper/QColorPicker.cpp - widgets/helper/QColorPicker.hpp widgets/helper/RegExpItemDelegate.cpp widgets/helper/RegExpItemDelegate.hpp - widgets/helper/TrimRegExpValidator.cpp - widgets/helper/TrimRegExpValidator.hpp widgets/helper/ResizingTextEdit.cpp widgets/helper/ResizingTextEdit.hpp widgets/helper/ScrollbarHighlight.cpp @@ -447,8 +638,17 @@ set(SOURCE_FILES widgets/helper/SettingsDialogTab.hpp widgets/helper/SignalLabel.cpp widgets/helper/SignalLabel.hpp + widgets/helper/TableStyles.cpp + widgets/helper/TableStyles.hpp widgets/helper/TitlebarButton.cpp widgets/helper/TitlebarButton.hpp + widgets/helper/TitlebarButtons.cpp + widgets/helper/TitlebarButtons.hpp + widgets/helper/TrimRegExpValidator.cpp + widgets/helper/TrimRegExpValidator.hpp + + widgets/layout/FlowLayout.cpp + widgets/layout/FlowLayout.hpp widgets/listview/GenericItemDelegate.cpp widgets/listview/GenericItemDelegate.hpp @@ -485,17 +685,22 @@ set(SOURCE_FILES widgets/settingspages/NicknamesPage.hpp widgets/settingspages/NotificationPage.cpp widgets/settingspages/NotificationPage.hpp + widgets/settingspages/PluginsPage.cpp + widgets/settingspages/PluginsPage.hpp widgets/settingspages/SettingsPage.cpp widgets/settingspages/SettingsPage.hpp widgets/splits/ClosedSplits.cpp widgets/splits/ClosedSplits.hpp + widgets/splits/DraggedSplit.cpp + widgets/splits/DraggedSplit.hpp widgets/splits/InputCompletionItem.cpp widgets/splits/InputCompletionItem.hpp widgets/splits/InputCompletionPopup.cpp widgets/splits/InputCompletionPopup.hpp widgets/splits/Split.cpp widgets/splits/Split.hpp + widgets/splits/SplitCommon.hpp widgets/splits/SplitContainer.cpp widgets/splits/SplitContainer.hpp widgets/splits/SplitHeader.cpp @@ -505,20 +710,10 @@ set(SOURCE_FILES widgets/splits/SplitOverlay.cpp widgets/splits/SplitOverlay.hpp - autogenerated/ResourcesAutogen.cpp - autogenerated/ResourcesAutogen.hpp - ${CMAKE_SOURCE_DIR}/resources/resources.qrc - ${CMAKE_SOURCE_DIR}/resources/resources_autogenerated.qrc ) -if (WIN32) - # clang-cl doesn't support resource files - if (NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang") - list(APPEND SOURCE_FILES "${CMAKE_SOURCE_DIR}/resources/windows.rc") - endif () - -elseif (APPLE) +if (APPLE) set(MACOS_BUNDLE_ICON_FILE "${CMAKE_SOURCE_DIR}/resources/chatterino.icns") list(APPEND SOURCE_FILES "${MACOS_BUNDLE_ICON_FILE}") set_source_files_properties(${MACOS_BUNDLE_ICON_FILE} PROPERTIES MACOSX_PACKAGE_LOCATION "Resources") @@ -527,15 +722,40 @@ endif () # Generate source groups for use in IDEs source_group(TREE ${CMAKE_SOURCE_DIR} FILES ${SOURCE_FILES}) +# Add autogenerated files +list(APPEND SOURCE_FILES ${RES_AUTOGEN_FILES}) + add_library(${LIBRARY_PROJECT} OBJECT ${SOURCE_FILES}) +if(CHATTERINO_PLUGINS) + target_compile_definitions(${LIBRARY_PROJECT} + PUBLIC + CHATTERINO_HAVE_PLUGINS + ) + message(STATUS "Building Chatterino with lua plugin support enabled.") +endif() + +if (CHATTERINO_GENERATE_COVERAGE) + include(CodeCoverage) + append_coverage_compiler_flags_to_target(${LIBRARY_PROJECT}) + message(STATUS "project source dir: ${PROJECT_SOURCE_DIR}/src") + setup_target_for_coverage_gcovr_html( + NAME coverage + EXECUTABLE ctest + EXCLUDE "/usr/include/*" + EXCLUDE "build-*/*" + EXCLUDE "lib/*" + EXCLUDE "*/ui_*.h" + EXCLUDE "*/moc_*.cpp" + ) +endif () + target_link_libraries(${LIBRARY_PROJECT} PUBLIC Qt${MAJOR_QT_VERSION}::Core Qt${MAJOR_QT_VERSION}::Widgets Qt${MAJOR_QT_VERSION}::Gui Qt${MAJOR_QT_VERSION}::Network - Qt${MAJOR_QT_VERSION}::Multimedia Qt${MAJOR_QT_VERSION}::Svg Qt${MAJOR_QT_VERSION}::Concurrent @@ -548,7 +768,12 @@ target_link_libraries(${LIBRARY_PROJECT} RapidJSON::RapidJSON LRUCache MagicEnum + $<$:Wtsapi32> ) +if (CHATTERINO_PLUGINS) + target_link_libraries(${LIBRARY_PROJECT} PUBLIC lua) +endif() + if (BUILD_WITH_QTKEYCHAIN) target_link_libraries(${LIBRARY_PROJECT} PUBLIC @@ -561,38 +786,95 @@ else() ) endif() +# Set the output of TARGET to be +# - CMAKE_BIN_DIR/lib for libraries +# - CMAKE_BIN_DIR/bin for BINARIES +# an additional argument specifies the subdirectory. +function(set_target_directory_hierarchy TARGET) + set_target_properties(${TARGET} + PROPERTIES + ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib/${ARGV1}" + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib/${ARGV1}" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/${ARGV1}" + RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}/bin/${ARGV1}" + RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}/bin/${ARGV1}" + RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO "${CMAKE_BINARY_DIR}/bin/${ARGV1}" + ) +endfunction() + if (BUILD_APP) if (APPLE) add_executable(${EXECUTABLE_PROJECT} ${MACOS_BUNDLE_ICON_FILE} main.cpp) else() add_executable(${EXECUTABLE_PROJECT} main.cpp) endif() - add_sanitizers(${EXECUTABLE_PROJECT}) - target_include_directories(${EXECUTABLE_PROJECT} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + if(COMMAND add_sanitizers) + add_sanitizers(${EXECUTABLE_PROJECT}) + else() + message(WARNING "Sanitizers support is disabled") + endif() + + if (CHATTERINO_STATIC_QT_BUILD) + qt_import_plugins(${EXECUTABLE_PROJECT} INCLUDE_BY_TYPE + platforms Qt::QXcbIntegrationPlugin + Qt::QMinimalIntegrationPlugin + ) + endif () + + target_include_directories(${EXECUTABLE_PROJECT} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_BINARY_DIR}/autogen/) target_link_libraries(${EXECUTABLE_PROJECT} PUBLIC ${LIBRARY_PROJECT}) - set_target_properties(${EXECUTABLE_PROJECT} - PROPERTIES - ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" - LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" - RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}/bin" - RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}/bin" - RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO "${CMAKE_BINARY_DIR}/bin" - ) + set_target_directory_hierarchy(${EXECUTABLE_PROJECT}) - if (MSVC) - get_target_property(Qt_Core_Location Qt${MAJOR_QT_VERSION}::Core LOCATION) - get_filename_component(QT_BIN_DIR ${Qt_Core_Location} DIRECTORY) - set(WINDEPLOYQT_COMMAND "${QT_BIN_DIR}/windeployqt.exe" $ --release --no-compiler-runtime --no-translations --no-opengl-sw) + if (WIN32) + if (WINDEPLOYQT_PATH) + file(TO_CMAKE_PATH "${WINDEPLOYQT_PATH}" WINDEPLOYQT_PATH) + else() + if (VCPKG_INSTALLED_DIR AND (CMAKE_BUILD_TYPE STREQUAL "Debug")) + find_program(WINDEPLOYQT_PATH NAMES windeployqt.debug.bat) + else() + find_program(WINDEPLOYQT_PATH NAMES windeployqt) + endif() + endif() - install(TARGETS ${EXECUTABLE_PROJECT} - RUNTIME DESTINATION . - ) + if (NOT EXISTS ${WINDEPLOYQT_PATH}) + message(FATAL_ERROR "windeployqt.exe not found") + endif() - install(CODE "execute_process(COMMAND ${WINDEPLOYQT_COMMAND} --dir \${CMAKE_INSTALL_PREFIX})") + if (CMAKE_BUILD_TYPE STREQUAL "Debug") + set(WINDEPLOYQT_MODE --debug) + get_target_property(QT_CORE_LOC Qt${MAJOR_QT_VERSION}::Core LOCATION_DEBUG) + else() + set(WINDEPLOYQT_MODE --release) + get_target_property(QT_CORE_LOC Qt${MAJOR_QT_VERSION}::Core LOCATION) + endif() + get_filename_component(QT_BIN_DIR ${QT_CORE_LOC} DIRECTORY) + + # This assumes the installed CRT is up-to-date (see .CI/deploy-crt.ps1) + set(WINDEPLOYQT_COMMAND_ARGV "${WINDEPLOYQT_PATH}" "$" ${WINDEPLOYQT_MODE} --no-compiler-runtime --no-translations --no-opengl-sw) + string(REPLACE ";" " " WINDEPLOYQT_COMMAND "${WINDEPLOYQT_COMMAND_ARGV}") + + install(TARGETS ${EXECUTABLE_PROJECT} + RUNTIME_DEPENDENCIES + PRE_EXCLUDE_REGEXES "api-ms-" "ext-ms-" + POST_EXCLUDE_REGEXES ".*system32/.*\\.dll" + DIRECTORIES ${QT_BIN_DIR} + RUNTIME DESTINATION .) + + # Hardcoded list of DLLs to install from Qt - these are marked as optional since they only exist for vcpkg + install(FILES + ${QT_BIN_DIR}/jpeg62.dll + ${QT_BIN_DIR}/libwebpdemux.dll + ${QT_BIN_DIR}/libwebpmux.dll + ${QT_BIN_DIR}/libwebp.dll + ${QT_BIN_DIR}/libsharpyuv.dll + DESTINATION . + OPTIONAL) + + install(CODE "message(\"-- Running: ${WINDEPLOYQT_COMMAND} --dir \\\"\${CMAKE_INSTALL_PREFIX}\\\"\")") + install(CODE "execute_process(COMMAND ${WINDEPLOYQT_COMMAND} --dir \"\${CMAKE_INSTALL_PREFIX}\" COMMAND_ERROR_IS_FATAL ANY)") elseif (APPLE) install(TARGETS ${EXECUTABLE_PROJECT} RUNTIME DESTINATION bin @@ -616,6 +898,12 @@ if (BUILD_APP) DESTINATION share/icons/hicolor/256x256/apps ) endif () + + if(CHATTERINO_ENABLE_LTO) + message(STATUS "Enabling LTO for ${EXECUTABLE_PROJECT}") + set_property(TARGET ${EXECUTABLE_PROJECT} + PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE) + endif() endif () if (USE_PRECOMPILED_HEADERS) @@ -633,35 +921,40 @@ set_target_properties(${LIBRARY_PROJECT} AUTOUIC ON ) -# Used to provide a date of build in the About page (for nightly builds). Getting the actual time of -# compilation in CMake is a more involved, as documented in https://stackoverflow.com/q/24292898. -# For CI runs, however, the date of build file generation should be consistent with the date of -# compilation so this approximation is "good enough" for our purpose. -if (DEFINED ENV{CHATTERINO_SKIP_DATE_GEN}) - set(cmake_gen_date "1970-01-01") -else () - string(TIMESTAMP cmake_gen_date "%Y-%m-%d") -endif () +# The version project has definitions about the build. +# To avoid recompilations because of changing preprocessor definitions, +# this is its own project. +set(VERSION_SOURCE_FILES common/Version.cpp common/Version.hpp) +add_library(${VERSION_PROJECT} STATIC ${VERSION_SOURCE_FILES}) +target_compile_definitions(${VERSION_PROJECT} PRIVATE + $<$:USEWINSDK> + $<$:CHATTERINO_WITH_CRASHPAD> +) + +# source group for IDEs +source_group(TREE ${CMAKE_SOURCE_DIR} FILES ${VERSION_SOURCE_FILES}) +target_include_directories(${VERSION_PROJECT} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) +target_link_libraries(${VERSION_PROJECT} PRIVATE Qt${MAJOR_QT_VERSION}::Core) +target_compile_definitions(${VERSION_PROJECT} PRIVATE + CHATTERINO_GIT_HASH=\"${GIT_HASH}\" + CHATTERINO_GIT_RELEASE=\"${GIT_RELEASE}\" + CHATTERINO_GIT_COMMIT=\"${GIT_COMMIT}\" + CHATTERINO_GIT_MODIFIED=${GIT_MODIFIED} + + CHATTERINO_CMAKE_GEN_DATE=\"${cmake_gen_date}\" +) + +target_link_libraries(${LIBRARY_PROJECT} PRIVATE ${VERSION_PROJECT}) target_compile_definitions(${LIBRARY_PROJECT} PUBLIC CHATTERINO UNICODE - AB_CUSTOM_THEME AB_CUSTOM_SETTINGS IRC_STATIC IRC_NAMESPACE=Communi - - CHATTERINO_GIT_HASH=\"${GIT_HASH}\" - CHATTERINO_GIT_RELEASE=\"${GIT_RELEASE}\" - CHATTERINO_GIT_COMMIT=\"${GIT_COMMIT}\" - - CHATTERINO_CMAKE_GEN_DATE=\"${cmake_gen_date}\" + $<$:_WIN32_WINNT=0x0A00> # Windows 10 ) -if (GIT_MODIFIED) - target_compile_definitions(${LIBRARY_PROJECT} PUBLIC - CHATTERINO_GIT_MODIFIED - ) -endif () + if (USE_SYSTEM_QTKEYCHAIN) target_compile_definitions(${LIBRARY_PROJECT} PUBLIC CMAKE_BUILD @@ -675,9 +968,12 @@ if (WIN32) set_target_properties(${EXECUTABLE_PROJECT} PROPERTIES WIN32_EXECUTABLE TRUE) endif () endif () +if (CHATTERINO_DEBUG_NATIVE_MESSAGES) + target_compile_definitions(${LIBRARY_PROJECT} PRIVATE CHATTERINO_DEBUG_NM) +endif () if (MSVC) - target_compile_options(${LIBRARY_PROJECT} PUBLIC /EHsc /bigobj) + target_compile_options(${LIBRARY_PROJECT} PUBLIC /EHsc /bigobj /utf-8) endif () if (APPLE AND BUILD_APP) @@ -686,7 +982,6 @@ if (APPLE AND BUILD_APP) PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "Chatterino" MACOSX_BUNDLE_GUI_IDENTIFIER "com.chatterino" - MACOSX_BUNDLE_INFO_STRING "Chat client for Twitch" MACOSX_BUNDLE_LONG_VERSION_STRING "${PROJECT_VERSION}" MACOSX_BUNDLE_SHORT_VERSION_STRING "${PROJECT_VERSION}" MACOSX_BUNDLE_BUNDLE_VERSION "${PROJECT_VERSION}" @@ -694,38 +989,50 @@ if (APPLE AND BUILD_APP) ) endif () -target_include_directories(${LIBRARY_PROJECT} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_include_directories(${LIBRARY_PROJECT} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_BINARY_DIR}/autogen/) -if (WinToast_FOUND) +# semver dependency https://github.com/Neargye/semver +target_include_directories(${LIBRARY_PROJECT} PUBLIC ${CMAKE_SOURCE_DIR}/lib/semver/include) + +# expected-lite dependency https://github.com/martinmoene/expected-lite +target_include_directories(${LIBRARY_PROJECT} PUBLIC ${CMAKE_SOURCE_DIR}/lib/expected-lite/include) + +# miniaudio dependency https://github.com/mackron/miniaudio +if (USE_SYSTEM_MINIAUDIO) + message(STATUS "Building with system miniaudio") + include(CheckIncludeFileCXX) + CHECK_INCLUDE_FILE_CXX("miniaudio.h" MINIAUDIO_FOUND) + if (NOT MINIAUDIO_FOUND) + message(FATAL_ERROR "miniaudio.h not found on your system") + endif() +else () + target_include_directories(${LIBRARY_PROJECT} PUBLIC ${CMAKE_SOURCE_DIR}/lib/miniaudio) +endif () + +if (UNIX) + if (CMAKE_DL_LIBS) + # libdl is a requirement for miniaudio on Linux + message(STATUS "Linking with CMake DL libs: '${CMAKE_DL_LIBS}'") + target_link_libraries(${LIBRARY_PROJECT} PUBLIC ${CMAKE_DL_LIBS}) + endif () +endif () + +if (WIN32) target_link_libraries(${LIBRARY_PROJECT} PUBLIC WinToast) endif () -if (USE_CONAN AND TARGET CONAN_PKG::boost) - target_link_libraries(${LIBRARY_PROJECT} - PUBLIC - CONAN_PKG::boost - ) -else () - target_link_libraries(${LIBRARY_PROJECT} +target_link_libraries(${LIBRARY_PROJECT} PUBLIC ${Boost_LIBRARIES} ) -endif () -if (USE_CONAN AND TARGET CONAN_PKG::openssl) - target_link_libraries(${LIBRARY_PROJECT} - PUBLIC - CONAN_PKG::openssl - ) -else () - target_link_libraries(${LIBRARY_PROJECT} +target_link_libraries(${LIBRARY_PROJECT} PUBLIC OpenSSL::SSL OpenSSL::Crypto ) -endif () target_include_directories(${LIBRARY_PROJECT} PUBLIC ${RapidJSON_INCLUDE_DIRS}) @@ -736,30 +1043,61 @@ if (LIBRT) ) endif () +if (BUILD_WITH_CRASHPAD) + target_compile_definitions(${LIBRARY_PROJECT} PUBLIC CHATTERINO_WITH_CRASHPAD) + target_link_libraries(${LIBRARY_PROJECT} PUBLIC crashpad::client) +endif() + # Configure compiler warnings if (MSVC) - # 4714 - function marked as __forceinline not inlined - # 4996 - occurs when the compiler encounters a function or variable that is marked as deprecated. - # These functions may have a different preferred name, may be insecure or have - # a more secure variant, or may be obsolete. - # 4505 - unreferenced local version has been removed - # 4127 - conditional expression is constant - # 4503 - decorated name length exceeded, name was truncated - # 4100 - unreferences formal parameter - # 4305 - possible truncation of data - # 4267 - possible loss of data in return + # Change flags for RelWithDebInfo + + # Default: "/debug /INCREMENTAL" + # Changes: + # - Disable incremental linking to reduce padding + # - Enable all optimizations - by default when /DEBUG is specified, + # these optimizations will be disabled. We need /DEBUG to generate a PDB. + # See https://gitlab.kitware.com/cmake/cmake/-/issues/20812 for more details. + set(CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO "/DEBUG /INCREMENTAL:NO /OPT:REF,ICF,LBR") + + # Use the function inlining level from 'Release' mode (2). + string(REPLACE "/Ob1" "/Ob2" CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO}") + + # Configure warnings + + # Someone adds /W3 before we add /W4. + # This makes sure, only /W4 is specified. + string(REPLACE "/W3" "/W4" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") + # 4100 - "unreferenced formal parameter" + # There are a lot of functions and methods where + # an argument was given a name but never used. + # There's a clang-tidy rule that will catch this + # for new/updated functions/methods. + # + # 4267 - "possible loss of data in return" + # These are implicit conversions from size_t to int/qsizetype. + # We don't use size_t in a lot of cases, since + # Qt doesn't use it - it uses int (or qsizetype in Qt6). + # + # 4458 - "declaration of 'identifier' hides class member" + # We have a rule of exclusively using `this->` + # to access class members, thus it's fine to reclare a variable + # with the same name as a class member. target_compile_options(${LIBRARY_PROJECT} PUBLIC /W4 - # Disable the following warnings - /wd4714 - /wd4996 - /wd4505 - /wd4127 - /wd4503 + # 5038 - warnings about initialization order + /w15038 + # 4855 - implicit capture of 'this' via '[=]' is deprecated + /w14855 + # Disable the following warnings (see reasoning above) /wd4100 - /wd4305 /wd4267 + /wd4458 + # Enable updated '__cplusplus' macro - workaround for CMake#18837 + /Zc:__cplusplus ) + # Disable min/max macros from Windows.h + target_compile_definitions(${LIBRARY_PROJECT} PUBLIC NOMINMAX) else () target_compile_options(${LIBRARY_PROJECT} PUBLIC -Wall @@ -768,18 +1106,22 @@ else () -Wno-switch -Wno-deprecated-declarations -Wno-sign-compare - -Wno-unused-variable # Disabling strict-aliasing warnings for now, although we probably want to re-enable this in the future -Wno-strict-aliasing -Werror=return-type + -Werror=reorder ) if (CMAKE_CXX_COMPILER_ID MATCHES "Clang") target_compile_options(${LIBRARY_PROJECT} PUBLIC -Wno-unused-local-typedef -Wno-unused-private-field + -Werror=inconsistent-missing-override + -Werror=final-dtor-non-final-class + -Werror=ambiguous-reversed-operator + ) else () target_compile_options(${LIBRARY_PROJECT} PUBLIC @@ -787,3 +1129,25 @@ else () ) endif() endif () + +if(CHATTERINO_ENABLE_LTO) + message(STATUS "Enabling LTO for ${LIBRARY_PROJECT}") + set_property(TARGET ${LIBRARY_PROJECT} + PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE) +endif() + +if(NOT CHATTERINO_UPDATER) + message(STATUS "Disabling the updater.") + target_compile_definitions(${LIBRARY_PROJECT} PUBLIC CHATTERINO_DISABLE_UPDATER) +endif() + +if (DOXYGEN_FOUND) + message(STATUS "Doxygen found, adding doxygen target") + # output will be in docs/html + set(DOXYGEN_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/docs") + + doxygen_add_docs( + doxygen + ${CMAKE_CURRENT_LIST_DIR} + ) +endif () diff --git a/src/PrecompiledHeader.hpp b/src/PrecompiledHeader.hpp index fea276d09..5a66ec73f 100644 --- a/src/PrecompiledHeader.hpp +++ b/src/PrecompiledHeader.hpp @@ -1,45 +1,40 @@ #ifdef __cplusplus -# include -# include -# include -# include -# include +# include +# include +# include +# include +# include +# include # include +# include +# include +# include +# include +# include # include -# include # include # include # include # include -# include # include # include # include # include # include -# include -# include # include # include # include # include # include # include -# include -# include -# include # include -# include # include # include # include # include # include -# include -# include # include # include -# include # include # include # include @@ -52,13 +47,11 @@ # include # include # include -# include # include # include # include # include # include -# include # include # include # include @@ -69,9 +62,9 @@ # include # include # include -# include # include # include +# include # include # include # include @@ -87,49 +80,35 @@ # include # include # include -# include # include # include # include # include # include # include +# include # include # include # include # include # include # include -# include # include +# include # include # include # include -# include -# include -# include -# include -# include -# include -# include -# include -# include -# include -# include -# include -# include -# include -# include +# include +# include +# include + # include -# include -# include -# include -# include # include # include # include # include # include +# include # include # include # include @@ -138,11 +117,7 @@ # include # include # include -# include -# include -# include -# include -# include +# include # include # include # include diff --git a/src/RunGui.cpp b/src/RunGui.cpp index 5c4ea9f20..ff77d6c47 100644 --- a/src/RunGui.cpp +++ b/src/RunGui.cpp @@ -1,24 +1,27 @@ #include "RunGui.hpp" +#include "Application.hpp" +#include "common/Args.hpp" +#include "common/Modes.hpp" +#include "common/network/NetworkManager.hpp" +#include "common/QLogging.hpp" +#include "singletons/CrashHandler.hpp" +#include "singletons/Paths.hpp" +#include "singletons/Resources.hpp" +#include "singletons/Settings.hpp" +#include "singletons/Updates.hpp" +#include "util/CombinePath.hpp" +#include "widgets/dialogs/LastRunCrashDialog.hpp" + #include #include #include #include #include #include -#include -#include "Application.hpp" -#include "common/Args.hpp" -#include "common/Modes.hpp" -#include "common/NetworkManager.hpp" -#include "common/QLogging.hpp" -#include "singletons/Paths.hpp" -#include "singletons/Resources.hpp" -#include "singletons/Settings.hpp" -#include "singletons/Updates.hpp" -#include "util/CombinePath.hpp" -#include "widgets/dialogs/LastRunCrashDialog.hpp" +#include +#include #ifdef USEWINSDK # include "util/WindowsHelper.hpp" @@ -38,69 +41,73 @@ namespace { { // borrowed from // https://stackoverflow.com/questions/15035767/is-the-qt-5-dark-fusion-theme-available-for-windows - auto dark = qApp->palette(); + auto dark = QApplication::palette(); dark.setColor(QPalette::Window, QColor(22, 22, 22)); dark.setColor(QPalette::WindowText, Qt::white); dark.setColor(QPalette::Text, Qt::white); - dark.setColor(QPalette::Disabled, QPalette::WindowText, - QColor(127, 127, 127)); dark.setColor(QPalette::Base, QColor("#333")); dark.setColor(QPalette::AlternateBase, QColor("#444")); dark.setColor(QPalette::ToolTipBase, Qt::white); - dark.setColor(QPalette::ToolTipText, Qt::white); - dark.setColor(QPalette::Disabled, QPalette::Text, - QColor(127, 127, 127)); + dark.setColor(QPalette::ToolTipText, Qt::black); dark.setColor(QPalette::Dark, QColor(35, 35, 35)); dark.setColor(QPalette::Shadow, QColor(20, 20, 20)); dark.setColor(QPalette::Button, QColor(70, 70, 70)); dark.setColor(QPalette::ButtonText, Qt::white); - dark.setColor(QPalette::Disabled, QPalette::ButtonText, - QColor(127, 127, 127)); dark.setColor(QPalette::BrightText, Qt::red); dark.setColor(QPalette::Link, QColor(42, 130, 218)); dark.setColor(QPalette::Highlight, QColor(42, 130, 218)); + dark.setColor(QPalette::HighlightedText, Qt::white); + dark.setColor(QPalette::PlaceholderText, QColor(127, 127, 127)); + dark.setColor(QPalette::Disabled, QPalette::Highlight, QColor(80, 80, 80)); - dark.setColor(QPalette::HighlightedText, Qt::white); dark.setColor(QPalette::Disabled, QPalette::HighlightedText, QColor(127, 127, 127)); + dark.setColor(QPalette::Disabled, QPalette::ButtonText, + QColor(127, 127, 127)); + dark.setColor(QPalette::Disabled, QPalette::Text, + QColor(127, 127, 127)); + dark.setColor(QPalette::Disabled, QPalette::WindowText, + QColor(127, 127, 127)); - qApp->setPalette(dark); + QApplication::setPalette(dark); } void initQt() { // set up the QApplication flags QApplication::setAttribute(Qt::AA_Use96Dpi, true); + #ifdef Q_OS_WIN32 - QApplication::setAttribute(Qt::AA_DisableHighDpiScaling, true); + // Avoid promoting child widgets to child windows + // This causes bugs with frameless windows as not all child events + // get sent to the parent - effectively making the window immovable. + QApplication::setAttribute(Qt::AA_DontCreateNativeWidgetSiblings); #endif QApplication::setStyle(QStyleFactory::create("Fusion")); +#ifndef Q_OS_MAC QApplication::setWindowIcon(QIcon(":/icon.ico")); +#endif + +#ifdef Q_OS_MAC + // On the Mac/Cocoa platform this attribute is enabled by default + // We override it to ensure shortcuts show in context menus on that platform + QApplication::setAttribute(Qt::AA_DontShowShortcutsInContextMenus, + false); +#endif installCustomPalette(); } - void showLastCrashDialog() + void showLastCrashDialog(const Args &args, const Paths &paths) { - //#ifndef C_DISABLE_CRASH_DIALOG - // LastRunCrashDialog dialog; - - // switch (dialog.exec()) - // { - // case QDialog::Accepted: - // { - // }; - // break; - // default: - // { - // _exit(0); - // } - // } - //#endif + auto *dialog = new LastRunCrashDialog(args, paths); + // Use exec() over open() to block the app from being loaded + // and to be able to set the safe mode. + dialog->exec(); } void createRunningFile(const QString &path) @@ -118,14 +125,13 @@ namespace { } std::chrono::steady_clock::time_point signalsInitTime; - bool restartOnSignal = false; [[noreturn]] void handleSignal(int signum) { using namespace std::chrono_literals; - if (restartOnSignal && - std::chrono::steady_clock::now() - signalsInitTime > 30s) + if (std::chrono::steady_clock::now() - signalsInitTime > 30s && + getApp()->getCrashHandler()->shouldRecover()) { QProcess proc; @@ -161,7 +167,7 @@ namespace { // true. void initSignalHandler() { -#ifdef NDEBUG +#if defined(NDEBUG) && !defined(CHATTERINO_WITH_CRASHPAD) signalsInitTime = std::chrono::steady_clock::now(); signal(SIGSEGV, handleSignal); @@ -172,85 +178,95 @@ namespace { // improved in the future. void clearCache(const QDir &dir) { - int deletedCount = 0; - for (auto &&info : dir.entryInfoList(QDir::Files)) + size_t deletedCount = 0; + for (const auto &info : dir.entryInfoList(QDir::Files)) { if (info.lastModified().addDays(14) < QDateTime::currentDateTime()) { bool res = QFile(info.absoluteFilePath()).remove(); if (res) + { ++deletedCount; + } } } - qCDebug(chatterinoCache) << "Deleted" << deletedCount << "files"; + qCDebug(chatterinoCache) + << "Deleted" << deletedCount << "files in" << dir.path(); + } + + // We delete all but the five most recent crashdumps. This strategy may be + // improved in the future. + void clearCrashes(QDir dir) + { + // crashpad crashdumps are stored inside the Crashes/report directory + if (!dir.cd("reports")) + { + // no reports directory exists = no files to delete + return; + } + + dir.setNameFilters({"*.dmp"}); + + size_t deletedCount = 0; + // TODO: use std::views::drop once supported by all compilers + size_t filesToSkip = 5; + for (auto &&info : dir.entryInfoList(QDir::Files, QDir::Time)) + { + if (filesToSkip > 0) + { + filesToSkip--; + continue; + } + + if (QFile(info.absoluteFilePath()).remove()) + { + deletedCount++; + } + } + qCDebug(chatterinoApp) << "Deleted" << deletedCount << "crashdumps"; } } // namespace -void runGui(QApplication &a, Paths &paths, Settings &settings) +void runGui(QApplication &a, const Paths &paths, Settings &settings, + const Args &args, Updates &updates) { initQt(); initResources(); initSignalHandler(); - settings.restartOnCrash.connect([](const bool &value) { - restartOnSignal = value; - }); +#ifdef Q_OS_WIN + if (args.crashRecovery) + { + showLastCrashDialog(args, paths); + } +#endif - auto thread = std::thread([dir = paths.miscDirectory] { - { - auto path = combinePath(dir, "Update.exe"); - if (QFile::exists(path)) - { - QFile::remove(path); - } - } - { - auto path = combinePath(dir, "update.zip"); - if (QFile::exists(path)) - { - QFile::remove(path); - } - } - }); + updates.deleteOldFiles(); // Clear the cache 1 minute after start. - QTimer::singleShot(60 * 1000, [cachePath = paths.cacheDirectory()] { - QtConcurrent::run([cachePath]() { + QTimer::singleShot(60 * 1000, [cachePath = paths.cacheDirectory(), + crashDirectory = paths.crashdumpDirectory, + avatarPath = paths.twitchProfileAvatars] { + std::ignore = QtConcurrent::run([cachePath] { clearCache(cachePath); }); + std::ignore = QtConcurrent::run([avatarPath] { + clearCache(avatarPath); + }); + std::ignore = QtConcurrent::run([crashDirectory] { + clearCrashes(crashDirectory); + }); }); chatterino::NetworkManager::init(); - chatterino::Updates::instance().checkForUpdates(); + updates.checkForUpdates(); -#ifdef C_USE_BREAKPAD - QBreakpadInstance.setDumpPath(getPaths()->settingsFolderPath + "/Crashes"); -#endif - - // Running file - auto runningPath = - paths.miscDirectory + "/running_" + paths.applicationFilePathHash; - - if (QFile::exists(runningPath)) - { - showLastCrashDialog(); - } - else - { - createRunningFile(runningPath); - } - - Application app(settings, paths); + Application app(settings, paths, args, updates); app.initialize(settings, paths); - app.run(a); + app.run(); app.save(); - removeRunningFile(runningPath); - - if (!getArgs().dontSaveSettings) - { - pajlada::Settings::SettingManager::gSave(); - } + settings.requestSave(); chatterino::NetworkManager::deinit(); @@ -258,7 +274,6 @@ void runGui(QApplication &a, Paths &paths, Settings &settings) // flushing windows clipboard to keep copied messages flushClipboard(); #endif - - _exit(0); } + } // namespace chatterino diff --git a/src/RunGui.hpp b/src/RunGui.hpp index 338164404..daf4cf155 100644 --- a/src/RunGui.hpp +++ b/src/RunGui.hpp @@ -3,8 +3,13 @@ class QApplication; namespace chatterino { + +class Args; class Paths; class Settings; +class Updates; + +void runGui(QApplication &a, const Paths &paths, Settings &settings, + const Args &args, Updates &updates); -void runGui(QApplication &a, Paths &paths, Settings &settings); } // namespace chatterino diff --git a/src/autogenerated/ResourcesAutogen.cpp b/src/autogenerated/ResourcesAutogen.cpp deleted file mode 100644 index 9661241da..000000000 --- a/src/autogenerated/ResourcesAutogen.cpp +++ /dev/null @@ -1,75 +0,0 @@ -#include "ResourcesAutogen.hpp" - -namespace chatterino { - -Resources2::Resources2() -{ - this->avatars._1xelerate = QPixmap(":/avatars/_1xelerate.png"); - this->avatars.alazymeme = QPixmap(":/avatars/alazymeme.png"); - this->avatars.brian6932 = QPixmap(":/avatars/brian6932.png"); - this->avatars.fourtf = QPixmap(":/avatars/fourtf.png"); - this->avatars.hicupalot = QPixmap(":/avatars/hicupalot.png"); - this->avatars.iprodigy = QPixmap(":/avatars/iprodigy.png"); - this->avatars.jaxkey = QPixmap(":/avatars/jaxkey.png"); - this->avatars.kararty = QPixmap(":/avatars/kararty.png"); - this->avatars.karlpolice = QPixmap(":/avatars/karlpolice.png"); - this->avatars.mm2pl = QPixmap(":/avatars/mm2pl.png"); - this->avatars.pajlada = QPixmap(":/avatars/pajlada.png"); - this->avatars.slch = QPixmap(":/avatars/slch.png"); - this->avatars.xheaveny = QPixmap(":/avatars/xheaveny.png"); - this->avatars.zneix = QPixmap(":/avatars/zneix.png"); - this->buttons.addSplit = QPixmap(":/buttons/addSplit.png"); - this->buttons.addSplitDark = QPixmap(":/buttons/addSplitDark.png"); - this->buttons.ban = QPixmap(":/buttons/ban.png"); - this->buttons.banRed = QPixmap(":/buttons/banRed.png"); - this->buttons.clearSearch = QPixmap(":/buttons/clearSearch.png"); - this->buttons.copyDark = QPixmap(":/buttons/copyDark.png"); - this->buttons.copyLight = QPixmap(":/buttons/copyLight.png"); - this->buttons.menuDark = QPixmap(":/buttons/menuDark.png"); - this->buttons.menuLight = QPixmap(":/buttons/menuLight.png"); - this->buttons.mod = QPixmap(":/buttons/mod.png"); - this->buttons.modModeDisabled = QPixmap(":/buttons/modModeDisabled.png"); - this->buttons.modModeDisabled2 = QPixmap(":/buttons/modModeDisabled2.png"); - this->buttons.modModeEnabled = QPixmap(":/buttons/modModeEnabled.png"); - this->buttons.modModeEnabled2 = QPixmap(":/buttons/modModeEnabled2.png"); - this->buttons.replyDark = QPixmap(":/buttons/replyDark.png"); - this->buttons.replyThreadDark = QPixmap(":/buttons/replyThreadDark.png"); - this->buttons.search = QPixmap(":/buttons/search.png"); - this->buttons.timeout = QPixmap(":/buttons/timeout.png"); - this->buttons.trashCan = QPixmap(":/buttons/trashCan.png"); - this->buttons.unban = QPixmap(":/buttons/unban.png"); - this->buttons.unmod = QPixmap(":/buttons/unmod.png"); - this->buttons.unvip = QPixmap(":/buttons/unvip.png"); - this->buttons.update = QPixmap(":/buttons/update.png"); - this->buttons.updateError = QPixmap(":/buttons/updateError.png"); - this->buttons.viewersDark = QPixmap(":/buttons/viewersDark.png"); - this->buttons.viewersLight = QPixmap(":/buttons/viewersLight.png"); - this->buttons.vip = QPixmap(":/buttons/vip.png"); - this->error = QPixmap(":/error.png"); - this->icon = QPixmap(":/icon.png"); - this->pajaDank = QPixmap(":/pajaDank.png"); - this->scrolling.downScroll = QPixmap(":/scrolling/downScroll.png"); - this->scrolling.neutralScroll = QPixmap(":/scrolling/neutralScroll.png"); - this->scrolling.upScroll = QPixmap(":/scrolling/upScroll.png"); - this->settings.aboutlogo = QPixmap(":/settings/aboutlogo.png"); - this->split.down = QPixmap(":/split/down.png"); - this->split.left = QPixmap(":/split/left.png"); - this->split.move = QPixmap(":/split/move.png"); - this->split.right = QPixmap(":/split/right.png"); - this->split.up = QPixmap(":/split/up.png"); - this->streamerMode = QPixmap(":/streamerMode.png"); - this->twitch.admin = QPixmap(":/twitch/admin.png"); - this->twitch.automod = QPixmap(":/twitch/automod.png"); - this->twitch.broadcaster = QPixmap(":/twitch/broadcaster.png"); - this->twitch.cheer1 = QPixmap(":/twitch/cheer1.png"); - this->twitch.globalmod = QPixmap(":/twitch/globalmod.png"); - this->twitch.moderator = QPixmap(":/twitch/moderator.png"); - this->twitch.prime = QPixmap(":/twitch/prime.png"); - this->twitch.staff = QPixmap(":/twitch/staff.png"); - this->twitch.subscriber = QPixmap(":/twitch/subscriber.png"); - this->twitch.turbo = QPixmap(":/twitch/turbo.png"); - this->twitch.verified = QPixmap(":/twitch/verified.png"); - this->twitch.vip = QPixmap(":/twitch/vip.png"); -} - -} // namespace chatterino diff --git a/src/autogenerated/ResourcesAutogen.hpp b/src/autogenerated/ResourcesAutogen.hpp deleted file mode 100644 index 7aed77a2b..000000000 --- a/src/autogenerated/ResourcesAutogen.hpp +++ /dev/null @@ -1,91 +0,0 @@ -#include -#include "common/Singleton.hpp" - -namespace chatterino { - -class Resources2 : public Singleton -{ -public: - Resources2(); - - struct { - QPixmap _1xelerate; - QPixmap alazymeme; - QPixmap brian6932; - QPixmap fourtf; - QPixmap hicupalot; - QPixmap iprodigy; - QPixmap jaxkey; - QPixmap kararty; - QPixmap karlpolice; - QPixmap mm2pl; - QPixmap pajlada; - QPixmap slch; - QPixmap xheaveny; - QPixmap zneix; - } avatars; - struct { - QPixmap addSplit; - QPixmap addSplitDark; - QPixmap ban; - QPixmap banRed; - QPixmap clearSearch; - QPixmap copyDark; - QPixmap copyLight; - QPixmap menuDark; - QPixmap menuLight; - QPixmap mod; - QPixmap modModeDisabled; - QPixmap modModeDisabled2; - QPixmap modModeEnabled; - QPixmap modModeEnabled2; - QPixmap replyDark; - QPixmap replyThreadDark; - QPixmap search; - QPixmap timeout; - QPixmap trashCan; - QPixmap unban; - QPixmap unmod; - QPixmap unvip; - QPixmap update; - QPixmap updateError; - QPixmap viewersDark; - QPixmap viewersLight; - QPixmap vip; - } buttons; - QPixmap error; - QPixmap icon; - QPixmap pajaDank; - struct { - QPixmap downScroll; - QPixmap neutralScroll; - QPixmap upScroll; - } scrolling; - struct { - QPixmap aboutlogo; - } settings; - struct { - QPixmap down; - QPixmap left; - QPixmap move; - QPixmap right; - QPixmap up; - } split; - QPixmap streamerMode; - struct { - QPixmap admin; - QPixmap automod; - QPixmap broadcaster; - QPixmap cheer1; - QPixmap globalmod; - QPixmap moderator; - QPixmap prime; - QPixmap staff; - QPixmap subscriber; - QPixmap turbo; - QPixmap verified; - QPixmap vip; - } twitch; -}; - -} // namespace chatterino diff --git a/src/common/Aliases.hpp b/src/common/Aliases.hpp index 36fbc94ff..847d807ac 100644 --- a/src/common/Aliases.hpp +++ b/src/common/Aliases.hpp @@ -2,6 +2,7 @@ #include #include + #include #define QStringAlias(name) \ diff --git a/src/common/Args.cpp b/src/common/Args.cpp index 2894c3f7b..eeebbfc20 100644 --- a/src/common/Args.cpp +++ b/src/common/Args.cpp @@ -1,6 +1,7 @@ -#include "Args.hpp" +#include "common/Args.hpp" #include "common/QLogging.hpp" +#include "debug/AssertInGuiThread.hpp" #include "singletons/Paths.hpp" #include "singletons/WindowManager.hpp" #include "util/AttachToConsole.hpp" @@ -14,44 +15,136 @@ #include #include +namespace { + +using namespace chatterino; + +template +QCommandLineOption hiddenOption(Args... args) +{ + QCommandLineOption opt(args...); + opt.setFlags(QCommandLineOption::HiddenFromHelp); + return opt; +} + +QStringList extractCommandLine( + const QCommandLineParser &parser, + std::initializer_list options) +{ + QStringList args; + for (const auto &option : options) + { + if (parser.isSet(option)) + { + auto optionName = option.names().first(); + if (optionName.length() == 1) + { + optionName.prepend(u'-'); + } + else + { + optionName.prepend("--"); + } + + auto values = parser.values(option); + if (values.empty()) + { + args += optionName; + } + else + { + for (const auto &value : values) + { + args += optionName; + args += value; + } + } + } + } + return args; +} + +std::optional parseActivateOption(QString input) +{ + auto colon = input.indexOf(u':'); + if (colon >= 0) + { + auto ty = input.left(colon); + if (ty != u"t") + { + qCWarning(chatterinoApp).nospace() + << "Failed to parse active channel (unknown type: " << ty + << ")"; + return std::nullopt; + } + + input = input.mid(colon + 1); + } + + return Args::Channel{ + .provider = ProviderId::Twitch, + .name = input, + }; +} + +} // namespace + namespace chatterino { -Args::Args(const QApplication &app) +Args::Args(const QApplication &app, const Paths &paths) { QCommandLineParser parser; parser.setApplicationDescription("Chatterino 2 Client for Twitch Chat"); parser.addHelpOption(); // Used internally by app to restart after unexpected crashes - QCommandLineOption crashRecoveryOption("crash-recovery"); - crashRecoveryOption.setFlags(QCommandLineOption::HiddenFromHelp); + auto crashRecoveryOption = hiddenOption("crash-recovery"); + auto exceptionCodeOption = hiddenOption("cr-exception-code", "", "code"); + auto exceptionMessageOption = + hiddenOption("cr-exception-message", "", "message"); // Added to ignore the parent-window option passed during native messaging - QCommandLineOption parentWindowOption("parent-window"); - parentWindowOption.setFlags(QCommandLineOption::HiddenFromHelp); - QCommandLineOption parentWindowIdOption("x-attach-split-to-window", "", - "window-id"); - parentWindowIdOption.setFlags(QCommandLineOption::HiddenFromHelp); + auto parentWindowOption = hiddenOption("parent-window"); + auto parentWindowIdOption = + hiddenOption("x-attach-split-to-window", "", "window-id"); // Verbose - QCommandLineOption verboseOption({{"v", "verbose"}, - "Attaches to the Console on windows, " - "allowing you to see debug output."}); - crashRecoveryOption.setFlags(QCommandLineOption::HiddenFromHelp); + auto verboseOption = QCommandLineOption( + QStringList{"v", "verbose"}, "Attaches to the Console on windows, " + "allowing you to see debug output."); + // Safe mode + QCommandLineOption safeModeOption( + "safe-mode", "Starts Chatterino without loading Plugins and always " + "show the settings button."); - parser.addOptions({ - {{"V", "version"}, "Displays version information."}, - crashRecoveryOption, - parentWindowOption, - parentWindowIdOption, - verboseOption, - }); - parser.addOption(QCommandLineOption( + // Channel layout + auto channelLayout = QCommandLineOption( {"c", "channels"}, "Joins only supplied channels on startup. Use letters with colons to " "specify platform. Only Twitch channels are supported at the moment.\n" "If platform isn't specified, default is Twitch.", - "t:channel1;t:channel2;...")); + "t:channel1;t:channel2;..."); + + QCommandLineOption activateOption( + {"a", "activate"}, + "Activate the tab with this channel or add one in the main " + "window.\nOnly Twitch is " + "supported at the moment (prefix: 't:').\nIf the platform isn't " + "specified, Twitch is assumed.", + "t:channel"); + + parser.addOptions({ + {{"V", "version"}, "Displays version information."}, + crashRecoveryOption, + exceptionCodeOption, + exceptionMessageOption, + parentWindowOption, + parentWindowIdOption, + verboseOption, + safeModeOption, + channelLayout, + activateOption, + }); if (!parser.parse(app.arguments())) { @@ -71,15 +164,25 @@ Args::Args(const QApplication &app) (args.size() > 0 && (args[0].startsWith("chrome-extension://") || args[0].endsWith(".json"))); - if (parser.isSet("c")) + if (parser.isSet(channelLayout)) { - this->applyCustomChannelLayout(parser.value("c")); + this->applyCustomChannelLayout(parser.value(channelLayout), paths); } this->verbose = parser.isSet(verboseOption); this->printVersion = parser.isSet("V"); - this->crashRecovery = parser.isSet("crash-recovery"); + + this->crashRecovery = parser.isSet(crashRecoveryOption); + if (parser.isSet(exceptionCodeOption)) + { + this->exceptionCode = + static_cast(parser.value(exceptionCodeOption).toULong()); + } + if (parser.isSet(exceptionMessageOption)) + { + this->exceptionMessage = parser.value(exceptionMessageOption); + } if (parser.isSet(parentWindowIdOption)) { @@ -89,9 +192,31 @@ Args::Args(const QApplication &app) this->parentWindowId = parser.value(parentWindowIdOption).toULongLong(); } + if (parser.isSet(safeModeOption)) + { + this->safeMode = true; + } + + if (parser.isSet(activateOption)) + { + this->activateChannel = + parseActivateOption(parser.value(activateOption)); + } + + this->currentArguments_ = extractCommandLine(parser, { + verboseOption, + safeModeOption, + channelLayout, + activateOption, + }); } -void Args::applyCustomChannelLayout(const QString &argValue) +QStringList Args::currentArguments() const +{ + return this->currentArguments_; +} + +void Args::applyCustomChannelLayout(const QString &argValue, const Paths &paths) { WindowLayout layout; WindowDescriptor window; @@ -103,10 +228,9 @@ void Args::applyCustomChannelLayout(const QString &argValue) window.type_ = WindowType::Main; // Load main window layout from config file so we can use the same geometry - const QRect configMainLayout = [] { - const QString windowLayoutFile = - combinePath(getPaths()->settingsDirectory, - WindowManager::WINDOW_LAYOUT_FILENAME); + const QRect configMainLayout = [paths] { + const QString windowLayoutFile = combinePath( + paths.settingsDirectory, WindowManager::WINDOW_LAYOUT_FILENAME); const WindowLayout configLayout = WindowLayout::loadFromFile(windowLayoutFile); @@ -114,7 +238,9 @@ void Args::applyCustomChannelLayout(const QString &argValue) for (const WindowDescriptor &window : configLayout.windows_) { if (window.type_ != WindowType::Main) + { continue; + } return window.geometry_; } @@ -128,7 +254,9 @@ void Args::applyCustomChannelLayout(const QString &argValue) for (const QString &channelArg : channelArgList) { if (channelArg.isEmpty()) + { continue; + } // Twitch is default platform QString platform = "t"; @@ -164,18 +292,4 @@ void Args::applyCustomChannelLayout(const QString &argValue) } } -static Args *instance = nullptr; - -void initArgs(const QApplication &app) -{ - instance = new Args(app); -} - -const Args &getArgs() -{ - assert(instance); - - return *instance; -} - } // namespace chatterino diff --git a/src/common/Args.hpp b/src/common/Args.hpp index 4087b336f..b3eeb20ff 100644 --- a/src/common/Args.hpp +++ b/src/common/Args.hpp @@ -1,35 +1,74 @@ #pragma once -#include -#include +#include "common/ProviderId.hpp" #include "common/WindowDescriptors.hpp" +#include + +#include + namespace chatterino { +class Paths; + /// Command line arguments passed to Chatterino. +/// +/// All accepted arguments: +/// +/// Crash recovery: +/// --crash-recovery +/// --cr-exception-code code +/// --cr-exception-message message +/// +/// Native messaging: +/// --parent-window +/// --x-attach-split-to-window=window-id +/// +/// -v, --verbose +/// -V, --version +/// -c, --channels=t:channel1;t:channel2;... +/// -a, --activate=t:channel +/// --safe-mode +/// +/// See documentation on `QGuiApplication` for documentation on Qt arguments like -platform. class Args { public: - Args(const QApplication &app); + struct Channel { + ProviderId provider; + QString name; + }; + + Args() = default; + Args(const QApplication &app, const Paths &paths); bool printVersion{}; + bool crashRecovery{}; + /// Native, platform-specific exception code from crashpad + std::optional exceptionCode{}; + /// Text version of the exception code. Potentially contains more context. + std::optional exceptionMessage{}; + bool shouldRunBrowserExtensionHost{}; // Shows a single chat. Used on windows to embed in another application. bool isFramelessEmbed{}; - boost::optional parentWindowId{}; + std::optional parentWindowId{}; // Not settings directly bool dontSaveSettings{}; bool dontLoadMainWindow{}; - boost::optional customChannelLayout; + std::optional customChannelLayout; + std::optional activateChannel; bool verbose{}; + bool safeMode{}; + + QStringList currentArguments() const; private: - void applyCustomChannelLayout(const QString &argValue); + void applyCustomChannelLayout(const QString &argValue, const Paths &paths); + + QStringList currentArguments_; }; -void initArgs(const QApplication &app); -const Args &getArgs(); - } // namespace chatterino diff --git a/src/common/Atomic.hpp b/src/common/Atomic.hpp index 86603c086..cb6686c5f 100644 --- a/src/common/Atomic.hpp +++ b/src/common/Atomic.hpp @@ -1,22 +1,28 @@ #pragma once -#include +#include +#include #include namespace chatterino { template -class Atomic : boost::noncopyable +class Atomic { public: - Atomic() + Atomic() = default; + ~Atomic() = default; + + Atomic(T &&val) + : value_(std::move(val)) { } - Atomic(T &&val) - : value_(val) - { - } + Atomic(const Atomic &) = delete; + Atomic &operator=(const Atomic &) = delete; + + Atomic(Atomic &&) = delete; + Atomic &operator=(Atomic &&) = delete; T get() const { @@ -44,4 +50,67 @@ private: T value_; }; +#if defined(__cpp_lib_atomic_shared_ptr) && defined(__cpp_concepts) + +template +class Atomic> +{ + // Atomic> must be instantated with a const T +}; + +template + requires std::is_const_v +class Atomic> +{ +public: + Atomic() = default; + ~Atomic() = default; + + Atomic(T &&val) + : value_(std::make_shared(std::move(val))) + { + } + + Atomic(std::shared_ptr &&val) + : value_(std::move(val)) + { + } + + Atomic(const Atomic &) = delete; + Atomic &operator=(const Atomic &) = delete; + + Atomic(Atomic &&) = delete; + Atomic &operator=(Atomic &&) = delete; + + std::shared_ptr get() const + { + return this->value_.load(); + } + + void set(const T &val) + { + this->value_.store(std::make_shared(val)); + } + + void set(T &&val) + { + this->value_.store(std::make_shared(std::move(val))); + } + + void set(const std::shared_ptr &val) + { + this->value_.store(val); + } + + void set(std::shared_ptr &&val) + { + this->value_.store(std::move(val)); + } + +private: + std::atomic> value_; +}; + +#endif + } // namespace chatterino diff --git a/src/common/Channel.cpp b/src/common/Channel.cpp index e47362572..b33726151 100644 --- a/src/common/Channel.cpp +++ b/src/common/Channel.cpp @@ -3,13 +3,12 @@ #include "Application.hpp" #include "messages/Message.hpp" #include "messages/MessageBuilder.hpp" -#include "providers/irc/IrcChannel2.hpp" -#include "providers/irc/IrcServer.hpp" #include "providers/twitch/IrcMessageHandler.hpp" #include "singletons/Emotes.hpp" #include "singletons/Logging.hpp" #include "singletons/Settings.hpp" #include "singletons/WindowManager.hpp" +#include "util/ChannelHelpers.hpp" #include #include @@ -25,11 +24,16 @@ namespace chatterino { // Channel // Channel::Channel(const QString &name, Type type) - : completionModel(*this) + : completionModel(new TabCompletionModel(*this, nullptr)) , lastDate_(QDate::currentDate()) , name_(name) + , messages_(getSettings()->scrollbackSplitLimit) , type_(type) { + if (this->isTwitchChannel()) + { + this->platform_ = "twitch"; + } } Channel::~Channel() @@ -77,130 +81,52 @@ LimitedQueueSnapshot Channel::getMessageSnapshot() return this->messages_.getSnapshot(); } -void Channel::addMessage(MessagePtr message, - boost::optional overridingFlags) +void Channel::addMessage(MessagePtr message, MessageContext context, + std::optional overridingFlags) { - auto app = getApp(); MessagePtr deleted; - if (!overridingFlags || !overridingFlags->has(MessageFlag::DoNotLog)) + if (context == MessageContext::Original) { - QString channelPlatform("other"); - if (this->type_ == Type::Irc) + // Only log original messages + auto isDoNotLogSet = + (overridingFlags && overridingFlags->has(MessageFlag::DoNotLog)) || + message->flags.has(MessageFlag::DoNotLog); + + if (!isDoNotLogSet) { - auto *irc = dynamic_cast(this); - if (irc != nullptr) - { - channelPlatform = QString("irc-%1").arg( - irc->server()->userFriendlyIdentifier()); - } + // Only log messages where the `DoNotLog` flag is not set + getApp()->getChatLogger()->addMessage(this->name_, message, + this->platform_, + this->getCurrentStreamID()); } - else if (this->isTwitchChannel()) - { - channelPlatform = "twitch"; - } - app->logging->addMessage(this->name_, message, channelPlatform); } if (this->messages_.pushBack(message, deleted)) { - this->messageRemovedFromStart.invoke(deleted); + this->messageRemovedFromStart(deleted); } this->messageAppended.invoke(message, overridingFlags); } +void Channel::addSystemMessage(const QString &contents) +{ + auto msg = makeSystemMessage(contents); + this->addMessage(msg, MessageContext::Original); +} + void Channel::addOrReplaceTimeout(MessagePtr message) { - LimitedQueueSnapshot snapshot = this->getMessageSnapshot(); - int snapshotLength = snapshot.size(); - - int end = std::max(0, snapshotLength - 20); - - bool addMessage = true; - - QTime minimumTime = QTime::currentTime().addSecs(-5); - - auto timeoutStackStyle = static_cast( - getSettings()->timeoutStackStyle.getValue()); - - for (int i = snapshotLength - 1; i >= end; --i) - { - auto &s = snapshot[i]; - - if (s->parseTime < minimumTime) - { - break; - } - - if (s->flags.has(MessageFlag::Untimeout) && - s->timeoutUser == message->timeoutUser) - { - break; - } - - if (timeoutStackStyle == TimeoutStackStyle::DontStackBeyondUserMessage) - { - if (s->loginName == message->timeoutUser && - s->flags.hasNone({MessageFlag::Disabled, MessageFlag::Timeout, - MessageFlag::Untimeout})) - { - break; - } - } - - if (s->flags.has(MessageFlag::Timeout) && - s->timeoutUser == message->timeoutUser) - { - if (message->flags.has(MessageFlag::PubSub) && - !s->flags.has(MessageFlag::PubSub)) - { - this->replaceMessage(s, message); - addMessage = false; - break; - } - if (!message->flags.has(MessageFlag::PubSub) && - s->flags.has(MessageFlag::PubSub)) - { - addMessage = timeoutStackStyle == TimeoutStackStyle::DontStack; - break; - } - - int count = s->count + 1; - - MessageBuilder replacement(timeoutMessage, message->timeoutUser, - message->loginName, message->searchText, - count); - - replacement->timeoutUser = message->timeoutUser; - replacement->count = count; - replacement->flags = message->flags; - - this->replaceMessage(s, replacement.release()); - - addMessage = false; - break; - } - } - - // disable the messages from the user - for (int i = 0; i < snapshotLength; i++) - { - auto &s = snapshot[i]; - if (s->loginName == message->timeoutUser && - s->flags.hasNone({MessageFlag::Timeout, MessageFlag::Untimeout, - MessageFlag::Whisper})) - { - // FOURTF: disabled for now - // PAJLADA: Shitty solution described in Message.hpp - s->flags.set(MessageFlag::Disabled); - } - } - - if (addMessage) - { - this->addMessage(message); - } + addOrReplaceChannelTimeout( + this->getMessageSnapshot(), std::move(message), QTime::currentTime(), + [this](auto /*idx*/, auto msg, auto replacement) { + this->replaceMessage(msg, replacement); + }, + [this](auto msg) { + this->addMessage(msg, MessageContext::Original); + }, + true); // XXX: Might need the following line // WindowManager::instance().repaintVisibleChatWidgets(this); @@ -212,7 +138,7 @@ void Channel::disableAllMessages() int snapshotLength = snapshot.size(); for (int i = 0; i < snapshotLength; i++) { - auto &message = snapshot[i]; + const auto &message = snapshot[i]; if (message->flags.hasAny({MessageFlag::System, MessageFlag::Timeout, MessageFlag::Whisper})) { @@ -256,7 +182,7 @@ void Channel::fillInMissingMessages(const std::vector &messages) existingMessageIds.reserve(snapshot.size()); // First, collect the ids of every message already present in the channel - for (auto &msg : snapshot) + for (const auto &msg : snapshot) { if (msg->flags.has(MessageFlag::System) || msg->id.isEmpty()) { @@ -273,7 +199,7 @@ void Channel::fillInMissingMessages(const std::vector &messages) // being able to insert just-loaded historical messages at the end // in the correct place. auto lastMsg = snapshot[snapshot.size() - 1]; - for (auto &msg : messages) + for (const auto &msg : messages) { // check if message already exists if (existingMessageIds.count(msg->id) != 0) @@ -285,7 +211,7 @@ void Channel::fillInMissingMessages(const std::vector &messages) anyInserted = true; bool insertedFlag = false; - for (auto &snapshotMsg : snapshot) + for (const auto &snapshotMsg : snapshot) { if (snapshotMsg->flags.has(MessageFlag::System)) { @@ -373,7 +299,8 @@ bool Channel::isWritable() const { using Type = Channel::Type; auto type = this->getType(); - return type != Type::TwitchMentions && type != Type::TwitchLive; + return type != Type::TwitchMentions && type != Type::TwitchLive && + type != Type::TwitchAutomod; } void Channel::sendMessage(const QString &message) @@ -392,7 +319,6 @@ bool Channel::isBroadcaster() const bool Channel::hasModRights() const { - // fourtf: check if staff return this->isMod() || this->isBroadcaster(); } @@ -406,9 +332,15 @@ bool Channel::isLive() const return false; } +bool Channel::isRerun() const +{ + return false; +} + bool Channel::shouldIgnoreHighlights() const { - return this->type_ == Type::TwitchMentions || + return this->type_ == Type::TwitchAutomod || + this->type_ == Type::TwitchMentions || this->type_ == Type::TwitchWhispers; } @@ -421,6 +353,11 @@ void Channel::reconnect() { } +QString Channel::getCurrentStreamID() const +{ + return {}; +} + std::shared_ptr Channel::getEmpty() { static std::shared_ptr channel(new Channel("", Type::None)); @@ -431,6 +368,10 @@ void Channel::onConnected() { } +void Channel::messageRemovedFromStart(const MessagePtr &msg) +{ +} + // // Indirect channel // diff --git a/src/common/Channel.hpp b/src/common/Channel.hpp index 8d6ef6557..67e715af5 100644 --- a/src/common/Channel.hpp +++ b/src/common/Channel.hpp @@ -1,23 +1,22 @@ #pragma once -#include "common/CompletionModel.hpp" -#include "common/FlagsEnum.hpp" +#include "controllers/completion/TabCompletionModel.hpp" #include "messages/LimitedQueue.hpp" +#include "messages/MessageFlag.hpp" +#include +#include #include #include #include -#include -#include #include +#include namespace chatterino { struct Message; using MessagePtr = std::shared_ptr; -enum class MessageFlag : uint32_t; -using MessageFlags = FlagsEnum; enum class TimeoutStackStyle : int { StackHard = 0, @@ -27,9 +26,21 @@ enum class TimeoutStackStyle : int { Default = DontStackBeyondUserMessage, }; +/// Context of the message being added to a channel +enum class MessageContext { + /// This message is the original + Original, + /// This message is a repost of a message that has already been added in a channel + Repost, +}; + class Channel : public std::enable_shared_from_this { public: + // This is for Lua. See scripts/make_luals_meta.py + /** + * @exposeenum c2.ChannelType + */ enum class Type { None, Direct, @@ -38,9 +49,9 @@ public: TwitchWatching, TwitchMentions, TwitchLive, + TwitchAutomod, TwitchEnd, - Irc, - Misc + Misc, }; explicit Channel(const QString &name, Type type); @@ -52,8 +63,7 @@ public: pajlada::Signals::Signal sendReplySignal; - pajlada::Signals::Signal messageRemovedFromStart; - pajlada::Signals::Signal> + pajlada::Signals::Signal> messageAppended; pajlada::Signals::Signal &> messagesAddedAtStart; pajlada::Signals::Signal messageReplaced; @@ -61,8 +71,6 @@ public: pajlada::Signals::Signal &> filledInMessages; pajlada::Signals::NoArgSignal destroyed; pajlada::Signals::NoArgSignal displayNameChanged; - /// Invoked when AbstractIrcServer::onReadConnected occurs - pajlada::Signals::NoArgSignal connected; Type getType() const; const QString &getName() const; @@ -76,11 +84,12 @@ public: // overridingFlags can be filled in with flags that should be used instead // of the message's flags. This is useful in case a flag is specific to a // type of split - void addMessage( - MessagePtr message, - boost::optional overridingFlags = boost::none); + void addMessage(MessagePtr message, MessageContext context, + std::optional overridingFlags = std::nullopt); void addMessagesAtStart(const std::vector &messages_); + void addSystemMessage(const QString &contents); + /// Inserts the given messages in order by Message::serverReceivedTime. void fillInMissingMessages(const std::vector &messages); @@ -103,17 +112,21 @@ public: virtual bool hasModRights() const; virtual bool hasHighRateLimit() const; virtual bool isLive() const; + virtual bool isRerun() const; virtual bool shouldIgnoreHighlights() const; virtual bool canReconnect() const; virtual void reconnect(); + virtual QString getCurrentStreamID() const; static std::shared_ptr getEmpty(); - CompletionModel completionModel; + TabCompletionModel *completionModel; QDate lastDate_; protected: virtual void onConnected(); + virtual void messageRemovedFromStart(const MessagePtr &msg); + QString platform_{"other"}; private: const QString name_; @@ -148,3 +161,30 @@ private: }; } // namespace chatterino + +template <> +constexpr magic_enum::customize::customize_t + magic_enum::customize::enum_name( + chatterino::Channel::Type value) noexcept +{ + using Type = chatterino::Channel::Type; + switch (value) + { + case Type::Twitch: + return "twitch"; + case Type::TwitchWhispers: + return "whispers"; + case Type::TwitchWatching: + return "watching"; + case Type::TwitchMentions: + return "mentions"; + case Type::TwitchLive: + return "live"; + case Type::TwitchAutomod: + return "automod"; + case Type::Misc: + return "misc"; + default: + return default_tag; + } +} diff --git a/src/common/ChannelChatters.cpp b/src/common/ChannelChatters.cpp index 3f4f10420..575b90302 100644 --- a/src/common/ChannelChatters.cpp +++ b/src/common/ChannelChatters.cpp @@ -1,8 +1,10 @@ -#include "ChannelChatters.hpp" +#include "common/ChannelChatters.hpp" +#include "common/Channel.hpp" #include "messages/Message.hpp" #include "messages/MessageBuilder.hpp" -#include "providers/twitch/TwitchMessageBuilder.hpp" + +#include namespace chatterino { @@ -36,11 +38,11 @@ void ChannelChatters::addJoinedUser(const QString &user) auto joinedUsers = this->joinedUsers_.access(); joinedUsers->sort(); - MessageBuilder builder; - TwitchMessageBuilder::listOfUsersSystemMessage( - "Users joined:", *joinedUsers, &this->channel_, &builder); - builder->flags.set(MessageFlag::Collapsed); - this->channel_.addMessage(builder.release()); + this->channel_.addMessage( + MessageBuilder::makeListOfUsersMessage( + "Users joined:", *joinedUsers, &this->channel_, + {MessageFlag::Collapsed}), + MessageContext::Original); joinedUsers->clear(); this->joinedUsersMergeQueued_ = false; @@ -61,11 +63,11 @@ void ChannelChatters::addPartedUser(const QString &user) auto partedUsers = this->partedUsers_.access(); partedUsers->sort(); - MessageBuilder builder; - TwitchMessageBuilder::listOfUsersSystemMessage( - "Users parted:", *partedUsers, &this->channel_, &builder); - builder->flags.set(MessageFlag::Collapsed); - this->channel_.addMessage(builder.release()); + this->channel_.addMessage( + MessageBuilder::makeListOfUsersMessage( + "Users parted:", *partedUsers, &this->channel_, + {MessageFlag::Collapsed}), + MessageContext::Original); partedUsers->clear(); this->partedUsersMergeQueued_ = false; diff --git a/src/common/ChannelChatters.hpp b/src/common/ChannelChatters.hpp index 96d4810d1..b15717dc9 100644 --- a/src/common/ChannelChatters.hpp +++ b/src/common/ChannelChatters.hpp @@ -1,15 +1,18 @@ #pragma once -#include "common/Channel.hpp" #include "common/ChatterSet.hpp" #include "common/UniqueAccess.hpp" #include "lrucache/lrucache.hpp" #include "util/QStringHash.hpp" +#include +#include #include namespace chatterino { +class Channel; + class ChannelChatters { public: diff --git a/src/common/ChatterSet.cpp b/src/common/ChatterSet.cpp index b11b1dc7f..e54ae6947 100644 --- a/src/common/ChatterSet.cpp +++ b/src/common/ChatterSet.cpp @@ -1,12 +1,11 @@ #include "common/ChatterSet.hpp" -#include #include "debug/Benchmark.hpp" namespace chatterino { ChatterSet::ChatterSet() - : items(chatterLimit) + : items(ChatterSet::CHATTER_LIMIT) { } @@ -21,16 +20,20 @@ void ChatterSet::updateOnlineChatters( BenchmarkGuard bench("update online chatters"); // Create a new lru cache without the users that are not present anymore. - cache::lru_cache tmp(chatterLimit); + cache::lru_cache tmp(ChatterSet::CHATTER_LIMIT); for (auto &&chatter : lowerCaseUsernames) { if (this->items.exists(chatter)) + { tmp.put(chatter, this->items.get(chatter)); - // Less chatters than the limit => try to preserve as many as possible. - else if (lowerCaseUsernames.size() < chatterLimit) + // Less chatters than the limit => try to preserve as many as possible. + } + else if (lowerCaseUsernames.size() < ChatterSet::CHATTER_LIMIT) + { tmp.put(chatter, chatter); + } } this->items = std::move(tmp); @@ -49,10 +52,17 @@ std::vector ChatterSet::filterByPrefix(const QString &prefix) const for (auto &&item : this->items) { if (item.first.startsWith(lowerPrefix)) + { result.push_back(item.second); + } } return result; } +std::vector> ChatterSet::all() const +{ + return {this->items.begin(), this->items.end()}; +} + } // namespace chatterino diff --git a/src/common/ChatterSet.hpp b/src/common/ChatterSet.hpp index bf21c4f00..c06d866ec 100644 --- a/src/common/ChatterSet.hpp +++ b/src/common/ChatterSet.hpp @@ -1,13 +1,13 @@ #pragma once -#include -#include -#include -#include -#include -#include "lrucache/lrucache.hpp" #include "util/QStringHash.hpp" +#include +#include + +#include +#include + namespace chatterino { /// ChatterSet is a limited container that contains a list of recent chatters @@ -16,7 +16,7 @@ class ChatterSet { public: /// The limit of how many chatters can be saved for a channel. - static constexpr size_t chatterLimit = 2000; + static constexpr size_t CHATTER_LIMIT = 2000; ChatterSet(); @@ -36,6 +36,10 @@ public: /// are in mixed case if available. std::vector filterByPrefix(const QString &prefix) const; + /// Get all recent chatters. The first pair element contains the username + /// in lowercase, while the second pair element is the original case. + std::vector> all() const; + private: // user name in lower case -> user name in normal case cache::lru_cache items; diff --git a/src/common/ChatterinoSetting.cpp b/src/common/ChatterinoSetting.cpp index b0acb854a..284033a62 100644 --- a/src/common/ChatterinoSetting.cpp +++ b/src/common/ChatterinoSetting.cpp @@ -1,6 +1,6 @@ #include "common/ChatterinoSetting.hpp" -#include "BaseSettings.hpp" +#include "singletons/Settings.hpp" namespace chatterino { diff --git a/src/common/ChatterinoSetting.hpp b/src/common/ChatterinoSetting.hpp index 2e48956b4..6dab1a0c6 100644 --- a/src/common/ChatterinoSetting.hpp +++ b/src/common/ChatterinoSetting.hpp @@ -1,7 +1,9 @@ #pragma once -#include +#include "util/QMagicEnum.hpp" + #include +#include namespace chatterino { @@ -12,13 +14,16 @@ class ChatterinoSetting : public pajlada::Settings::Setting { public: ChatterinoSetting(const std::string &path) - : pajlada::Settings::Setting(path) + : pajlada::Settings::Setting( + path, pajlada::Settings::SettingOption::CompareBeforeSet) { _registerSetting(this->getData()); } ChatterinoSetting(const std::string &path, const Type &defaultValue) - : pajlada::Settings::Setting(path, defaultValue) + : pajlada::Settings::Setting( + path, defaultValue, + pajlada::Settings::SettingOption::CompareBeforeSet) { _registerSetting(this->getData()); } @@ -85,4 +90,65 @@ public: } }; +/** + * Setters in this class allow for bad values, it's only the enum-specific getters that are protected. + * If you get a QString from this setting, it will be the raw value from the settings file. + * Use the explicit Enum conversions or getEnum to get a typed check with a default + **/ +template +class EnumStringSetting : public pajlada::Settings::Setting +{ +public: + EnumStringSetting(const std::string &path, const Enum &defaultValue_) + : pajlada::Settings::Setting(path) + , defaultValue(defaultValue_) + { + _registerSetting(this->getData()); + } + + template + EnumStringSetting &operator=(Enum newValue) + { + this->setValue(qmagicenum::enumNameString(newValue).toLower()); + + return *this; + } + + EnumStringSetting &operator=(QString newValue) + { + this->setValue(newValue.toLower()); + + return *this; + } + + operator Enum() + { + return this->getEnum(); + } + + Enum getEnum() + { + return qmagicenum::enumCast(this->getValue(), + qmagicenum::CASE_INSENSITIVE) + .value_or(this->defaultValue); + } + + Enum defaultValue; + + using pajlada::Settings::Setting::operator==; + using pajlada::Settings::Setting::operator!=; + + using pajlada::Settings::Setting::operator QString; +}; + +template +struct IsChatterinoSettingT : std::false_type { +}; +template +struct IsChatterinoSettingT> : std::true_type { +}; + +template +concept IsChatterinoSetting = IsChatterinoSettingT::value; + } // namespace chatterino diff --git a/src/common/Common.hpp b/src/common/Common.hpp index 2284fd0ea..f1ce96754 100644 --- a/src/common/Common.hpp +++ b/src/common/Common.hpp @@ -1,44 +1,36 @@ #pragma once +#include #include #include -#include -#include -#include -#include -#include "common/Aliases.hpp" -#include "common/Outcome.hpp" -#include "common/ProviderId.hpp" +#include namespace chatterino { +constexpr QStringView LINK_CHATTERINO_WIKI = u"https://wiki.chatterino.com"; +constexpr QStringView LINK_CHATTERINO_DISCORD = + u"https://discord.gg/7Y5AYhAK4z"; +constexpr QStringView LINK_CHATTERINO_SOURCE = + u"https://github.com/Chatterino/chatterino2"; + +constexpr QStringView TWITCH_PLAYER_URL = + u"https://player.twitch.tv/?channel=%1&parent=twitch.tv"; + enum class HighlightState { None, Highlighted, NewMessage, }; -inline QString qS(const std::string &string) -{ - return QString::fromStdString(string); -} - -const Qt::KeyboardModifiers showSplitOverlayModifiers = +constexpr Qt::KeyboardModifiers SHOW_SPLIT_OVERLAY_MODIFIERS = Qt::ControlModifier | Qt::AltModifier; -const Qt::KeyboardModifiers showAddSplitRegions = +constexpr Qt::KeyboardModifiers SHOW_ADD_SPLIT_REGIONS = Qt::ControlModifier | Qt::AltModifier; -const Qt::KeyboardModifiers showResizeHandlesModifiers = Qt::ControlModifier; +constexpr Qt::KeyboardModifiers SHOW_RESIZE_HANDLES_MODIFIERS = + Qt::ControlModifier; -#ifndef ATTR_UNUSED -# ifdef Q_OS_WIN -# define ATTR_UNUSED -# else -# define ATTR_UNUSED __attribute__((unused)) -# endif -#endif - -static const char *ANONYMOUS_USERNAME_LABEL ATTR_UNUSED = " - anonymous - "; +constexpr const char *ANONYMOUS_USERNAME_LABEL = " - anonymous - "; template std::weak_ptr weakOf(T *element) diff --git a/src/common/CompletionModel.cpp b/src/common/CompletionModel.cpp deleted file mode 100644 index c03fc830d..000000000 --- a/src/common/CompletionModel.cpp +++ /dev/null @@ -1,243 +0,0 @@ -#include "common/CompletionModel.hpp" - -#include "Application.hpp" -#include "common/ChatterSet.hpp" -#include "common/Common.hpp" -#include "controllers/accounts/AccountController.hpp" -#include "controllers/commands/CommandController.hpp" -#include "debug/Benchmark.hpp" -#include "providers/twitch/TwitchChannel.hpp" -#include "providers/twitch/TwitchCommon.hpp" -#include "providers/twitch/TwitchIrcServer.hpp" -#include "singletons/Emotes.hpp" -#include "singletons/Settings.hpp" -#include "util/Helpers.hpp" -#include "util/QStringHash.hpp" - -#include -#include - -namespace chatterino { - -// -// TaggedString -// - -CompletionModel::TaggedString::TaggedString(const QString &_string, Type _type) - : string(_string) - , type(_type) -{ -} - -bool CompletionModel::TaggedString::isEmote() const -{ - return this->type > Type::EmoteStart && this->type < Type::EmoteEnd; -} - -bool CompletionModel::TaggedString::operator<(const TaggedString &that) const -{ - if (this->isEmote() != that.isEmote()) - { - return this->isEmote(); - } - - return CompletionModel::compareStrings(this->string, that.string); -} - -// -// CompletionModel -// -CompletionModel::CompletionModel(Channel &channel) - : channel_(channel) -{ -} - -int CompletionModel::columnCount(const QModelIndex &) const -{ - return 1; -} - -QVariant CompletionModel::data(const QModelIndex &index, int) const -{ - std::lock_guard lock(this->itemsMutex_); - - auto it = this->items_.begin(); - std::advance(it, index.row()); - return QVariant(it->string); -} - -int CompletionModel::rowCount(const QModelIndex &) const -{ - std::lock_guard lock(this->itemsMutex_); - - return this->items_.size(); -} - -void CompletionModel::refresh(const QString &prefix, bool isFirstWord) -{ - std::lock_guard guard(this->itemsMutex_); - this->items_.clear(); - - if (prefix.length() < 2 || !this->channel_.isTwitchChannel()) - { - return; - } - - // Twitch channel - auto tc = dynamic_cast(&this->channel_); - - auto addString = [=](const QString &str, TaggedString::Type type) { - // Special case for handling default Twitch commands - if (type == TaggedString::TwitchCommand) - { - if (prefix.size() < 2) - { - return; - } - - auto prefixChar = prefix.at(0); - - static std::set validPrefixChars{'/', '.'}; - - if (validPrefixChars.find(prefixChar) == validPrefixChars.end()) - { - return; - } - - if (startsWithOrContains((prefixChar + str), prefix, - Qt::CaseInsensitive, - getSettings()->prefixOnlyEmoteCompletion)) - { - this->items_.emplace((prefixChar + str + " "), type); - } - - return; - } - - if (startsWithOrContains(str, prefix, Qt::CaseInsensitive, - getSettings()->prefixOnlyEmoteCompletion)) - { - this->items_.emplace(str + " ", type); - } - }; - - if (auto account = getApp()->accounts->twitch.getCurrent()) - { - // Twitch Emotes available globally - for (const auto &emote : account->accessEmotes()->emotes) - { - addString(emote.first.string, TaggedString::TwitchGlobalEmote); - } - - // Twitch Emotes available locally - auto localEmoteData = account->accessLocalEmotes(); - if (tc && localEmoteData->find(tc->roomId()) != localEmoteData->end()) - { - for (const auto &emote : localEmoteData->at(tc->roomId())) - { - addString(emote.first.string, - TaggedString::Type::TwitchLocalEmote); - } - } - } - - // Bttv Global - for (auto &emote : *getApp()->twitch->getBttvEmotes().emotes()) - { - addString(emote.first.string, TaggedString::Type::BTTVChannelEmote); - } - - // Ffz Global - for (auto &emote : *getApp()->twitch->getFfzEmotes().emotes()) - { - addString(emote.first.string, TaggedString::Type::FFZChannelEmote); - } - - // Emojis - if (prefix.startsWith(":")) - { - const auto &emojiShortCodes = getApp()->emotes->emojis.shortCodes; - for (auto &m : emojiShortCodes) - { - addString(QString(":%1:").arg(m), TaggedString::Type::Emoji); - } - } - - // - // Stuff below is available only in regular Twitch channels - if (!tc) - { - return; - } - - // Usernames - if (prefix.startsWith("@")) - { - QString usernamePrefix = prefix; - usernamePrefix.remove(0, 1); - - auto chatters = tc->accessChatters()->filterByPrefix(usernamePrefix); - - for (const auto &name : chatters) - { - addString( - "@" + formatUserMention(name, isFirstWord, - getSettings()->mentionUsersWithComma), - TaggedString::Type::Username); - } - } - else if (!getSettings()->userCompletionOnlyWithAt) - { - auto chatters = tc->accessChatters()->filterByPrefix(prefix); - - for (const auto &name : chatters) - { - addString(formatUserMention(name, isFirstWord, - getSettings()->mentionUsersWithComma), - TaggedString::Type::Username); - } - } - - // Bttv Channel - for (auto &emote : *tc->bttvEmotes()) - { - addString(emote.first.string, TaggedString::Type::BTTVGlobalEmote); - } - - // Ffz Channel - for (auto &emote : *tc->ffzEmotes()) - { - addString(emote.first.string, TaggedString::Type::BTTVGlobalEmote); - } - - // Custom Chatterino commands - for (auto &command : getApp()->commands->items) - { - addString(command.name, TaggedString::CustomCommand); - } - - // Default Chatterino commands - for (auto &command : getApp()->commands->getDefaultChatterinoCommandList()) - { - addString(command, TaggedString::ChatterinoCommand); - } - - // Default Twitch commands - for (auto &command : TWITCH_DEFAULT_COMMANDS) - { - addString(command, TaggedString::TwitchCommand); - } -} - -bool CompletionModel::compareStrings(const QString &a, const QString &b) -{ - // try comparing insensitively, if they are the same then senstively - // (fixes order of LuL and LUL) - int k = QString::compare(a, b, Qt::CaseInsensitive); - if (k == 0) - return a > b; - - return k < 0; -} - -} // namespace chatterino diff --git a/src/common/CompletionModel.hpp b/src/common/CompletionModel.hpp deleted file mode 100644 index ee810bbe6..000000000 --- a/src/common/CompletionModel.hpp +++ /dev/null @@ -1,63 +0,0 @@ -#pragma once - -#include - -#include -#include -#include - -namespace chatterino { - -class Channel; - -class CompletionModel : public QAbstractListModel -{ - struct TaggedString { - enum Type { - Username, - - // emotes - EmoteStart, - FFZGlobalEmote, - FFZChannelEmote, - BTTVGlobalEmote, - BTTVChannelEmote, - TwitchGlobalEmote, - TwitchLocalEmote, - TwitchSubscriberEmote, - Emoji, - EmoteEnd, - // end emotes - - CustomCommand, - ChatterinoCommand, - TwitchCommand, - }; - - TaggedString(const QString &string, Type type); - - bool isEmote() const; - bool operator<(const TaggedString &that) const; - - QString string; - Type type; - }; - -public: - CompletionModel(Channel &channel); - - virtual int columnCount(const QModelIndex &) const override; - virtual QVariant data(const QModelIndex &index, int) const override; - virtual int rowCount(const QModelIndex &) const override; - - void refresh(const QString &prefix, bool isFirstWord = false); - - static bool compareStrings(const QString &a, const QString &b); - -private: - std::set items_; - mutable std::mutex itemsMutex_; - Channel &channel_; -}; - -} // namespace chatterino diff --git a/src/common/ConcurrentMap.hpp b/src/common/ConcurrentMap.hpp deleted file mode 100644 index 6f385559b..000000000 --- a/src/common/ConcurrentMap.hpp +++ /dev/null @@ -1,102 +0,0 @@ -#pragma once - -#include -#include -#include - -#include -#include -#include - -namespace chatterino { - -template -class ConcurrentMap -{ -public: - ConcurrentMap() = default; - - bool tryGet(const TKey &name, TValue &value) const - { - QMutexLocker lock(&this->mutex); - - auto a = this->data.find(name); - if (a == this->data.end()) - { - return false; - } - - value = a.value(); - - return true; - } - - TValue getOrAdd(const TKey &name, std::function addLambda) - { - QMutexLocker lock(&this->mutex); - - auto a = this->data.find(name); - if (a == this->data.end()) - { - TValue value = addLambda(); - this->data.insert(name, value); - return value; - } - - return a.value(); - } - - TValue &operator[](const TKey &name) - { - QMutexLocker lock(&this->mutex); - - return this->data[name]; - } - - void clear() - { - QMutexLocker lock(&this->mutex); - - this->data.clear(); - } - - void insert(const TKey &name, const TValue &value) - { - QMutexLocker lock(&this->mutex); - - this->data.insert(name, value); - } - - void each( - std::function func) const - { - QMutexLocker lock(&this->mutex); - - QMapIterator it(this->data); - - while (it.hasNext()) - { - it.next(); - func(it.key(), it.value()); - } - } - - void each(std::function func) - { - QMutexLocker lock(&this->mutex); - - QMutableMapIterator it(this->data); - - while (it.hasNext()) - { - it.next(); - func(it.key(), it.value()); - } - } - -private: - mutable QMutex mutex; - QMap data; -}; - -} // namespace chatterino diff --git a/src/common/Credentials.cpp b/src/common/Credentials.cpp index 5913e0d03..1effa8f9b 100644 --- a/src/common/Credentials.cpp +++ b/src/common/Credentials.cpp @@ -1,189 +1,198 @@ -#include "Credentials.hpp" +#include "common/Credentials.hpp" +#include "Application.hpp" +#include "common/Modes.hpp" #include "debug/AssertInGuiThread.hpp" #include "singletons/Paths.hpp" #include "singletons/Settings.hpp" #include "util/CombinePath.hpp" -#include "util/Overloaded.hpp" +#include "util/Variant.hpp" +#include #include #include +#include +#include + +#include #ifndef NO_QTKEYCHAIN # ifdef CMAKE_BUILD -# include "qt5keychain/keychain.h" +# if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +# include "qt6keychain/keychain.h" +# else +# include "qt5keychain/keychain.h" +# endif # else # include "keychain.h" # endif #endif -#include -#include - -#define FORMAT_NAME \ - ([&] { \ - assert(!provider.contains(":")); \ - return QString("chatterino:%1:%2").arg(provider).arg(name_); \ - })() - -namespace chatterino { namespace { - bool useKeyring() - { + +using namespace chatterino; + +QString formatName(const QString &provider, const QString &name) +{ + assert(!provider.contains(":")); + return u"chatterino:" % provider % u':' % name; +} + +bool useKeyring() +{ #ifdef NO_QTKEYCHAIN + return false; +#endif + if (Modes::instance().isPortable) + { return false; -#endif - if (getPaths()->isPortable()) - { - return false; - } - else - { + } + #ifdef Q_OS_LINUX - return getSettings()->useKeyring; + return getSettings()->useKeyring; #else - return true; + return true; #endif - } - } +} - // Insecure storage: - QString insecurePath() +// Insecure storage: +QString insecurePath() +{ + return combinePath(getApp()->getPaths().settingsDirectory, + "credentials.json"); +} + +QJsonDocument loadInsecure() +{ + QFile file(insecurePath()); + file.open(QIODevice::ReadOnly); + return QJsonDocument::fromJson(file.readAll()); +} + +void storeInsecure(const QJsonDocument &doc) +{ + QSaveFile file(insecurePath()); + file.open(QIODevice::WriteOnly); + file.write(doc.toJson()); + file.commit(); +} + +QJsonDocument &insecureInstance() +{ + static auto store = loadInsecure(); + return store; +} + +void queueInsecureSave() +{ + static bool isQueued = false; + + if (!isQueued) { - return combinePath(getPaths()->settingsDirectory, "credentials.json"); + isQueued = true; + QTimer::singleShot(200, QApplication::instance(), [] { + storeInsecure(insecureInstance()); + isQueued = false; + }); } +} - QJsonDocument loadInsecure() - { - QFile file(insecurePath()); - file.open(QIODevice::ReadOnly); - return QJsonDocument::fromJson(file.readAll()); - } +// QKeychain runs jobs asyncronously, so we have to assure that set/erase +// jobs gets executed in order. +struct SetJob { + QString name; + QString credential; +}; - void storeInsecure(const QJsonDocument &doc) - { - QSaveFile file(insecurePath()); - file.open(QIODevice::WriteOnly); - file.write(doc.toJson()); - file.commit(); - } +struct EraseJob { + QString name; +}; - QJsonDocument &insecureInstance() - { - static auto store = loadInsecure(); - return store; - } +using Job = std::variant; - void queueInsecureSave() - { - static bool isQueued = false; +std::queue &jobQueue() +{ + static std::queue jobs; + return jobs; +} - if (!isQueued) - { - isQueued = true; - QTimer::singleShot(200, qApp, [] { - storeInsecure(insecureInstance()); - isQueued = false; - }); - } - } - - // QKeychain runs jobs asyncronously, so we have to assure that set/erase - // jobs gets executed in order. - struct SetJob { - QString name; - QString credential; - }; - - struct EraseJob { - QString name; - }; - - using Job = boost::variant; - - static std::queue &jobQueue() - { - static std::queue jobs; - return jobs; - } - - static void runNextJob() - { +void runNextJob() +{ #ifndef NO_QTKEYCHAIN - auto &&queue = jobQueue(); + auto &&queue = jobQueue(); - if (!queue.empty()) - { - // we were gonna use std::visit here but macos is shit - - auto &&item = queue.front(); - - if (item.which() == 0) // set job - { - auto set = boost::get(item); - auto job = new QKeychain::WritePasswordJob("chatterino"); - job->setAutoDelete(true); - job->setKey(set.name); - job->setTextData(set.credential); - QObject::connect(job, &QKeychain::Job::finished, qApp, - [](auto) { - runNextJob(); - }); - job->start(); - } - else // erase job - { - auto erase = boost::get(item); - auto job = new QKeychain::DeletePasswordJob("chatterino"); - job->setAutoDelete(true); - job->setKey(erase.name); - QObject::connect(job, &QKeychain::Job::finished, qApp, - [](auto) { - runNextJob(); - }); - job->start(); - } - - queue.pop(); - } -#endif - } - - static void queueJob(Job &&job) + if (!queue.empty()) { - auto &&queue = jobQueue(); + // we were gonna use std::visit here but macos is shit - queue.push(std::move(job)); - if (queue.size() == 1) - { - runNextJob(); - } + auto &&item = queue.front(); + + std::visit( + variant::Overloaded{ + [](const SetJob &set) { + auto *job = new QKeychain::WritePasswordJob("chatterino"); + job->setAutoDelete(true); + job->setKey(set.name); + job->setTextData(set.credential); + QObject::connect(job, &QKeychain::Job::finished, + QApplication::instance(), [](auto) { + runNextJob(); + }); + job->start(); + }, + [](const EraseJob &erase) { + auto *job = new QKeychain::DeletePasswordJob("chatterino"); + job->setAutoDelete(true); + job->setKey(erase.name); + QObject::connect(job, &QKeychain::Job::finished, + QApplication::instance(), [](auto) { + runNextJob(); + }); + job->start(); + }, + }, + item); + + queue.pop(); } +#endif +} + +void queueJob(Job &&job) +{ + auto &&queue = jobQueue(); + + queue.push(std::move(job)); + if (queue.size() == 1) + { + runNextJob(); + } +} + } // namespace +namespace chatterino { + Credentials &Credentials::instance() { static Credentials creds; return creds; } -Credentials::Credentials() -{ -} - +// NOLINTNEXTLINE(readability-convert-member-functions-to-static) void Credentials::get(const QString &provider, const QString &name_, QObject *receiver, std::function &&onLoaded) { assertInGuiThread(); - auto name = FORMAT_NAME; + auto name = formatName(provider, name_); if (useKeyring()) { #ifndef NO_QTKEYCHAIN // if NO_QTKEYCHAIN is set, then this code is never used either way - auto job = new QKeychain::ReadPasswordJob("chatterino"); + auto *job = new QKeychain::ReadPasswordJob("chatterino"); job->setAutoDelete(true); job->setKey(name); QObject::connect( @@ -197,12 +206,13 @@ void Credentials::get(const QString &provider, const QString &name_, } else { - auto &instance = insecureInstance(); + const auto &instance = insecureInstance(); - onLoaded(instance.object().find(name).value().toString()); + onLoaded(instance[name].toString()); } } +// NOLINTNEXTLINE(readability-convert-member-functions-to-static) void Credentials::set(const QString &provider, const QString &name_, const QString &credential) { @@ -211,7 +221,7 @@ void Credentials::set(const QString &provider, const QString &name_, /// On linux, we try to use a keychain but show a message to disable it when it fails. /// XXX: add said message - auto name = FORMAT_NAME; + auto name = formatName(provider, name_); if (useKeyring()) { @@ -229,11 +239,12 @@ void Credentials::set(const QString &provider, const QString &name_, } } +// NOLINTNEXTLINE(readability-convert-member-functions-to-static) void Credentials::erase(const QString &provider, const QString &name_) { assertInGuiThread(); - auto name = FORMAT_NAME; + auto name = formatName(provider, name_); if (useKeyring()) { diff --git a/src/common/Credentials.hpp b/src/common/Credentials.hpp index 0b144f602..d3bec378a 100644 --- a/src/common/Credentials.hpp +++ b/src/common/Credentials.hpp @@ -19,7 +19,7 @@ public: void erase(const QString &provider, const QString &name); private: - Credentials(); + Credentials() = default; }; } // namespace chatterino diff --git a/src/common/DownloadManager.cpp b/src/common/DownloadManager.cpp deleted file mode 100644 index f8344c304..000000000 --- a/src/common/DownloadManager.cpp +++ /dev/null @@ -1,83 +0,0 @@ -#include "DownloadManager.hpp" - -#include "common/QLogging.hpp" -#include "singletons/Paths.hpp" - -#include - -namespace chatterino { - -DownloadManager::DownloadManager(QObject *parent) - : QObject(parent) - , manager_(new QNetworkAccessManager) -{ -} - -DownloadManager::~DownloadManager() -{ - this->manager_->deleteLater(); -} - -void DownloadManager::setFile(QString fileURL, const QString &channelName) -{ - QString saveFilePath; - saveFilePath = - getPaths()->twitchProfileAvatars + "/twitch/" + channelName + ".png"; - QNetworkRequest request; - request.setUrl(QUrl(fileURL)); - this->reply_ = this->manager_->get(request); - - this->file_ = new QFile; - this->file_->setFileName(saveFilePath); - this->file_->open(QIODevice::WriteOnly); - - connect(this->reply_, SIGNAL(downloadProgress(qint64, qint64)), this, - SLOT(onDownloadProgress(qint64, qint64))); - connect(this->manager_, SIGNAL(finished(QNetworkReply *)), this, - SLOT(onFinished(QNetworkReply *))); - connect(this->reply_, SIGNAL(readyRead()), this, SLOT(onReadyRead())); - connect(this->reply_, SIGNAL(finished()), this, SLOT(onReplyFinished())); -} - -void DownloadManager::onDownloadProgress(qint64 bytesRead, qint64 bytesTotal) -{ - qCDebug(chatterinoCommon) - << "Download progress: " << bytesRead << "/" << bytesTotal; -} - -void DownloadManager::onFinished(QNetworkReply *reply) -{ - switch (reply->error()) - { - case QNetworkReply::NoError: { - qCDebug(chatterinoCommon) << "file is downloaded successfully."; - } - break; - default: { - qCDebug(chatterinoCommon) << reply->errorString().toLatin1(); - }; - } - - if (this->file_->isOpen()) - { - this->file_->close(); - this->file_->deleteLater(); - } - emit downloadComplete(); -} - -void DownloadManager::onReadyRead() -{ - this->file_->write(this->reply_->readAll()); -} - -void DownloadManager::onReplyFinished() -{ - if (this->file_->isOpen()) - { - this->file_->close(); - this->file_->deleteLater(); - } -} - -} // namespace chatterino diff --git a/src/common/DownloadManager.hpp b/src/common/DownloadManager.hpp deleted file mode 100644 index c41cae9c8..000000000 --- a/src/common/DownloadManager.hpp +++ /dev/null @@ -1,35 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -namespace chatterino { - -class DownloadManager : public QObject -{ - Q_OBJECT -public: - explicit DownloadManager(QObject *parent = nullptr); - virtual ~DownloadManager(); - void setFile(QString fileURL, const QString &channelName); - -private: - QNetworkAccessManager *manager_; - QNetworkReply *reply_; - QFile *file_; - -private slots: - void onDownloadProgress(qint64, qint64); - void onFinished(QNetworkReply *); - void onReadyRead(); - void onReplyFinished(); - -signals: - void downloadComplete(); -}; - -} // namespace chatterino diff --git a/src/common/Env.cpp b/src/common/Env.cpp index 36757b841..6a7f5dac3 100644 --- a/src/common/Env.cpp +++ b/src/common/Env.cpp @@ -3,6 +3,7 @@ #include "common/QLogging.hpp" #include "util/TypeName.hpp" +#include #include namespace chatterino { @@ -10,16 +11,8 @@ namespace chatterino { namespace { template - void warn(const char *envName, T defaultValue) + void warn(const char *envName, const QString &envString, T defaultValue) { - auto *envString = std::getenv(envName); - if (!envString) - { - // This function is not supposed to be used for non-existant - // environment variables. - return; - } - const auto typeName = QString::fromStdString( std::string(type_name())); @@ -33,43 +26,41 @@ namespace { .arg(defaultValue); } - QString readStringEnv(const char *envName, QString defaultValue) + std::optional readOptionalStringEnv(const char *envName) { - auto envString = std::getenv(envName); - if (envString != nullptr) + auto envString = qEnvironmentVariable(envName); + if (!envString.isEmpty()) { - return QString(envString); + return envString; } - return defaultValue; + return std::nullopt; } uint16_t readPortEnv(const char *envName, uint16_t defaultValue) { - auto envString = std::getenv(envName); - if (envString != nullptr) + auto envString = qEnvironmentVariable(envName); + if (!envString.isEmpty()) { - bool ok; - auto val = QString(envString).toUShort(&ok); + bool ok = false; + auto val = envString.toUShort(&ok); if (ok) { return val; } - else - { - warn(envName, defaultValue); - } + + warn(envName, envString, defaultValue); } return defaultValue; } - uint16_t readBoolEnv(const char *envName, bool defaultValue) + bool readBoolEnv(const char *envName, bool defaultValue) { - auto envString = std::getenv(envName); - if (envString != nullptr) + auto envString = qEnvironmentVariable(envName); + if (!envString.isEmpty()) { - return QVariant(QString(envString)).toBool(); + return QVariant(envString).toBool(); } return defaultValue; @@ -79,16 +70,17 @@ namespace { Env::Env() : recentMessagesApiUrl( - readStringEnv("CHATTERINO2_RECENT_MESSAGES_URL", - "https://recent-messages.robotty.de/api/v2/" - "recent-messages/%1")) - , linkResolverUrl(readStringEnv( + qEnvironmentVariable("CHATTERINO2_RECENT_MESSAGES_URL", + "https://recent-messages.robotty.de/api/v2/" + "recent-messages/%1")) + , linkResolverUrl(qEnvironmentVariable( "CHATTERINO2_LINK_RESOLVER_URL", "https://braize.pajlada.com/chatterino/link_resolver/%1")) - , twitchServerHost( - readStringEnv("CHATTERINO2_TWITCH_SERVER_HOST", "irc.chat.twitch.tv")) + , twitchServerHost(qEnvironmentVariable("CHATTERINO2_TWITCH_SERVER_HOST", + "irc.chat.twitch.tv")) , twitchServerPort(readPortEnv("CHATTERINO2_TWITCH_SERVER_PORT", 443)) , twitchServerSecure(readBoolEnv("CHATTERINO2_TWITCH_SERVER_SECURE", true)) + , proxyUrl(readOptionalStringEnv("CHATTERINO2_PROXY_URL")) { } diff --git a/src/common/Env.hpp b/src/common/Env.hpp index b334e8e96..353a6af48 100644 --- a/src/common/Env.hpp +++ b/src/common/Env.hpp @@ -2,6 +2,8 @@ #include +#include + namespace chatterino { class Env @@ -16,6 +18,7 @@ public: const QString twitchServerHost; const uint16_t twitchServerPort; const bool twitchServerSecure; + const std::optional proxyUrl; }; } // namespace chatterino diff --git a/src/common/FlagsEnum.hpp b/src/common/FlagsEnum.hpp index c5d65e186..d2ae4ebf7 100644 --- a/src/common/FlagsEnum.hpp +++ b/src/common/FlagsEnum.hpp @@ -1,88 +1,146 @@ #pragma once -#include +#include #include namespace chatterino { -template ::type> +template + requires std::is_enum_v class FlagsEnum { public: - FlagsEnum() - : value_(static_cast(0)) + using Int = std::underlying_type_t; + + constexpr FlagsEnum() noexcept = default; + + constexpr FlagsEnum(std::convertible_to auto... flags) noexcept + : value_( + static_cast((static_cast(static_cast(flags)) | ...))) { } - FlagsEnum(T value) - : value_(value) + friend constexpr bool operator==(FlagsEnum lhs, FlagsEnum rhs) noexcept { + return lhs.value_ == rhs.value_; + } + friend constexpr bool operator!=(FlagsEnum lhs, FlagsEnum rhs) noexcept + { + return lhs.value_ != rhs.value_; } - FlagsEnum(std::initializer_list flags) + friend constexpr bool operator==(FlagsEnum lhs, T rhs) noexcept { - for (auto flag : flags) + return lhs.value_ == rhs; + } + friend constexpr bool operator!=(FlagsEnum lhs, T rhs) noexcept + { + return lhs.value_ != rhs; + } + + friend constexpr bool operator==(T lhs, FlagsEnum rhs) noexcept + { + return lhs == rhs.value_; + } + friend constexpr bool operator!=(T lhs, FlagsEnum rhs) noexcept + { + return lhs != rhs.value_; + } + + constexpr void set(std::convertible_to auto... flags) noexcept + { + this->value_ = + static_cast(static_cast(this->value_) | + (static_cast(static_cast(flags)) | ...)); + } + + /** Adds the flags from `flags` in this enum. */ + constexpr void set(FlagsEnum flags) noexcept + { + this->value_ = static_cast(static_cast(this->value_) | + static_cast(flags.value_)); + } + + constexpr void unset(std::convertible_to auto... flags) noexcept + { + this->value_ = + static_cast(static_cast(this->value_) & + ~(static_cast(static_cast(flags)) | ...)); + } + + constexpr void set(T flag, bool value) noexcept + { + if (value) { this->set(flag); } - } - - bool operator==(const FlagsEnum &other) - { - return this->value_ == other.value_; - } - - bool operator!=(const FlagsEnum &other) - { - return this->value_ != other.value_; - } - - void set(T flag) - { - reinterpret_cast(this->value_) |= static_cast(flag); - } - - void unset(T flag) - { - reinterpret_cast(this->value_) &= ~static_cast(flag); - } - - void set(T flag, bool value) - { - if (value) - this->set(flag); else + { this->unset(flag); + } } - bool has(T flag) const + constexpr FlagsEnum operator|(T flag) const noexcept { - return static_cast(this->value_) & static_cast(flag); + return static_cast(static_cast(this->value_) | + static_cast(flag)); } - FlagsEnum operator|(T flag) + constexpr FlagsEnum operator|(FlagsEnum rhs) const noexcept { - FlagsEnum xd; - xd.value_ = this->value_; - xd.set(flag, true); - - return xd; + return static_cast(static_cast(this->value_) | + static_cast(rhs.value_)); } - bool hasAny(FlagsEnum flags) const + constexpr bool has(T flag) const noexcept { - return static_cast(this->value_) & static_cast(flags.value_); + return static_cast(this->value_) & static_cast(flag); } - bool hasAll(FlagsEnum flags) const + constexpr bool hasAny(FlagsEnum flags) const noexcept { - return (static_cast(this->value_) & static_cast(flags.value_)) && - static_cast(flags->value); + return (static_cast(this->value_) & + static_cast(flags.value_)) != 0; } - bool hasNone(std::initializer_list flags) const + constexpr bool hasAny(std::convertible_to auto... flags) const noexcept { - return !this->hasAny(flags); + return this->hasAny(FlagsEnum{flags...}); + } + + constexpr bool hasAll(FlagsEnum flags) const noexcept + { + return (static_cast(this->value_) & + static_cast(flags.value_)) == + static_cast(flags.value_); + } + + constexpr bool hasAll(std::convertible_to auto... flags) const noexcept + { + return this->hasAll(FlagsEnum{flags...}); + } + + constexpr bool hasNone(FlagsEnum flags) const noexcept + { + return (static_cast(this->value_) & + static_cast(flags.value_)) == 0; + } + + constexpr bool hasNone() const noexcept = delete; + constexpr bool hasNone(std::convertible_to auto... flags) const noexcept + { + return this->hasNone(FlagsEnum{flags...}); + } + + /// Returns true if the enum has no flag set (i.e. its underlying value is 0) + constexpr bool isEmpty() const noexcept + { + return static_cast(this->value_) == 0; + } + + constexpr T value() const noexcept + { + return this->value_; } private: diff --git a/src/common/IrcColors.hpp b/src/common/IrcColors.hpp deleted file mode 100644 index 972110565..000000000 --- a/src/common/IrcColors.hpp +++ /dev/null @@ -1,62 +0,0 @@ -#pragma once - -#include -#include - -namespace chatterino { - -// Colors taken from https://modern.ircdocs.horse/formatting.html -static QMap IRC_COLORS = { - {0, QColor("white")}, {1, QColor("black")}, - {2, QColor("blue")}, {3, QColor("green")}, - {4, QColor("red")}, {5, QColor("brown")}, - {6, QColor("purple")}, {7, QColor("orange")}, - {8, QColor("yellow")}, {9, QColor("lightgreen")}, - {10, QColor("cyan")}, {11, QColor("lightcyan")}, - {12, QColor("lightblue")}, {13, QColor("pink")}, - {14, QColor("gray")}, {15, QColor("lightgray")}, - {16, QColor("#470000")}, {17, QColor("#472100")}, - {18, QColor("#474700")}, {19, QColor("#324700")}, - {20, QColor("#004700")}, {21, QColor("#00472c")}, - {22, QColor("#004747")}, {23, QColor("#002747")}, - {24, QColor("#000047")}, {25, QColor("#2e0047")}, - {26, QColor("#470047")}, {27, QColor("#47002a")}, - {28, QColor("#740000")}, {29, QColor("#743a00")}, - {30, QColor("#747400")}, {31, QColor("#517400")}, - {32, QColor("#007400")}, {33, QColor("#007449")}, - {34, QColor("#007474")}, {35, QColor("#004074")}, - {36, QColor("#000074")}, {37, QColor("#4b0074")}, - {38, QColor("#740074")}, {39, QColor("#740045")}, - {40, QColor("#b50000")}, {41, QColor("#b56300")}, - {42, QColor("#b5b500")}, {43, QColor("#7db500")}, - {44, QColor("#00b500")}, {45, QColor("#00b571")}, - {46, QColor("#00b5b5")}, {47, QColor("#0063b5")}, - {48, QColor("#0000b5")}, {49, QColor("#7500b5")}, - {50, QColor("#b500b5")}, {51, QColor("#b5006b")}, - {52, QColor("#ff0000")}, {53, QColor("#ff8c00")}, - {54, QColor("#ffff00")}, {55, QColor("#b2ff00")}, - {56, QColor("#00ff00")}, {57, QColor("#00ffa0")}, - {58, QColor("#00ffff")}, {59, QColor("#008cff")}, - {60, QColor("#0000ff")}, {61, QColor("#a500ff")}, - {62, QColor("#ff00ff")}, {63, QColor("#ff0098")}, - {64, QColor("#ff5959")}, {65, QColor("#ffb459")}, - {66, QColor("#ffff71")}, {67, QColor("#cfff60")}, - {68, QColor("#6fff6f")}, {69, QColor("#65ffc9")}, - {70, QColor("#6dffff")}, {71, QColor("#59b4ff")}, - {72, QColor("#5959ff")}, {73, QColor("#c459ff")}, - {74, QColor("#ff66ff")}, {75, QColor("#ff59bc")}, - {76, QColor("#ff9c9c")}, {77, QColor("#ffd39c")}, - {78, QColor("#ffff9c")}, {79, QColor("#e2ff9c")}, - {80, QColor("#9cff9c")}, {81, QColor("#9cffdb")}, - {82, QColor("#9cffff")}, {83, QColor("#9cd3ff")}, - {84, QColor("#9c9cff")}, {85, QColor("#dc9cff")}, - {86, QColor("#ff9cff")}, {87, QColor("#ff94d3")}, - {88, QColor("#000000")}, {89, QColor("#131313")}, - {90, QColor("#282828")}, {91, QColor("#363636")}, - {92, QColor("#4d4d4d")}, {93, QColor("#656565")}, - {94, QColor("#818181")}, {95, QColor("#9f9f9f")}, - {96, QColor("#bcbcbc")}, {97, QColor("#e2e2e2")}, - {98, QColor("#ffffff")}, -}; - -} // namespace chatterino diff --git a/src/common/LinkParser.cpp b/src/common/LinkParser.cpp index 1b8c3f14d..c6ec1ab2d 100644 --- a/src/common/LinkParser.cpp +++ b/src/common/LinkParser.cpp @@ -1,202 +1,314 @@ +#define QT_NO_CAST_FROM_ASCII // avoids unexpected implicit casts #include "common/LinkParser.hpp" +#include "util/QCompareCaseInsensitive.hpp" + #include -#include -#include -#include #include -#include +#include #include -namespace chatterino { +#include + namespace { - QSet &tlds() - { - static QSet tlds = [] { - QFile file(":/tlds.txt"); - file.open(QFile::ReadOnly); - QTextStream stream(&file); - stream.setCodec("UTF-8"); - int safetyMax = 20000; - QSet set; +using namespace chatterino; - while (!stream.atEnd()) - { - auto line = stream.readLine(); - set.insert(line); +using TldSet = std::set; - if (safetyMax-- == 0) - break; - } +TldSet &tlds() +{ + static TldSet tlds = [] { + QFile file(QStringLiteral(":/tlds.txt")); + file.open(QFile::ReadOnly); + QTextStream stream(&file); - return set; - }(); - return tlds; - } - - bool isValidHostname(QStringRef &host) - { - int index = host.lastIndexOf('.'); - - return index != -1 && - tlds().contains(host.mid(index + 1).toString().toLower()); - } - - bool isValidIpv4(QStringRef &host) - { - static auto exp = QRegularExpression("^\\d{1,3}(?:\\.\\d{1,3}){3}$"); - - return exp.match(host).hasMatch(); - } - -#ifdef C_MATCH_IPV6_LINK - bool isValidIpv6(QStringRef &host) - { - static auto exp = QRegularExpression("^\\[[a-fA-F0-9:%]+\\]$"); - - return exp.match(host).hasMatch(); - } +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + // Default encoding of QTextStream is already UTF-8, at least in Qt6 +#else + stream.setCodec("UTF-8"); #endif + + TldSet set; + + while (!stream.atEnd()) + { + set.emplace(stream.readLine()); + } + + return set; + }(); + return tlds; +} + +bool isValidTld(QStringView tld) +{ + return tlds().contains(tld); +} + +bool isValidIpv4(QStringView host) +{ + // We don't care about the actual value, + // we only want to verify the ip. + + char16_t sectionValue = 0; // 0..256 + uint8_t octetNumber = 0; // 0..4 + uint8_t sectionDigits = 0; // 0..3 + bool lastWasDot = true; + + for (auto c : host) + { + char16_t current = c.unicode(); + if (current == '.') + { + if (lastWasDot || octetNumber == 3) + { + return false; + } + lastWasDot = true; + octetNumber++; + sectionValue = 0; + sectionDigits = 0; + continue; + } + lastWasDot = false; + + if (current > u'9' || current < u'0') + { + return false; + } + + sectionValue = sectionValue * 10 + (current - u'0'); + sectionDigits++; + if (sectionValue >= 256 || sectionDigits > 3) + { + return false; + } + } + + return octetNumber == 3 && !lastWasDot; +} + +/** + * @brief Checks if the string starts with a port number. + * + * The value of the port number isn't checked. A port in this implementation + * can be in the range 0..100'000. + */ +bool startsWithPort(QStringView string) +{ + for (qsizetype i = 0; i < std::min(5, string.length()); i++) + { + char16_t c = string[i].unicode(); + if (i >= 1 && (c == u'/' || c == u'?' || c == u'#')) + { + return true; + } + + if (!string[i].isDigit()) + { + return false; + } + } + return true; +} + +/// @brief Strips ignored characters off @a source +/// +/// As per https://github.github.com/gfm/#autolinks-extension-: +/// +/// '<', '*', '_', '~', and '(' are ignored at the beginning +/// '>', '?', '!', '.', ',', ':', '*', '~', and ')' are ignored at the end. +/// +/// A difference to GFM is that '_' isn't a valid suffix. +/// +/// This might remove more than desired (e.g. "(a.com/(foo))" -> "a.com/(foo"). +/// Parentheses are counted after recognizing a valid IP/host. +void strip(QStringView &source) +{ + while (!source.isEmpty()) + { + auto c = source.first(); + if (c == u'<' || c == u'*' || c == u'_' || c == u'~' || c == u'(') + { + source = source.mid(1); + continue; + } + break; + } + + while (!source.isEmpty()) + { + auto c = source.last(); + if (c == u'>' || c == u'?' || c == u'!' || c == u'.' || c == u',' || + c == u':' || c == u'*' || c == u'~' || c == u')') + { + source.chop(1); + continue; + } + break; + } +} + +/// @brief Checks if @a c is valid in a domain +/// +/// Valid characters are 0-9, A-Z, a-z, '-', '_', and '.' (like in GFM) +/// and all non-ASCII characters (unlike in GFM). +Q_ALWAYS_INLINE bool isValidDomainChar(char16_t c) +{ + return c >= 0x80 || (u'0' <= c && c <= u'9') || (u'A' <= c && c <= u'Z') || + (u'a' <= c && c <= u'z') || c == u'_' || c == u'-' || c == u'.'; +} + } // namespace -LinkParser::LinkParser(const QString &unparsedString) +namespace chatterino::linkparser { + +std::optional parse(const QString &source) noexcept { - this->match_ = unparsedString; + using SizeType = QString::size_type; + std::optional result; // This is not implemented with a regex to increase performance. - // We keep removing parts of the url until there's either nothing left or we fail. - QStringRef l(&unparsedString); - bool hasHttp = false; + QStringView link{source}; + strip(link); - // Protocol `https?://` - if (l.startsWith("https://", Qt::CaseInsensitive)) + QStringView remaining = link; + QStringView protocol; + + // Check protocol for https?:// + if (remaining.startsWith(u"http", Qt::CaseInsensitive) && + remaining.length() >= 4 + 3 + 1) // 'http' + '://' + [any] { - hasHttp = true; - l = l.mid(8); - } - else if (l.startsWith("http://", Qt::CaseInsensitive)) - { - hasHttp = true; - l = l.mid(7); + // optimistic view assuming there's a protocol (http or https) + auto withProto = remaining.mid(4); // 'http' + + if (withProto[0] == QChar(u's') || withProto[0] == QChar(u'S')) + { + withProto = withProto.mid(1); + } + + if (withProto.startsWith(u"://")) + { + // there's really a protocol => consume it + remaining = withProto.mid(3); + protocol = {link.begin(), remaining.begin()}; + } } - // Http basic auth `user:password`. - // Not supported for security reasons (misleading links) + // Http basic auth `user:password` isn't supported for security reasons (misleading links) // Host `a.b.c.com` - QStringRef host = l; + QStringView host = remaining; + QStringView rest; bool lastWasDot = true; - bool inIpv6 = false; + SizeType lastDotPos = -1; + SizeType nDots = 0; - for (int i = 0; i < l.size(); i++) + // Extract the host + for (SizeType i = 0; i < remaining.size(); i++) { - if (l[i] == '.') + char16_t currentChar = remaining[i].unicode(); + if (currentChar == u'.') { - if (lastWasDot == true) // no double dots .. - goto error; + if (lastWasDot) // no double dots .. + { + return result; + } + lastDotPos = i; lastWasDot = true; + nDots++; } else { lastWasDot = false; } - if (l[i] == ':' && !inIpv6) + // found a port + if (currentChar == u':') { - host = l.mid(0, i); - l = l.mid(i + 1); - goto parsePort; - } - else if (l[i] == '/') - { - host = l.mid(0, i); - l = l.mid(i + 1); - goto parsePath; - } - else if (l[i] == '?') - { - host = l.mid(0, i); - l = l.mid(i + 1); - goto parseQuery; - } - else if (l[i] == '#') - { - host = l.mid(0, i); - l = l.mid(i + 1); - goto parseAnchor; + host = remaining.mid(0, i); + rest = remaining.mid(i); + remaining = remaining.mid(i + 1); + + if (!startsWithPort(remaining)) + { + return result; + } + + break; } - // ipv6 - if (l[i] == '[') + // we accept everything in the path/query/anchor + if (currentChar == u'/' || currentChar == u'?' || currentChar == u'#') { - if (i == 0) - inIpv6 = true; - else - goto error; + host = remaining.mid(0, i); + rest = remaining.mid(i); + break; } - else if (l[i] == ']') + + if (!isValidDomainChar(currentChar)) { - inIpv6 = false; + return result; } } - if (lastWasDot) - goto error; - else - goto done; - -parsePort: - // Port `:12345` - for (int i = 0; i < std::min(5, l.size()); i++) + if (lastWasDot || lastDotPos <= 0) { - if (l[i] == '/') - goto parsePath; - else if (l[i] == '?') - goto parseQuery; - else if (l[i] == '#') - goto parseAnchor; - - if (!l[i].isDigit()) - goto error; + return result; } - goto done; - -parsePath: -parseQuery: -parseAnchor: - // we accept everything in the path/query/anchor - -done: - // check host - this->hasMatch_ = isValidHostname(host) || isValidIpv4(host) -#ifdef C_MATCH_IPV6_LINK - - || (hasHttp && isValidIpv6(host)) -#endif - ; - - if (this->hasMatch_) + // check host/tld + if ((nDots == 3 && isValidIpv4(host)) || + isValidTld(host.mid(lastDotPos + 1))) { - this->match_ = unparsedString; + // scan for parentheses (only if there were characters excluded) + if (link.end() != source.end() && !rest.empty()) + { + size_t nestingLevel = 0; + // position after the last closing brace (i.e. the minimum characters to include) + const auto *lastClose = link.end(); + + // scan source from rest until the end: + // lastClose + // v + // (example.com/foo/bar/#baz_(qox)), + // ▏╌╌rest (before)╌ ▏ + // ▏╌╌╌╌╌╌╌link (before)╌╌╌╌╌╌╌ ▏ + // ▏╌╌rest (after)╌╌╌ ▏ + // ▏╌╌╌╌╌╌╌link (after)╌╌╌╌╌╌╌╌╌ ▏ + // ▏╌╌╌╌╌╌╌╌╌╌╌╌╌source╌╌╌╌╌╌╌╌╌╌╌╌ ▏ + // ▏╌╌╌╌╌╌╌search╌╌╌╌╌╌ ▏ + for (const auto *it = rest.begin(); it < source.end(); it++) + { + if (it->unicode() == u'(') + { + nestingLevel++; + continue; + } + + if (nestingLevel != 0 && it->unicode() == u')') + { + nestingLevel--; + if (nestingLevel == 0) + { + lastClose = it + 1; + } + } + } + link = QStringView{link.begin(), std::max(link.end(), lastClose)}; + rest = QStringView{rest.begin(), std::max(rest.end(), lastClose)}; + } + result = Parsed{ + .protocol = protocol, + .host = host, + .rest = rest, + .link = link, + }; } - return; - -error: - hasMatch_ = false; + return result; } -bool LinkParser::hasMatch() const -{ - return this->hasMatch_; -} - -QString LinkParser::getCaptured() const -{ - return this->match_; -} - -} // namespace chatterino +} // namespace chatterino::linkparser diff --git a/src/common/LinkParser.hpp b/src/common/LinkParser.hpp index 907d22a80..151fb0834 100644 --- a/src/common/LinkParser.hpp +++ b/src/common/LinkParser.hpp @@ -1,21 +1,130 @@ #pragma once -#include #include -namespace chatterino { +#include -class LinkParser -{ -public: - explicit LinkParser(const QString &unparsedString); +namespace chatterino::linkparser { - bool hasMatch() const; - QString getCaptured() const; +/// @brief Represents a parsed link +/// +/// A parsed link is represented as views over the source string for its +/// different segments. In this simplified model, a link consists of an optional +/// @a protocol, a mandatory @a host and an optional @a rest. These segments are +/// always next to eachother in the input string, however together, they don't +/// span the whole input as it could contain prefixes or suffixes. +/// +/// Prefixes and suffixes are almost identical to the ones in GitHub Flavored +/// Markdown (GFM - https://github.github.com/gfm/#autolinks-extension-). +/// The main difference is that '_' isn't a valid suffix. +/// Parentheses are counted inside the @a rest: parsing "(a.com/(foo))" would +/// result in the link "a.com/(foo)". +/// Matching is done case insensitive (e.g. "HTTp://a.com" would be valid). +/// +/// A @a protocol can either be empty, "http://", or "https://". +/// A @a host can either be an IPv4 address or a hostname. The hostname must end +/// in a valid top level domain. Otherwise, there are no restrictions on it. +/// The @a rest can start with an optional port followed by either a '/', '?', +/// or '#'. +/// +/// @b Example +/// +/// ```text +/// (https://wiki.chatterino.com/Help/#overview) +/// ▏▏proto ▕ host ▏ rest ▏▏ +/// ▏▏ link ▏▏ +/// ▏ source ▏ +/// ``` +struct Parsed { + /// The parsed protocol of the link. Can be empty. + /// + /// ```text + /// https://www.forsen.tv/commands + /// ▏╌╌╌╌╌╌▕ + /// + /// www.forsen.tv/commands + /// (empty) + /// ``` + QStringView protocol; -private: - bool hasMatch_{false}; - QString match_; + /// The parsed host of the link. Can not be empty. + /// + /// ```text + /// https://www.forsen.tv/commands + /// ▏╌╌╌╌╌╌╌╌╌╌╌▕ + /// ``` + QStringView host; + + /// The remainder of the link. Can be empty. + /// + /// ```text + /// https://www.forsen.tv/commands + /// ▏╌╌╌╌╌╌╌▕ + /// + /// https://www.forsen.tv + /// (empty) + /// ``` + QStringView rest; + + /// The matched link. Can not be empty. + /// + /// ```text + /// (https://www.forsen.tv/commands) + /// ▏╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌▕ + /// ``` + QStringView link; + + /// Checks if the parsed link contains a prefix + bool hasPrefix(const QString &source) const noexcept + { + return this->link.begin() != source.begin(); + } + + /// The prefix before the parsed link inside @a source. May be empty. + /// + /// ```text + /// (https://www.forsen.tv/commands) + /// ^ + /// + /// https://www.forsen.tv/commands + /// (empty) + /// ``` + QStringView prefix(const QString &source) const noexcept + { + return {source.data(), this->link.begin()}; + } + + /// Checks if the parsed link contains a suffix + bool hasSuffix(const QString &source) const noexcept + { + return this->link.end() != source.end(); + } + + /// The suffix after the parsed link inside @a source. May be empty. + /// + /// ```text + /// (https://www.forsen.tv/commands) + /// ^ + /// + /// https://www.forsen.tv/commands + /// (empty) + /// ``` + QStringView suffix(const QString &source) const noexcept + { + return { + this->link.begin() + this->link.size(), + source.data() + source.length(), + }; + } }; -} // namespace chatterino +/// @brief Parses a link from @a source into its segments +/// +/// If no link is contained in @a source, `std::nullopt` will be returned. +/// The returned value is valid as long as @a source exists, as it contains +/// views into @a source. +/// +/// For the accepted links, see Parsed. +std::optional parse(const QString &source) noexcept; + +} // namespace chatterino::linkparser diff --git a/src/common/Literals.hpp b/src/common/Literals.hpp new file mode 100644 index 000000000..b7276d499 --- /dev/null +++ b/src/common/Literals.hpp @@ -0,0 +1,170 @@ +#pragma once + +#include + +/// This namespace defines the string suffixes _s, _ba, and _L1 used to create Qt types at compile-time. +/// They're easier to use comapred to their corresponding macros. +/// +/// * u"foobar"_s creates a QString (like QStringLiteral). The u prefix is required. +/// +/// * "foobar"_ba creates a QByteArray (like QByteArrayLiteral). +/// +/// * "foobar"_L1 creates a QLatin1String(-View). +namespace chatterino::literals { + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + +// This makes sure that the backing data never causes allocation after compilation. +// It's essentially the QStringLiteral macro inlined. +// +// From desktop-app/lib_base +// https://github.com/desktop-app/lib_base/blob/f904c60987115a4b514a575b23009ff25de0fafa/base/basic_types.h#L63-L152 +// And qt/qtbase (5.15) +// https://github.com/qt/qtbase/blob/29400a683f96867133b28299c0d0bd6bcf40df35/src/corelib/text/qstringliteral.h#L64-L104 +namespace detail { + // NOLINTBEGIN(modernize-avoid-c-arrays) + // NOLINTBEGIN(cppcoreguidelines-avoid-c-arrays) + + template + struct LiteralResolver { + template + constexpr LiteralResolver(const char16_t (&text)[N], + std::index_sequence /*seq*/) + : utf16Text{text[I]...} + { + } + template + constexpr LiteralResolver(const char (&text)[N], + std::index_sequence /*seq*/) + : latin1Text{text[I]...} + , latin1(true) + { + } + constexpr LiteralResolver(const char16_t (&text)[N]) + : LiteralResolver(text, std::make_index_sequence{}) + { + } + constexpr LiteralResolver(const char (&text)[N]) + : LiteralResolver(text, std::make_index_sequence{}) + { + } + + const char16_t utf16Text[N]{}; + const char latin1Text[N]{}; + size_t length = N; + bool latin1 = false; + }; + + template + struct StaticStringData { + template + constexpr StaticStringData(const char16_t (&text)[N], + std::index_sequence /*seq*/) + : data Q_STATIC_STRING_DATA_HEADER_INITIALIZER(N - 1) + , text{text[I]...} + { + } + QArrayData data; + char16_t text[N]; + + QStringData *pointer() + { + Q_ASSERT(data.ref.isStatic()); + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-static-cast-downcast) + return static_cast(&data); + } + }; + + template + struct StaticByteArrayData { + template + constexpr StaticByteArrayData(const char (&text)[N], + std::index_sequence /*seq*/) + : data Q_STATIC_BYTE_ARRAY_DATA_HEADER_INITIALIZER(N - 1) + , text{text[I]...} + { + } + QByteArrayData data; + char text[N]; + + QByteArrayData *pointer() + { + Q_ASSERT(data.ref.isStatic()); + return &data; + } + }; + + // NOLINTEND(cppcoreguidelines-avoid-c-arrays) + // NOLINTEND(modernize-avoid-c-arrays) + +} // namespace detail + +template +inline QString operator""_s() noexcept +{ + static_assert(R.length > 0); // always has a terminating null + static_assert(!R.latin1, "QString literals must be made up of 16bit " + "characters. Forgot a u\"\"?"); + + static auto literal = detail::StaticStringData( + R.utf16Text, std::make_index_sequence{}); + return QString{QStringDataPtr{literal.pointer()}}; +}; + +template +inline QByteArray operator""_ba() noexcept +{ + static_assert(R.length > 0); // always has a terminating null + static_assert(R.latin1, "QByteArray literals must be made up of 8bit " + "characters. Misplaced u\"\"?"); + + static auto literal = detail::StaticByteArrayData( + R.latin1Text, std::make_index_sequence{}); + return QByteArray{QByteArrayDataPtr{literal.pointer()}}; +}; + +#elif QT_VERSION < QT_VERSION_CHECK(6, 4, 0) + +// The operators were added in 6.4, but their implementation works in any 6.x version. +// +// NOLINTBEGIN(cppcoreguidelines-pro-type-const-cast) +inline QString operator""_s(const char16_t *str, size_t size) noexcept +{ + return QString( + QStringPrivate(nullptr, const_cast(str), qsizetype(size))); +} + +inline QByteArray operator""_ba(const char *str, size_t size) noexcept +{ + return QByteArray( + QByteArrayData(nullptr, const_cast(str), qsizetype(size))); +} +// NOLINTEND(cppcoreguidelines-pro-type-const-cast) + +#else + +inline QString operator""_s(const char16_t *str, size_t size) noexcept +{ + return Qt::Literals::StringLiterals::operator""_s(str, size); +} + +inline QByteArray operator""_ba(const char *str, size_t size) noexcept +{ + return Qt::Literals::StringLiterals::operator""_ba(str, size); +} + +#endif + +constexpr inline QLatin1String operator""_L1(const char *str, + size_t size) noexcept +{ +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + using SizeType = int; +#else + using SizeType = qsizetype; +#endif + + return QLatin1String{str, static_cast(size)}; +} + +} // namespace chatterino::literals diff --git a/src/common/Modes.cpp b/src/common/Modes.cpp index ead79dea9..588a304f1 100644 --- a/src/common/Modes.cpp +++ b/src/common/Modes.cpp @@ -1,4 +1,4 @@ -#include "Modes.hpp" +#include "common/Modes.hpp" #include "util/CombinePath.hpp" diff --git a/src/common/NetworkManager.cpp b/src/common/NetworkManager.cpp deleted file mode 100644 index 1ff1bf635..000000000 --- a/src/common/NetworkManager.cpp +++ /dev/null @@ -1,22 +0,0 @@ -#include "common/NetworkManager.hpp" - -#include - -namespace chatterino { - -QThread NetworkManager::workerThread; -QNetworkAccessManager NetworkManager::accessManager; - -void NetworkManager::init() -{ - NetworkManager::accessManager.moveToThread(&NetworkManager::workerThread); - NetworkManager::workerThread.start(); -} - -void NetworkManager::deinit() -{ - NetworkManager::workerThread.quit(); - NetworkManager::workerThread.wait(); -} - -} // namespace chatterino diff --git a/src/common/NetworkPrivate.cpp b/src/common/NetworkPrivate.cpp deleted file mode 100644 index c49fcff97..000000000 --- a/src/common/NetworkPrivate.cpp +++ /dev/null @@ -1,403 +0,0 @@ -#include "common/NetworkPrivate.hpp" - -#include "common/NetworkManager.hpp" -#include "common/NetworkResult.hpp" -#include "common/Outcome.hpp" -#include "debug/AssertInGuiThread.hpp" -#include "singletons/Paths.hpp" -#include "util/DebugCount.hpp" -#include "util/PostToThread.hpp" - -#include -#include -#include -#include -#include "common/QLogging.hpp" - -namespace chatterino { - -NetworkData::NetworkData() - : lifetimeManager_(new QObject) -{ - DebugCount::increase("NetworkData"); -} - -NetworkData::~NetworkData() -{ - this->lifetimeManager_->deleteLater(); - - DebugCount::decrease("NetworkData"); -} - -QString NetworkData::getHash() -{ - static std::mutex mu; - - std::lock_guard lock(mu); - - if (this->hash_.isEmpty()) - { - QByteArray bytes; - - bytes.append(this->request_.url().toString().toUtf8()); - - for (const auto &header : this->request_.rawHeaderList()) - { - bytes.append(header); - } - - QByteArray hashBytes( - QCryptographicHash::hash(bytes, QCryptographicHash::Sha256)); - - this->hash_ = hashBytes.toHex(); - } - - return this->hash_; -} - -void writeToCache(const std::shared_ptr &data, - const QByteArray &bytes) -{ - if (data->cache_) - { - QtConcurrent::run([data, bytes] { - QFile cachedFile(getPaths()->cacheDirectory() + "/" + - data->getHash()); - - if (cachedFile.open(QIODevice::WriteOnly)) - { - cachedFile.write(bytes); - } - }); - } -} - -void loadUncached(const std::shared_ptr &data) -{ - DebugCount::increase("http request started"); - - NetworkRequester requester; - NetworkWorker *worker = new NetworkWorker; - - worker->moveToThread(&NetworkManager::workerThread); - - auto onUrlRequested = [data, worker]() mutable { - if (data->hasTimeout_) - { - data->timer_ = new QTimer(); - data->timer_->setSingleShot(true); - data->timer_->start(data->timeoutMS_); - } - - auto reply = [&]() -> QNetworkReply * { - switch (data->requestType_) - { - case NetworkRequestType::Get: - return NetworkManager::accessManager.get(data->request_); - - case NetworkRequestType::Put: - return NetworkManager::accessManager.put(data->request_, - data->payload_); - - case NetworkRequestType::Delete: - return NetworkManager::accessManager.deleteResource( - data->request_); - - case NetworkRequestType::Post: - if (data->multiPartPayload_) - { - assert(data->payload_.isNull()); - - return NetworkManager::accessManager.post( - data->request_, data->multiPartPayload_); - } - else - { - return NetworkManager::accessManager.post( - data->request_, data->payload_); - } - case NetworkRequestType::Patch: - if (data->multiPartPayload_) - { - assert(data->payload_.isNull()); - - return NetworkManager::accessManager.sendCustomRequest( - data->request_, "PATCH", data->multiPartPayload_); - } - else - { - return NetworkManager::accessManager.sendCustomRequest( - data->request_, "PATCH", data->payload_); - } - } - return nullptr; - }(); - - if (reply == nullptr) - { - qCDebug(chatterinoCommon) << "Unhandled request type"; - return; - } - - if (data->timer_ != nullptr && data->timer_->isActive()) - { - QObject::connect( - data->timer_, &QTimer::timeout, worker, [reply, data]() { - qCDebug(chatterinoCommon) << "Aborted!"; - reply->abort(); - qCDebug(chatterinoHTTP) - << QString("%1 [timed out] %2") - .arg(networkRequestTypes.at( - int(data->requestType_)), - data->request_.url().toString()); - - if (data->onError_) - { - postToThread([data] { - data->onError_(NetworkResult( - {}, NetworkResult::timedoutStatus)); - }); - } - - if (data->finally_) - { - postToThread([data] { - data->finally_(); - }); - } - }); - } - - if (data->onReplyCreated_) - { - data->onReplyCreated_(reply); - } - - auto handleReply = [data, reply]() mutable { - if (data->hasCaller_ && !data->caller_.get()) - { - return; - } - - // TODO(pajlada): A reply was received, kill the timeout timer - if (reply->error() != QNetworkReply::NetworkError::NoError) - { - if (reply->error() == - QNetworkReply::NetworkError::OperationCanceledError) - { - // Operation cancelled, most likely timed out - qCDebug(chatterinoHTTP) - << QString("%1 [cancelled] %2") - .arg(networkRequestTypes.at( - int(data->requestType_)), - data->request_.url().toString()); - return; - } - - if (data->onError_) - { - auto status = reply->attribute( - QNetworkRequest::HttpStatusCodeAttribute); - if (data->requestType_ == NetworkRequestType::Get) - { - qCDebug(chatterinoHTTP) - << QString("%1 %2 %3") - .arg(networkRequestTypes.at( - int(data->requestType_)), - QString::number(status.toInt()), - data->request_.url().toString()); - } - else - { - qCDebug(chatterinoHTTP) - << QString("%1 %2 %3 %4") - .arg(networkRequestTypes.at( - int(data->requestType_)), - QString::number(status.toInt()), - data->request_.url().toString(), - QString(data->payload_)); - } - // TODO: Should this always be run on the GUI thread? - postToThread([data, code = status.toInt()] { - data->onError_(NetworkResult({}, code)); - }); - } - - if (data->finally_) - { - postToThread([data] { - data->finally_(); - }); - } - return; - } - - QByteArray bytes = reply->readAll(); - writeToCache(data, bytes); - - auto status = - reply->attribute(QNetworkRequest::HttpStatusCodeAttribute); - - NetworkResult result(bytes, status.toInt()); - - DebugCount::increase("http request success"); - // log("starting {}", data->request_.url().toString()); - if (data->onSuccess_) - { - if (data->executeConcurrently_) - QtConcurrent::run([onSuccess = std::move(data->onSuccess_), - result = std::move(result)] { - onSuccess(result); - }); - else - data->onSuccess_(result); - } - // log("finished {}", data->request_.url().toString()); - - reply->deleteLater(); - - if (data->requestType_ == NetworkRequestType::Get) - { - qCDebug(chatterinoHTTP) - << QString("%1 %2 %3") - .arg(networkRequestTypes.at(int(data->requestType_)), - QString::number(status.toInt()), - data->request_.url().toString()); - } - else - { - qCDebug(chatterinoHTTP) - << QString("%1 %3 %2 %4") - .arg(networkRequestTypes.at(int(data->requestType_)), - data->request_.url().toString(), - QString::number(status.toInt()), - QString(data->payload_)); - } - if (data->finally_) - { - if (data->executeConcurrently_) - QtConcurrent::run([finally = std::move(data->finally_)] { - finally(); - }); - else - data->finally_(); - } - }; - - if (data->timer_ != nullptr) - { - QObject::connect(reply, &QNetworkReply::finished, data->timer_, - &QObject::deleteLater); - } - - QObject::connect( - reply, &QNetworkReply::finished, worker, - [data, handleReply, worker]() mutable { - if (data->executeConcurrently_ || isGuiThread()) - { - handleReply(); - delete worker; - } - else - { - postToThread( - [worker, cb = std::move(handleReply)]() mutable { - cb(); - delete worker; - }); - } - }); - }; - - QObject::connect(&requester, &NetworkRequester::requestUrl, worker, - onUrlRequested); - - emit requester.requestUrl(); -} - -// First tried to load cached, then uncached. -void loadCached(const std::shared_ptr &data) -{ - QFile cachedFile(getPaths()->cacheDirectory() + "/" + data->getHash()); - - if (!cachedFile.exists() || !cachedFile.open(QIODevice::ReadOnly)) - { - // File didn't exist OR File could not be opened - loadUncached(data); - return; - } - else - { - // XXX: check if bytes is empty? - QByteArray bytes = cachedFile.readAll(); - NetworkResult result(bytes, 200); - - qCDebug(chatterinoHTTP) - << QString("%1 [CACHED] 200 %2") - .arg(networkRequestTypes.at(int(data->requestType_)), - data->request_.url().toString()); - if (data->onSuccess_) - { - if (data->executeConcurrently_ || isGuiThread()) - { - // XXX: If outcome is Failure, we should invalidate the cache file - // somehow/somewhere - /*auto outcome =*/ - if (data->hasCaller_ && !data->caller_.get()) - { - return; - } - data->onSuccess_(result); - } - else - { - postToThread([data, result]() { - if (data->hasCaller_ && !data->caller_.get()) - { - return; - } - - data->onSuccess_(result); - }); - } - } - - if (data->finally_) - { - if (data->executeConcurrently_ || isGuiThread()) - { - if (data->hasCaller_ && !data->caller_.get()) - { - return; - } - - data->finally_(); - } - else - { - postToThread([data]() { - if (data->hasCaller_ && !data->caller_.get()) - { - return; - } - - data->finally_(); - }); - } - } - } -} - -void load(const std::shared_ptr &data) -{ - if (data->cache_) - { - QtConcurrent::run(loadCached, data); - } - else - { - loadUncached(data); - } -} - -} // namespace chatterino diff --git a/src/common/NetworkPrivate.hpp b/src/common/NetworkPrivate.hpp deleted file mode 100644 index 050207cee..000000000 --- a/src/common/NetworkPrivate.hpp +++ /dev/null @@ -1,72 +0,0 @@ -#pragma once - -#include "common/NetworkCommon.hpp" -#include "util/QObjectRef.hpp" - -#include -#include -#include -#include -#include - -class QNetworkReply; - -namespace chatterino { - -class NetworkResult; - -class NetworkRequester : public QObject -{ - Q_OBJECT - -signals: - void requestUrl(); -}; - -class NetworkWorker : public QObject -{ - Q_OBJECT - -signals: - void doneUrl(); -}; - -struct NetworkData { - NetworkData(); - ~NetworkData(); - - QNetworkRequest request_; - bool hasCaller_{}; - QObjectRef caller_; - bool cache_{}; - bool executeConcurrently_{}; - - NetworkReplyCreatedCallback onReplyCreated_; - NetworkErrorCallback onError_; - NetworkSuccessCallback onSuccess_; - NetworkFinallyCallback finally_; - - NetworkRequestType requestType_ = NetworkRequestType::Get; - - QByteArray payload_; - // lifetime secured by lifetimeManager_ - QHttpMultiPart *multiPartPayload_{}; - - // Timer that tracks the timeout - // By default, there's no explicit timeout for the request - // to enable the timer, the "setTimeout" function needs to be called before - // execute is called - bool hasTimeout_{}; - int timeoutMS_{}; - QTimer *timer_ = nullptr; - QObject *lifetimeManager_; - - QString getHash(); - -private: - QString hash_; -}; - -void load(const std::shared_ptr &data); - -} // namespace chatterino diff --git a/src/common/NetworkRequest.cpp b/src/common/NetworkRequest.cpp deleted file mode 100644 index 70cfa4979..000000000 --- a/src/common/NetworkRequest.cpp +++ /dev/null @@ -1,198 +0,0 @@ -#include "common/NetworkRequest.hpp" - -#include "common/NetworkPrivate.hpp" -#include "common/Outcome.hpp" -#include "common/Version.hpp" -#include "debug/AssertInGuiThread.hpp" -#include "providers/twitch/TwitchCommon.hpp" -#include "singletons/Paths.hpp" -#include "util/DebugCount.hpp" -#include "util/PostToThread.hpp" - -#include -#include -#include -#include "common/QLogging.hpp" - -#include - -namespace chatterino { - -NetworkRequest::NetworkRequest(const std::string &url, - NetworkRequestType requestType) - : data(new NetworkData) -{ - this->data->request_.setUrl(QUrl(QString::fromStdString(url))); - this->data->requestType_ = requestType; - - this->initializeDefaultValues(); -} - -NetworkRequest::NetworkRequest(QUrl url, NetworkRequestType requestType) - : data(new NetworkData) -{ - this->data->request_.setUrl(url); - this->data->requestType_ = requestType; - - this->initializeDefaultValues(); -} - -NetworkRequest::~NetworkRequest() -{ - //assert(!this->data || this->executed_); -} - -NetworkRequest NetworkRequest::type(NetworkRequestType newRequestType) && -{ - this->data->requestType_ = newRequestType; - return std::move(*this); -} - -NetworkRequest NetworkRequest::caller(const QObject *caller) && -{ - if (caller) - { - // Caller must be in gui thread - assert(caller->thread() == qApp->thread()); - - this->data->caller_ = const_cast(caller); - this->data->hasCaller_ = true; - } - return std::move(*this); -} - -NetworkRequest NetworkRequest::onReplyCreated(NetworkReplyCreatedCallback cb) && -{ - this->data->onReplyCreated_ = cb; - return std::move(*this); -} - -NetworkRequest NetworkRequest::onError(NetworkErrorCallback cb) && -{ - this->data->onError_ = cb; - return std::move(*this); -} - -NetworkRequest NetworkRequest::onSuccess(NetworkSuccessCallback cb) && -{ - this->data->onSuccess_ = cb; - return std::move(*this); -} - -NetworkRequest NetworkRequest::finally(NetworkFinallyCallback cb) && -{ - this->data->finally_ = cb; - return std::move(*this); -} - -NetworkRequest NetworkRequest::header(const char *headerName, - const char *value) && -{ - this->data->request_.setRawHeader(headerName, value); - return std::move(*this); -} - -NetworkRequest NetworkRequest::header(const char *headerName, - const QByteArray &value) && -{ - this->data->request_.setRawHeader(headerName, value); - return std::move(*this); -} - -NetworkRequest NetworkRequest::header(const char *headerName, - const QString &value) && -{ - this->data->request_.setRawHeader(headerName, value.toUtf8()); - return std::move(*this); -} - -NetworkRequest NetworkRequest::headerList( - const std::vector> &headers) && -{ - for (const auto &[headerName, headerValue] : headers) - { - this->data->request_.setRawHeader(headerName, headerValue); - } - return std::move(*this); -} - -NetworkRequest NetworkRequest::timeout(int ms) && -{ - this->data->hasTimeout_ = true; - this->data->timeoutMS_ = ms; - return std::move(*this); -} - -NetworkRequest NetworkRequest::concurrent() && -{ - this->data->executeConcurrently_ = true; - return std::move(*this); -} - -NetworkRequest NetworkRequest::authorizeTwitchV5(const QString &clientID, - const QString &oauthToken) && -{ - // TODO: make two overloads, with and without oauth token - auto tmp = std::move(*this) - .header("Client-ID", clientID) - .header("Accept", "application/vnd.twitchtv.v5+json"); - - if (!oauthToken.isEmpty()) - return std::move(tmp).header("Authorization", "OAuth " + oauthToken); - else - return tmp; -} - -NetworkRequest NetworkRequest::multiPart(QHttpMultiPart *payload) && -{ - payload->setParent(this->data->lifetimeManager_); - this->data->multiPartPayload_ = payload; - return std::move(*this); -} - -NetworkRequest NetworkRequest::payload(const QByteArray &payload) && -{ - this->data->payload_ = payload; - return std::move(*this); -} - -NetworkRequest NetworkRequest::cache() && -{ - this->data->cache_ = true; - return std::move(*this); -} - -void NetworkRequest::execute() -{ - this->executed_ = true; - - // Only allow caching for GET request - if (this->data->cache_ && - this->data->requestType_ != NetworkRequestType::Get) - { - qCDebug(chatterinoCommon) << "Can only cache GET requests!"; - this->data->cache_ = false; - } - - // Can not have a caller and be concurrent at the same time. - assert(!(this->data->caller_ && this->data->executeConcurrently_)); - - load(std::move(this->data)); -} - -void NetworkRequest::initializeDefaultValues() -{ - const auto userAgent = QString("chatterino/%1 (%2)") - .arg(CHATTERINO_VERSION, CHATTERINO_GIT_HASH) - .toUtf8(); - - this->data->request_.setRawHeader("User-Agent", userAgent); -} - -// Helper creator functions -NetworkRequest NetworkRequest::twitchRequest(QUrl url) -{ - return NetworkRequest(url).authorizeTwitchV5(getDefaultClientID()); -} - -} // namespace chatterino diff --git a/src/common/NetworkResult.hpp b/src/common/NetworkResult.hpp deleted file mode 100644 index 4edd6ad4a..000000000 --- a/src/common/NetworkResult.hpp +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once - -#include -#include -#include - -namespace chatterino { - -class NetworkResult -{ -public: - NetworkResult(const QByteArray &data, int status); - - /// Parses the result as json and returns the root as an object. - /// Returns empty object if parsing failed. - QJsonObject parseJson() const; - /// Parses the result as json and returns the root as an array. - /// Returns empty object if parsing failed. - QJsonArray parseJsonArray() const; - /// Parses the result as json and returns the document. - rapidjson::Document parseRapidJson() const; - const QByteArray &getData() const; - int status() const; - - static constexpr int timedoutStatus = -2; - -private: - QByteArray data_; - int status_; -}; - -} // namespace chatterino diff --git a/src/common/NullablePtr.hpp b/src/common/NullablePtr.hpp deleted file mode 100644 index 9fa1b9fb6..000000000 --- a/src/common/NullablePtr.hpp +++ /dev/null @@ -1,73 +0,0 @@ -#pragma once - -#include - -namespace chatterino { - -template -class NullablePtr -{ -public: - NullablePtr() - : element_(nullptr) - { - } - - NullablePtr(T *element) - : element_(element) - { - } - - T *operator->() const - { - assert(this->hasElement()); - - return element_; - } - - typename std::add_lvalue_reference::type operator*() const - { - assert(this->hasElement()); - - return *element_; - } - - T *get() const - { - assert(this->hasElement()); - - return this->element_; - } - - bool isNull() const - { - return this->element_ == nullptr; - } - - bool hasElement() const - { - return this->element_ != nullptr; - } - - operator bool() const - { - return this->hasElement(); - } - - bool operator!() const - { - return !this->hasElement(); - } - - template ::value>> - operator NullablePtr() const - { - return NullablePtr(this->element_); - } - -private: - T *element_; -}; - -} // namespace chatterino diff --git a/src/common/QLogging.cpp b/src/common/QLogging.cpp index 3d343de3a..a8cd8285d 100644 --- a/src/common/QLogging.cpp +++ b/src/common/QLogging.cpp @@ -1,6 +1,6 @@ #include "common/QLogging.hpp" -#ifdef DEBUG_OFF +#ifdef NDEBUG static constexpr QtMsgType logThreshold = QtWarningMsg; #else static constexpr QtMsgType logThreshold = QtDebugMsg; @@ -11,7 +11,10 @@ Q_LOGGING_CATEGORY(chatterinoArgs, "chatterino.args", logThreshold); Q_LOGGING_CATEGORY(chatterinoBenchmark, "chatterino.benchmark", logThreshold); Q_LOGGING_CATEGORY(chatterinoBttv, "chatterino.bttv", logThreshold); Q_LOGGING_CATEGORY(chatterinoCache, "chatterino.cache", logThreshold); +Q_LOGGING_CATEGORY(chatterinoCommands, "chatterino.commands", logThreshold); Q_LOGGING_CATEGORY(chatterinoCommon, "chatterino.common", logThreshold); +Q_LOGGING_CATEGORY(chatterinoCrashhandler, "chatterino.crashhandler", + logThreshold); Q_LOGGING_CATEGORY(chatterinoEmoji, "chatterino.emoji", logThreshold); Q_LOGGING_CATEGORY(chatterinoEnv, "chatterino.env", logThreshold); Q_LOGGING_CATEGORY(chatterinoFfzemotes, "chatterino.ffzemotes", logThreshold); @@ -22,24 +25,37 @@ Q_LOGGING_CATEGORY(chatterinoHTTP, "chatterino.http", logThreshold); Q_LOGGING_CATEGORY(chatterinoImage, "chatterino.image", logThreshold); Q_LOGGING_CATEGORY(chatterinoIrc, "chatterino.irc", logThreshold); Q_LOGGING_CATEGORY(chatterinoIvr, "chatterino.ivr", logThreshold); +Q_LOGGING_CATEGORY(chatterinoLiveupdates, "chatterino.liveupdates", + logThreshold); +Q_LOGGING_CATEGORY(chatterinoLua, "chatterino.lua", logThreshold); Q_LOGGING_CATEGORY(chatterinoMain, "chatterino.main", logThreshold); Q_LOGGING_CATEGORY(chatterinoMessage, "chatterino.message", logThreshold); Q_LOGGING_CATEGORY(chatterinoNativeMessage, "chatterino.nativemessage", logThreshold); +Q_LOGGING_CATEGORY(chatterinoNetwork, "chatterino.network", logThreshold); Q_LOGGING_CATEGORY(chatterinoNotification, "chatterino.notification", logThreshold); -Q_LOGGING_CATEGORY(chatterinoNuulsuploader, "chatterino.nuulsuploader", +Q_LOGGING_CATEGORY(chatterinoImageuploader, "chatterino.imageuploader", logThreshold); Q_LOGGING_CATEGORY(chatterinoPubSub, "chatterino.pubsub", logThreshold); Q_LOGGING_CATEGORY(chatterinoRecentMessages, "chatterino.recentmessages", logThreshold); -Q_LOGGING_CATEGORY(chatterinoStreamlink, "chatterino.streamlink", logThreshold); +Q_LOGGING_CATEGORY(chatterinoSettings, "chatterino.settings", logThreshold); +Q_LOGGING_CATEGORY(chatterinoSeventv, "chatterino.seventv", logThreshold); +Q_LOGGING_CATEGORY(chatterinoSeventvEventAPI, "chatterino.seventv.eventapi", + logThreshold); +Q_LOGGING_CATEGORY(chatterinoSound, "chatterino.sound", logThreshold); Q_LOGGING_CATEGORY(chatterinoStreamerMode, "chatterino.streamermode", logThreshold); +Q_LOGGING_CATEGORY(chatterinoStreamlink, "chatterino.streamlink", logThreshold); +Q_LOGGING_CATEGORY(chatterinoTheme, "chatterino.theme", logThreshold); Q_LOGGING_CATEGORY(chatterinoTokenizer, "chatterino.tokenizer", logThreshold); Q_LOGGING_CATEGORY(chatterinoTwitch, "chatterino.twitch", logThreshold); +Q_LOGGING_CATEGORY(chatterinoTwitchLiveController, + "chatterino.twitch.livecontroller", logThreshold); Q_LOGGING_CATEGORY(chatterinoUpdate, "chatterino.update", logThreshold); Q_LOGGING_CATEGORY(chatterinoWebsocket, "chatterino.websocket", logThreshold); Q_LOGGING_CATEGORY(chatterinoWidget, "chatterino.widget", logThreshold); Q_LOGGING_CATEGORY(chatterinoWindowmanager, "chatterino.windowmanager", logThreshold); +Q_LOGGING_CATEGORY(chatterinoXDG, "chatterino.xdg", logThreshold); diff --git a/src/common/QLogging.hpp b/src/common/QLogging.hpp index d9a0a0eee..b814bb332 100644 --- a/src/common/QLogging.hpp +++ b/src/common/QLogging.hpp @@ -7,7 +7,9 @@ Q_DECLARE_LOGGING_CATEGORY(chatterinoArgs); Q_DECLARE_LOGGING_CATEGORY(chatterinoBenchmark); Q_DECLARE_LOGGING_CATEGORY(chatterinoBttv); Q_DECLARE_LOGGING_CATEGORY(chatterinoCache); +Q_DECLARE_LOGGING_CATEGORY(chatterinoCommands); Q_DECLARE_LOGGING_CATEGORY(chatterinoCommon); +Q_DECLARE_LOGGING_CATEGORY(chatterinoCrashhandler); Q_DECLARE_LOGGING_CATEGORY(chatterinoEmoji); Q_DECLARE_LOGGING_CATEGORY(chatterinoEnv); Q_DECLARE_LOGGING_CATEGORY(chatterinoFfzemotes); @@ -16,20 +18,30 @@ Q_DECLARE_LOGGING_CATEGORY(chatterinoHighlights); Q_DECLARE_LOGGING_CATEGORY(chatterinoHotkeys); Q_DECLARE_LOGGING_CATEGORY(chatterinoHTTP); Q_DECLARE_LOGGING_CATEGORY(chatterinoImage); +Q_DECLARE_LOGGING_CATEGORY(chatterinoImageuploader); Q_DECLARE_LOGGING_CATEGORY(chatterinoIrc); Q_DECLARE_LOGGING_CATEGORY(chatterinoIvr); +Q_DECLARE_LOGGING_CATEGORY(chatterinoLiveupdates); +Q_DECLARE_LOGGING_CATEGORY(chatterinoLua); Q_DECLARE_LOGGING_CATEGORY(chatterinoMain); Q_DECLARE_LOGGING_CATEGORY(chatterinoMessage); Q_DECLARE_LOGGING_CATEGORY(chatterinoNativeMessage); +Q_DECLARE_LOGGING_CATEGORY(chatterinoNetwork); Q_DECLARE_LOGGING_CATEGORY(chatterinoNotification); -Q_DECLARE_LOGGING_CATEGORY(chatterinoNuulsuploader); Q_DECLARE_LOGGING_CATEGORY(chatterinoPubSub); Q_DECLARE_LOGGING_CATEGORY(chatterinoRecentMessages); -Q_DECLARE_LOGGING_CATEGORY(chatterinoStreamlink); +Q_DECLARE_LOGGING_CATEGORY(chatterinoSettings); +Q_DECLARE_LOGGING_CATEGORY(chatterinoSeventv); +Q_DECLARE_LOGGING_CATEGORY(chatterinoSeventvEventAPI); +Q_DECLARE_LOGGING_CATEGORY(chatterinoSound); Q_DECLARE_LOGGING_CATEGORY(chatterinoStreamerMode); +Q_DECLARE_LOGGING_CATEGORY(chatterinoStreamlink); +Q_DECLARE_LOGGING_CATEGORY(chatterinoTheme); Q_DECLARE_LOGGING_CATEGORY(chatterinoTokenizer); Q_DECLARE_LOGGING_CATEGORY(chatterinoTwitch); +Q_DECLARE_LOGGING_CATEGORY(chatterinoTwitchLiveController); Q_DECLARE_LOGGING_CATEGORY(chatterinoUpdate); Q_DECLARE_LOGGING_CATEGORY(chatterinoWebsocket); Q_DECLARE_LOGGING_CATEGORY(chatterinoWidget); Q_DECLARE_LOGGING_CATEGORY(chatterinoWindowmanager); +Q_DECLARE_LOGGING_CATEGORY(chatterinoXDG); diff --git a/src/common/SignalVector.hpp b/src/common/SignalVector.hpp index 96dfbe1c0..e01ebd5aa 100644 --- a/src/common/SignalVector.hpp +++ b/src/common/SignalVector.hpp @@ -1,12 +1,12 @@ #pragma once +#include "debug/AssertInGuiThread.hpp" + +#include #include #include -#include -#include -#include -#include "debug/AssertInGuiThread.hpp" +#include namespace chatterino { @@ -18,7 +18,7 @@ struct SignalVectorItemEvent { }; template -class SignalVector : boost::noncopyable +class SignalVector { public: pajlada::Signals::Signal> itemInserted; @@ -38,10 +38,16 @@ public: SignalVector(std::function &&compare) : SignalVector() { - itemCompare_ = std::move(compare); + this->itemCompare_ = std::move(compare); } - virtual bool isSorted() const + SignalVector(const SignalVector &) = delete; + SignalVector &operator=(const SignalVector &) = delete; + + SignalVector(SignalVector &&) = delete; + SignalVector &operator=(SignalVector &&) = delete; + + bool isSorted() const { return bool(this->itemCompare_); } @@ -76,9 +82,13 @@ public: else { if (index == -1) + { index = this->items_.size(); + } else + { assert(index >= 0 && index <= this->items_.size()); + } this->items_.insert(this->items_.begin() + index, item); } @@ -117,6 +127,27 @@ public: this->itemsChanged_(); } + bool removeFirstMatching(std::function matcher, + void *caller = nullptr) + { + assertInGuiThread(); + + for (int index = 0; index < this->items_.size(); ++index) + { + T item = this->items_[index]; + if (matcher(item)) + { + this->items_.erase(this->items_.begin() + index); + SignalVectorItemEvent args{item, index, caller}; + this->itemRemoved.invoke(args); + this->itemsChanged_(); + return true; + } + } + + return false; + } + const std::vector &raw() const { assertInGuiThread(); @@ -145,7 +176,7 @@ public: decltype(auto) operator[](size_t index) { assertInGuiThread(); - return this->items[index]; + return this->items_[index]; } auto empty() diff --git a/src/common/SignalVectorModel.hpp b/src/common/SignalVectorModel.hpp index 5054eaffc..620ca452d 100644 --- a/src/common/SignalVectorModel.hpp +++ b/src/common/SignalVectorModel.hpp @@ -2,15 +2,21 @@ #include "common/SignalVector.hpp" +#include #include #include #include -#include -#include +#include namespace chatterino { +template +class SignalVector; + +template +struct SignalVectorItemEvent; + template class SignalVectorModel : public QAbstractTableModel, pajlada::Signals::SignalHolder @@ -95,7 +101,7 @@ public: return this; } - virtual ~SignalVectorModel() + ~SignalVectorModel() override { for (Row &row : this->rows_) { @@ -122,7 +128,8 @@ public: QVariant data(const QModelIndex &index, int role) const override { - int row = index.row(), column = index.column(); + int row = index.row(); + int column = index.column(); if (row < 0 || column < 0 || row >= this->rows_.size() || column >= this->columnCount_) { @@ -135,7 +142,8 @@ public: bool setData(const QModelIndex &index, const QVariant &value, int role) override { - int row = index.row(), column = index.column(); + int row = index.row(); + int column = index.column(); if (row < 0 || column < 0 || row >= this->rows_.size() || column >= this->columnCount_) { @@ -157,12 +165,22 @@ public: else { int vecRow = this->getVectorIndexFromModelIndex(row); + // TODO: This is only a safety-thing for when we modify data that's being modified right now. + // It should not be necessary, but it would require some rethinking about this surrounding logic + if (vecRow >= this->vector_->readOnly()->size()) + { + return false; + } this->vector_->removeAt(vecRow, this); assert(this->rows_[row].original); TVectorItem item = this->getItemFromRow( - this->rows_[row].items, this->rows_[row].original.get()); + this->rows_[row].items, this->rows_[row].original.value()); this->vector_->insert(item, vecRow, this); + + QVector roles = QVector(); + roles.append(role); + emit dataChanged(index, index, roles); } return true; @@ -257,7 +275,7 @@ public: TVectorItem item = this->getItemFromRow(this->rows_[sourceRow].items, - this->rows_[sourceRow].original.get()); + this->rows_[sourceRow].original.value()); this->vector_->removeAt(signalVectorRow); this->vector_->insert( item, this->getVectorIndexFromModelIndex(destinationChild)); @@ -300,10 +318,12 @@ public: for (auto &&x : list) { if (x.row() != list.first().row()) + { return nullptr; + } } - auto data = new QMimeData; + auto *data = new QMimeData; data->setData("chatterino_row_id", QByteArray::number(list[0].row())); return data; } @@ -328,7 +348,7 @@ public: if (from != to) { - this->moveRow(this->index(from, to), from, parent, to); + this->moveRow(this->index(from, 0), from, parent, to); } // We return false since we remove items ourselves. @@ -412,7 +432,7 @@ protected: struct Row { std::vector items; - boost::optional original; + std::optional original; bool isCustomRow; Row(std::vector _items, bool _isCustomRow = false) diff --git a/src/common/Singleton.hpp b/src/common/Singleton.hpp deleted file mode 100644 index 401716d78..000000000 --- a/src/common/Singleton.hpp +++ /dev/null @@ -1,26 +0,0 @@ -#pragma once - -#include - -namespace chatterino { - -class Settings; -class Paths; - -class Singleton : boost::noncopyable -{ -public: - virtual ~Singleton() = default; - - virtual void initialize(Settings &settings, Paths &paths) - { - (void)(settings); - (void)(paths); - } - - virtual void save() - { - } -}; - -} // namespace chatterino diff --git a/src/common/Version.cpp b/src/common/Version.cpp index 9a5d4e978..5e23272cb 100644 --- a/src/common/Version.cpp +++ b/src/common/Version.cpp @@ -1,30 +1,21 @@ #include "common/Version.hpp" +#include "common/Literals.hpp" #include "common/Modes.hpp" #include - -#define UGLYMACROHACK1(s) #s -#define FROM_EXTERNAL_DEFINE(s) UGLYMACROHACK1(s) +#include namespace chatterino { +using namespace literals; + Version::Version() + : version_(CHATTERINO_VERSION) + , commitHash_(QStringLiteral(CHATTERINO_GIT_HASH)) + , isModified_(CHATTERINO_GIT_MODIFIED == 1) + , dateOfBuild_(QStringLiteral(CHATTERINO_CMAKE_GEN_DATE)) { - this->version_ = CHATTERINO_VERSION; - - this->commitHash_ = - QString(FROM_EXTERNAL_DEFINE(CHATTERINO_GIT_HASH)).remove('"'); - -#ifdef CHATTERINO_GIT_MODIFIED - this->isModified_ = true; -#endif - -#ifdef CHATTERINO_CMAKE_GEN_DATE - this->dateOfBuild_ = - QString(FROM_EXTERNAL_DEFINE(CHATTERINO_CMAKE_GEN_DATE)).remove('"'); -#endif - this->fullVersion_ = "Chatterino "; if (Modes::instance().isNightly) { @@ -92,14 +83,23 @@ QStringList Version::buildTags() const { QStringList tags; - tags.append("Qt " QT_VERSION_STR); + const auto *runtimeVersion = qVersion(); + if (runtimeVersion != QLatin1String{QT_VERSION_STR}) + { + tags.append(u"Qt "_s QT_VERSION_STR u" (running on " % runtimeVersion % + u")"); + } + else + { + tags.append(u"Qt "_s QT_VERSION_STR); + } -#ifdef USEWINSDK - tags.append("Windows SDK"); -#endif #ifdef _MSC_FULL_VER tags.append("MSVC " + QString::number(_MSC_FULL_VER, 10)); #endif +#ifdef CHATTERINO_WITH_CRASHPAD + tags.append("Crashpad"); +#endif return tags; } diff --git a/src/common/Version.hpp b/src/common/Version.hpp index f8a724fb1..8d9ef54a0 100644 --- a/src/common/Version.hpp +++ b/src/common/Version.hpp @@ -1,24 +1,32 @@ #pragma once #include -#include - -#define CHATTERINO_VERSION "2.3.5" - -#if defined(Q_OS_WIN) -# define CHATTERINO_OS "win" -#elif defined(Q_OS_MACOS) -# define CHATTERINO_OS "macos" -#elif defined(Q_OS_LINUX) -# define CHATTERINO_OS "linux" -#elif defined(Q_OS_FREEBSD) -# define CHATTERINO_OS "freebsd" -#else -# define CHATTERINO_OS "unknown" -#endif namespace chatterino { +/** + * Valid version formats, in order of latest to oldest + * + * Stable: + * - 2.4.0 + * + * Release candidate: + * - 2.4.0-rc.3 + * - 2.4.0-rc.2 + * - 2.4.0-rc + * + * Beta: + * - 2.4.0-beta.3 + * - 2.4.0-beta.2 + * - 2.4.0-beta + * + * Alpha: + * - 2.4.0-alpha.3 + * - 2.4.0-alpha.2 + * - 2.4.0-alpha + **/ +inline const QString CHATTERINO_VERSION = QStringLiteral("2.5.1"); + class Version { public: diff --git a/src/common/WindowDescriptors.cpp b/src/common/WindowDescriptors.cpp index 902de665f..6c02d5c95 100644 --- a/src/common/WindowDescriptors.cpp +++ b/src/common/WindowDescriptors.cpp @@ -219,9 +219,15 @@ WindowLayout WindowLayout::loadFromFile(const QString &path) } // Load emote popup position - QJsonObject emote_popup_obj = windowObj.value("emotePopup").toObject(); - layout.emotePopupPos_ = QPoint(emote_popup_obj.value("x").toInt(), - emote_popup_obj.value("y").toInt()); + { + auto emotePopup = windowObj["emotePopup"].toObject(); + layout.emotePopupBounds_ = QRect{ + emotePopup["x"].toInt(), + emotePopup["y"].toInt(), + emotePopup["width"].toInt(), + emotePopup["height"].toInt(), + }; + } layout.windows_.emplace_back(std::move(window)); } @@ -229,4 +235,108 @@ WindowLayout WindowLayout::loadFromFile(const QString &path) return layout; } +void WindowLayout::activateOrAddChannel(ProviderId provider, + const QString &name) +{ + if (provider != ProviderId::Twitch || name.startsWith(u'/') || + name.startsWith(u'$')) + { + qCWarning(chatterinoWindowmanager) + << "Only twitch channels can be set as active"; + return; + } + + auto mainWindow = std::find_if(this->windows_.begin(), this->windows_.end(), + [](const auto &win) { + return win.type_ == WindowType::Main; + }); + + if (mainWindow == this->windows_.end()) + { + this->windows_.emplace_back(WindowDescriptor{ + .type_ = WindowType::Main, + .geometry_ = {-1, -1, -1, -1}, + .tabs_ = + { + TabDescriptor{ + .selected_ = true, + .rootNode_ = SplitNodeDescriptor{{ + .type_ = "twitch", + .channelName_ = name, + }}, + }, + }, + }); + return; + } + + TabDescriptor *bestTab = nullptr; + // The tab score is calculated as follows: + // +2 for every split + // +1 if the desired split has filters + // Thus lower is better and having one split of a channel is preferred over multiple + size_t bestTabScore = std::numeric_limits::max(); + + for (auto &tab : mainWindow->tabs_) + { + tab.selected_ = false; + + if (!tab.rootNode_) + { + continue; + } + + // recursive visitor + struct Visitor { + const QString &spec; + size_t score = 0; + bool hasChannel = false; + + void operator()(const SplitNodeDescriptor &split) + { + this->score += 2; + if (split.channelName_ == this->spec) + { + hasChannel = true; + if (!split.filters_.empty()) + { + this->score += 1; + } + } + } + + void operator()(const ContainerNodeDescriptor &container) + { + for (const auto &item : container.items_) + { + std::visit(*this, item); + } + } + } visitor{name}; + + std::visit(visitor, *tab.rootNode_); + + if (visitor.hasChannel && visitor.score < bestTabScore) + { + bestTab = &tab; + bestTabScore = visitor.score; + } + } + + if (bestTab) + { + bestTab->selected_ = true; + return; + } + + TabDescriptor tab{ + .selected_ = true, + .rootNode_ = SplitNodeDescriptor{{ + .type_ = "twitch", + .channelName_ = name, + }}, + }; + mainWindow->tabs_.emplace_back(tab); +} + } // namespace chatterino diff --git a/src/common/WindowDescriptors.hpp b/src/common/WindowDescriptors.hpp index 49d00aabd..9964940ca 100644 --- a/src/common/WindowDescriptors.hpp +++ b/src/common/WindowDescriptors.hpp @@ -1,5 +1,7 @@ #pragma once +#include "common/ProviderId.hpp" + #include #include #include @@ -8,6 +10,7 @@ #include #include +#include namespace chatterino { @@ -29,7 +32,7 @@ namespace chatterino { enum class WindowType; struct SplitDescriptor { - // Twitch or mentions or watching or whispers or IRC + // Twitch or mentions or watching or live or automod or whispers or IRC QString type_; // Twitch Channel name or IRC channel name @@ -94,12 +97,22 @@ struct WindowDescriptor { class WindowLayout { public: - static WindowLayout loadFromFile(const QString &path); - // A complete window layout has a single emote popup position that is shared among all windows - QPoint emotePopupPos_; + QRect emotePopupBounds_; std::vector windows_; + + /// Selects the split containing the channel specified by @a name for the specified + /// @a provider. Currently, only Twitch is supported as the provider + /// and special channels (such as /mentions) are ignored. + /// + /// Tabs with fewer splits are preferred. + /// Channels without filters are preferred. + /// + /// If no split with the channel exists, a new one is added. + /// If no window exists, a new one is added. + void activateOrAddChannel(ProviderId provider, const QString &name); + static WindowLayout loadFromFile(const QString &path); }; } // namespace chatterino diff --git a/src/common/enums/MessageOverflow.hpp b/src/common/enums/MessageOverflow.hpp new file mode 100644 index 000000000..28dbd800c --- /dev/null +++ b/src/common/enums/MessageOverflow.hpp @@ -0,0 +1,18 @@ +#pragma once + +namespace chatterino { + +// MessageOverflow is used for controlling how to guide the user into not +// sending a message that will be discarded by Twitch +enum MessageOverflow { + // Allow overflowing characters to be inserted into the input box, but highlight them in red + Highlight, + + // Prevent more characters from being inserted into the input box + Prevent, + + // Do nothing + Allow, +}; + +} // namespace chatterino diff --git a/src/common/NetworkCommon.cpp b/src/common/network/NetworkCommon.cpp similarity index 95% rename from src/common/NetworkCommon.cpp rename to src/common/network/NetworkCommon.cpp index 850612417..0d4df7d76 100644 --- a/src/common/NetworkCommon.cpp +++ b/src/common/network/NetworkCommon.cpp @@ -1,4 +1,4 @@ -#include "common/NetworkCommon.hpp" +#include "common/network/NetworkCommon.hpp" #include diff --git a/src/common/NetworkCommon.hpp b/src/common/network/NetworkCommon.hpp similarity index 74% rename from src/common/NetworkCommon.hpp rename to src/common/network/NetworkCommon.hpp index cb37cf43d..215b828a7 100644 --- a/src/common/NetworkCommon.hpp +++ b/src/common/network/NetworkCommon.hpp @@ -1,22 +1,23 @@ #pragma once +#include + #include #include -#include - class QNetworkReply; namespace chatterino { -class Outcome; class NetworkResult; -using NetworkSuccessCallback = std::function; +using NetworkSuccessCallback = std::function; using NetworkErrorCallback = std::function; -using NetworkReplyCreatedCallback = std::function; using NetworkFinallyCallback = std::function; +/** + * @exposeenum HTTPMethod + */ enum class NetworkRequestType { Get, Post, @@ -24,13 +25,6 @@ enum class NetworkRequestType { Delete, Patch, }; -const static std::vector networkRequestTypes{ - "GET", // - "POST", // - "PUT", // - "DELETE", // - "PATCH", // -}; // parseHeaderList takes a list of headers in string form, // where each header pair is separated by semicolons (;) and the header name and value is divided by a colon (:) diff --git a/src/common/network/NetworkManager.cpp b/src/common/network/NetworkManager.cpp new file mode 100644 index 000000000..b1b6bd28f --- /dev/null +++ b/src/common/network/NetworkManager.cpp @@ -0,0 +1,44 @@ +#include "common/network/NetworkManager.hpp" + +#include + +namespace chatterino { + +QThread *NetworkManager::workerThread = nullptr; +QNetworkAccessManager *NetworkManager::accessManager = nullptr; + +void NetworkManager::init() +{ + assert(!NetworkManager::workerThread); + assert(!NetworkManager::accessManager); + + NetworkManager::workerThread = new QThread; + NetworkManager::workerThread->setObjectName("NetworkWorker"); + NetworkManager::workerThread->start(); + + NetworkManager::accessManager = new QNetworkAccessManager; + NetworkManager::accessManager->moveToThread(NetworkManager::workerThread); +} + +void NetworkManager::deinit() +{ + assert(NetworkManager::workerThread); + assert(NetworkManager::accessManager); + + // delete the access manager first: + // - put the event on the worker thread + // - wait for it to process + NetworkManager::accessManager->deleteLater(); + NetworkManager::accessManager = nullptr; + + if (NetworkManager::workerThread) + { + NetworkManager::workerThread->quit(); + NetworkManager::workerThread->wait(); + } + + NetworkManager::workerThread->deleteLater(); + NetworkManager::workerThread = nullptr; +} + +} // namespace chatterino diff --git a/src/common/NetworkManager.hpp b/src/common/network/NetworkManager.hpp similarity index 73% rename from src/common/NetworkManager.hpp rename to src/common/network/NetworkManager.hpp index 530aaae1f..b02ce04e5 100644 --- a/src/common/NetworkManager.hpp +++ b/src/common/network/NetworkManager.hpp @@ -10,8 +10,8 @@ class NetworkManager : public QObject Q_OBJECT public: - static QThread workerThread; - static QNetworkAccessManager accessManager; + static QThread *workerThread; + static QNetworkAccessManager *accessManager; static void init(); static void deinit(); diff --git a/src/common/network/NetworkPrivate.cpp b/src/common/network/NetworkPrivate.cpp new file mode 100644 index 000000000..425a1f5f2 --- /dev/null +++ b/src/common/network/NetworkPrivate.cpp @@ -0,0 +1,204 @@ +#include "common/network/NetworkPrivate.hpp" + +#include "Application.hpp" +#include "common/network/NetworkManager.hpp" +#include "common/network/NetworkResult.hpp" +#include "common/network/NetworkTask.hpp" +#include "common/QLogging.hpp" +#include "singletons/Paths.hpp" +#include "util/AbandonObject.hpp" +#include "util/DebugCount.hpp" +#include "util/PostToThread.hpp" +#include "util/QMagicEnum.hpp" + +#include +#include +#include +#include +#include +#include + +#ifdef NDEBUG +constexpr qsizetype SLOW_HTTP_THRESHOLD = 30; +#else +constexpr qsizetype SLOW_HTTP_THRESHOLD = 90; +#endif + +using namespace chatterino::network::detail; + +namespace { + +using namespace chatterino; + +void runCallback(bool concurrent, auto &&fn) +{ + if (concurrent) + { + std::ignore = QtConcurrent::run(std::forward(fn)); + } + else + { + runInGuiThread(std::forward(fn)); + } +} + +void loadUncached(std::shared_ptr &&data) +{ + DebugCount::increase("http request started"); + + NetworkRequester requester; + auto *worker = new NetworkTask(std::move(data)); + + worker->moveToThread(NetworkManager::workerThread); + + QObject::connect(&requester, &NetworkRequester::requestUrl, worker, + &NetworkTask::run); + + emit requester.requestUrl(); +} + +void loadCached(std::shared_ptr &&data) +{ + QFile cachedFile(getApp()->getPaths().cacheDirectory() + "/" + + data->getHash()); + + if (!cachedFile.exists() || !cachedFile.open(QIODevice::ReadOnly)) + { + loadUncached(std::move(data)); + return; + } + + // XXX: check if bytes is empty? + QByteArray bytes = cachedFile.readAll(); + + qCDebug(chatterinoHTTP).noquote() << data->typeString() << "[CACHED] 200" + << data->request.url().toString(); + + data->emitSuccess( + {NetworkResult::NetworkError::NoError, QVariant(200), bytes}); + data->emitFinally(); +} + +} // namespace + +namespace chatterino { + +NetworkData::NetworkData() +{ + DebugCount::increase("NetworkData"); +} + +NetworkData::~NetworkData() +{ + DebugCount::decrease("NetworkData"); +} + +QString NetworkData::getHash() +{ + if (this->hash_.isEmpty()) + { + QByteArray bytes; + + bytes.append(this->request.url().toString().toUtf8()); + + for (const auto &header : this->request.rawHeaderList()) + { + bytes.append(header); + } + + QByteArray hashBytes( + QCryptographicHash::hash(bytes, QCryptographicHash::Sha256)); + + this->hash_ = hashBytes.toHex(); + } + + return this->hash_; +} + +void NetworkData::emitSuccess(NetworkResult &&result) +{ + if (!this->onSuccess) + { + return; + } + + runCallback(this->executeConcurrently, + [cb = std::move(this->onSuccess), result = std::move(result), + url = this->request.url(), hasCaller = this->hasCaller, + caller = this->caller]() { + if (hasCaller && caller.isNull()) + { + return; + } + + QElapsedTimer timer; + timer.start(); + cb(result); + if (timer.elapsed() > SLOW_HTTP_THRESHOLD) + { + qCWarning(chatterinoHTTP) + << "Slow HTTP success handler for" << url.toString() + << timer.elapsed() + << "ms (threshold:" << SLOW_HTTP_THRESHOLD << "ms)"; + } + }); +} + +void NetworkData::emitError(NetworkResult &&result) +{ + if (!this->onError) + { + return; + } + + runCallback(this->executeConcurrently, + [cb = std::move(this->onError), result = std::move(result), + hasCaller = this->hasCaller, caller = this->caller]() { + if (hasCaller && caller.isNull()) + { + return; + } + + cb(result); + }); +} + +void NetworkData::emitFinally() +{ + if (!this->finally) + { + return; + } + + runCallback(this->executeConcurrently, + [cb = std::move(this->finally), hasCaller = this->hasCaller, + caller = this->caller]() { + if (hasCaller && caller.isNull()) + { + return; + } + + cb(); + }); +} + +QString NetworkData::typeString() const +{ + return qmagicenum::enumNameString(this->requestType); +} + +void load(std::shared_ptr &&data) +{ + if (data->cache) + { + std::ignore = QtConcurrent::run([data = std::move(data)]() mutable { + loadCached(std::move(data)); + }); + } + else + { + loadUncached(std::move(data)); + } +} + +} // namespace chatterino diff --git a/src/common/network/NetworkPrivate.hpp b/src/common/network/NetworkPrivate.hpp new file mode 100644 index 000000000..434d9f66d --- /dev/null +++ b/src/common/network/NetworkPrivate.hpp @@ -0,0 +1,71 @@ +#pragma once + +#include "common/Common.hpp" +#include "common/network/NetworkCommon.hpp" + +#include +#include +#include +#include + +#include +#include + +class QNetworkReply; + +namespace chatterino { + +class NetworkResult; + +class NetworkRequester : public QObject +{ + Q_OBJECT + +signals: + void requestUrl(); +}; + +class NetworkData +{ +public: + NetworkData(); + ~NetworkData(); + NetworkData(const NetworkData &) = delete; + NetworkData(NetworkData &&) = delete; + NetworkData &operator=(const NetworkData &) = delete; + NetworkData &operator=(NetworkData &&) = delete; + + QNetworkRequest request; + bool hasCaller{}; + QPointer caller; + bool cache{}; + bool executeConcurrently{}; + + NetworkSuccessCallback onSuccess; + NetworkErrorCallback onError; + NetworkFinallyCallback finally; + + NetworkRequestType requestType = NetworkRequestType::Get; + + QByteArray payload; + std::unique_ptr multiPartPayload; + + /// By default, there's no explicit timeout for the request. + /// To set a timeout, use NetworkRequest's timeout method + std::optional timeout{}; + + QString getHash(); + + void emitSuccess(NetworkResult &&result); + void emitError(NetworkResult &&result); + void emitFinally(); + + QString typeString() const; + +private: + QString hash_; +}; + +void load(std::shared_ptr &&data); + +} // namespace chatterino diff --git a/src/common/network/NetworkRequest.cpp b/src/common/network/NetworkRequest.cpp new file mode 100644 index 000000000..9413943b4 --- /dev/null +++ b/src/common/network/NetworkRequest.cpp @@ -0,0 +1,218 @@ +#include "common/network/NetworkRequest.hpp" + +#include "common/network/NetworkPrivate.hpp" +#include "common/QLogging.hpp" +#include "common/Version.hpp" + +#include +#include +#include +#include + +#include + +namespace chatterino { + +NetworkRequest::NetworkRequest(const std::string &url, + NetworkRequestType requestType) + : data(new NetworkData) +{ + this->data->request.setUrl(QUrl(QString::fromStdString(url))); + this->data->requestType = requestType; + + this->initializeDefaultValues(); +} + +NetworkRequest::NetworkRequest(const QUrl &url, NetworkRequestType requestType) + : data(new NetworkData) +{ + this->data->request.setUrl(url); + this->data->requestType = requestType; + + this->initializeDefaultValues(); +} + +NetworkRequest::~NetworkRequest() = default; + +NetworkRequest NetworkRequest::type(NetworkRequestType newRequestType) && +{ + this->data->requestType = newRequestType; + return std::move(*this); +} + +NetworkRequest NetworkRequest::caller(const QObject *caller) && +{ + if (caller) + { + // Caller must be in gui thread + assert(caller->thread() == QApplication::instance()->thread()); + + this->data->caller = const_cast(caller); + this->data->hasCaller = true; + } + return std::move(*this); +} + +NetworkRequest NetworkRequest::onError(NetworkErrorCallback cb) && +{ + this->data->onError = std::move(cb); + return std::move(*this); +} + +NetworkRequest NetworkRequest::onSuccess(NetworkSuccessCallback cb) && +{ + this->data->onSuccess = std::move(cb); + return std::move(*this); +} + +NetworkRequest NetworkRequest::finally(NetworkFinallyCallback cb) && +{ + this->data->finally = std::move(cb); + return std::move(*this); +} + +NetworkRequest NetworkRequest::header(const char *headerName, + const char *value) && +{ + this->data->request.setRawHeader(headerName, value); + return std::move(*this); +} + +NetworkRequest NetworkRequest::header(const char *headerName, + const QByteArray &value) && +{ + this->data->request.setRawHeader(headerName, value); + return std::move(*this); +} + +NetworkRequest NetworkRequest::header(const char *headerName, + const QString &value) && +{ + this->data->request.setRawHeader(headerName, value.toUtf8()); + return std::move(*this); +} + +NetworkRequest NetworkRequest::header(QNetworkRequest::KnownHeaders header, + const QVariant &value) && +{ + this->data->request.setHeader(header, value); + return std::move(*this); +} + +NetworkRequest NetworkRequest::header(const QByteArray &headerName, + const QByteArray &value) && +{ + this->data->request.setRawHeader(headerName, value); + return std::move(*this); +} + +NetworkRequest NetworkRequest::headerList( + const std::vector> &headers) && +{ + for (const auto &[headerName, headerValue] : headers) + { + this->data->request.setRawHeader(headerName, headerValue); + } + return std::move(*this); +} + +NetworkRequest NetworkRequest::timeout(int ms) && +{ + this->data->timeout = std::chrono::milliseconds(ms); + return std::move(*this); +} + +NetworkRequest NetworkRequest::concurrent() && +{ + this->data->executeConcurrently = true; + return std::move(*this); +} + +NetworkRequest NetworkRequest::multiPart(QHttpMultiPart *payload) && +{ + this->data->multiPartPayload = {payload, {}}; + return std::move(*this); +} + +NetworkRequest NetworkRequest::followRedirects(bool on) && +{ + if (on) + { + this->data->request.setAttribute( + QNetworkRequest::RedirectPolicyAttribute, + QNetworkRequest::NoLessSafeRedirectPolicy); + } + else + { + this->data->request.setAttribute( + QNetworkRequest::RedirectPolicyAttribute, + QNetworkRequest::ManualRedirectPolicy); + } + + return std::move(*this); +} + +NetworkRequest NetworkRequest::payload(const QByteArray &payload) && +{ + this->data->payload = payload; + return std::move(*this); +} + +NetworkRequest NetworkRequest::cache() && +{ + this->data->cache = true; + return std::move(*this); +} + +void NetworkRequest::execute() +{ + this->executed_ = true; + + // Only allow caching for GET request + if (this->data->cache && this->data->requestType != NetworkRequestType::Get) + { + qCDebug(chatterinoCommon) << "Can only cache GET requests!"; + this->data->cache = false; + } + + // Can not have a caller and be concurrent at the same time. + assert(!(this->data->caller && this->data->executeConcurrently)); + + load(std::move(this->data)); +} + +void NetworkRequest::initializeDefaultValues() +{ + const auto userAgent = QStringLiteral("chatterino/%1 (%2)") + .arg(Version::instance().version(), + Version::instance().commitHash()) + .toUtf8(); + + this->data->request.setRawHeader("User-Agent", userAgent); +} + +NetworkRequest NetworkRequest::json(const QJsonArray &root) && +{ + return std::move(*this).json(QJsonDocument(root)); +} + +NetworkRequest NetworkRequest::json(const QJsonObject &root) && +{ + return std::move(*this).json(QJsonDocument(root)); +} + +NetworkRequest NetworkRequest::json(const QJsonDocument &document) && +{ + return std::move(*this).json(document.toJson(QJsonDocument::Compact)); +} + +NetworkRequest NetworkRequest::json(const QByteArray &payload) && +{ + return std::move(*this) + .payload(payload) + .header(QNetworkRequest::ContentTypeHeader, "application/json") + .header(QNetworkRequest::ContentLengthHeader, payload.length()) + .header("Accept", "application/json"); +} + +} // namespace chatterino diff --git a/src/common/NetworkRequest.hpp b/src/common/network/NetworkRequest.hpp similarity index 68% rename from src/common/NetworkRequest.hpp rename to src/common/network/NetworkRequest.hpp index 51ee1e962..1308fb023 100644 --- a/src/common/NetworkRequest.hpp +++ b/src/common/network/NetworkRequest.hpp @@ -1,14 +1,18 @@ #pragma once -#include "common/NetworkCommon.hpp" -#include "common/NetworkResult.hpp" +#include "common/network/NetworkCommon.hpp" #include + #include +class QJsonArray; +class QJsonObject; +class QJsonDocument; + namespace chatterino { -struct NetworkData; +class NetworkData; class NetworkRequest final { @@ -24,8 +28,8 @@ public: explicit NetworkRequest( const std::string &url, NetworkRequestType requestType = NetworkRequestType::Get); - explicit NetworkRequest( - QUrl url, NetworkRequestType requestType = NetworkRequestType::Get); + explicit NetworkRequest(const QUrl &url, NetworkRequestType requestType = + NetworkRequestType::Get); // Enable move NetworkRequest(NetworkRequest &&other) = default; @@ -39,7 +43,6 @@ public: NetworkRequest type(NetworkRequestType newRequestType) &&; - NetworkRequest onReplyCreated(NetworkReplyCreatedCallback cb) &&; NetworkRequest onError(NetworkErrorCallback cb) &&; NetworkRequest onSuccess(NetworkSuccessCallback cb) &&; NetworkRequest finally(NetworkFinallyCallback cb) &&; @@ -54,18 +57,27 @@ public: NetworkRequest header(const char *headerName, const char *value) &&; NetworkRequest header(const char *headerName, const QByteArray &value) &&; NetworkRequest header(const char *headerName, const QString &value) &&; + NetworkRequest header(const QByteArray &headerName, + const QByteArray &value) &&; + NetworkRequest header(QNetworkRequest::KnownHeaders header, + const QVariant &value) &&; NetworkRequest headerList( const std::vector> &headers) &&; NetworkRequest timeout(int ms) &&; NetworkRequest concurrent() &&; - NetworkRequest authorizeTwitchV5(const QString &clientID, - const QString &oauthToken = QString()) &&; NetworkRequest multiPart(QHttpMultiPart *payload) &&; + /** + * This will change `RedirectPolicyAttribute`. + * `QNetworkRequest`'s defaults are used by default (Qt 5: no-follow, Qt 6: follow). + */ + NetworkRequest followRedirects(bool on) &&; + NetworkRequest json(const QJsonObject &root) &&; + NetworkRequest json(const QJsonArray &root) &&; + NetworkRequest json(const QJsonDocument &document) &&; + NetworkRequest json(const QByteArray &payload) &&; void execute(); - static NetworkRequest twitchRequest(QUrl url); - private: void initializeDefaultValues(); }; diff --git a/src/common/NetworkResult.cpp b/src/common/network/NetworkResult.cpp similarity index 50% rename from src/common/NetworkResult.cpp rename to src/common/network/NetworkResult.cpp index c2cb45e1d..c6544eaad 100644 --- a/src/common/NetworkResult.cpp +++ b/src/common/network/NetworkResult.cpp @@ -1,16 +1,23 @@ -#include "common/NetworkResult.hpp" +#include "common/network/NetworkResult.hpp" +#include "common/QLogging.hpp" + +#include +#include #include #include -#include -#include "common/QLogging.hpp" namespace chatterino { -NetworkResult::NetworkResult(const QByteArray &data, int status) - : data_(data) - , status_(status) +NetworkResult::NetworkResult(NetworkError error, const QVariant &httpStatusCode, + QByteArray data) + : data_(std::move(data)) + , error_(error) { + if (httpStatusCode.isValid()) + { + this->status_ = httpStatusCode.toInt(); + } } QJsonObject NetworkResult::parseJson() const @@ -58,9 +65,30 @@ const QByteArray &NetworkResult::getData() const return this->data_; } -int NetworkResult::status() const +QString NetworkResult::formatError() const { - return this->status_; + // Print the status for errors that mirror HTTP status codes (=0 || >99) + if (this->status_ && (this->error_ == QNetworkReply::NoError || + this->error_ > QNetworkReply::UnknownNetworkError)) + { + return QString::number(*this->status_); + } + + const auto *name = + QMetaEnum::fromType().valueToKey( + this->error_); + if (name == nullptr) + { + if (this->status_) + { + return QStringLiteral("unknown error (status: %1, error: %2)") + .arg(QString::number(*this->status_), + QString::number(this->error_)); + } + + return QStringLiteral("unknown error (%1)").arg(this->error_); + } + return name; } } // namespace chatterino diff --git a/src/common/network/NetworkResult.hpp b/src/common/network/NetworkResult.hpp new file mode 100644 index 000000000..9f0ada784 --- /dev/null +++ b/src/common/network/NetworkResult.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include +#include +#include +#include + +#include + +namespace chatterino { + +class NetworkResult +{ +public: + using NetworkError = QNetworkReply::NetworkError; + + NetworkResult(NetworkError error, const QVariant &httpStatusCode, + QByteArray data); + + /// Parses the result as json and returns the root as an object. + /// Returns empty object if parsing failed. + QJsonObject parseJson() const; + /// Parses the result as json and returns the root as an array. + /// Returns empty object if parsing failed. + QJsonArray parseJsonArray() const; + /// Parses the result as json and returns the document. + rapidjson::Document parseRapidJson() const; + const QByteArray &getData() const; + + /// The error code of the reply. + /// In case of a successful reply, this will be NoError (0) + NetworkError error() const + { + return this->error_; + } + + /// The HTTP status code if a response was received. + std::optional status() const + { + return this->status_; + } + + /// Formats the error. + /// If a reply is received, returns the HTTP status otherwise, the network error. + QString formatError() const; + +private: + QByteArray data_; + + NetworkError error_; + std::optional status_; +}; + +} // namespace chatterino diff --git a/src/common/network/NetworkTask.cpp b/src/common/network/NetworkTask.cpp new file mode 100644 index 000000000..bebe34051 --- /dev/null +++ b/src/common/network/NetworkTask.cpp @@ -0,0 +1,191 @@ +#include "common/network/NetworkTask.hpp" + +#include "Application.hpp" +#include "common/network/NetworkManager.hpp" +#include "common/network/NetworkPrivate.hpp" +#include "common/network/NetworkResult.hpp" +#include "common/QLogging.hpp" +#include "singletons/Paths.hpp" +#include "util/AbandonObject.hpp" +#include "util/DebugCount.hpp" + +#include +#include +#include + +namespace chatterino::network::detail { + +NetworkTask::NetworkTask(std::shared_ptr &&data) + : data_(std::move(data)) +{ +} + +NetworkTask::~NetworkTask() +{ + if (this->reply_) + { + this->reply_->deleteLater(); + } +} + +void NetworkTask::run() +{ + const auto &timeout = this->data_->timeout; + if (timeout.has_value()) + { + this->timer_ = new QTimer(this); + this->timer_->setSingleShot(true); + this->timer_->start(timeout.value()); + QObject::connect(this->timer_, &QTimer::timeout, this, + &NetworkTask::timeout); + } + + this->reply_ = this->createReply(); + if (!this->reply_) + { + this->deleteLater(); + return; + } + QObject::connect(this->reply_, &QNetworkReply::finished, this, + &NetworkTask::finished); +} + +QNetworkReply *NetworkTask::createReply() +{ + const auto &data = this->data_; + const auto &request = this->data_->request; + auto *accessManager = NetworkManager::accessManager; + switch (this->data_->requestType) + { + case NetworkRequestType::Get: + return accessManager->get(request); + + case NetworkRequestType::Put: + return accessManager->put(request, data->payload); + + case NetworkRequestType::Delete: + return accessManager->deleteResource(data->request); + + case NetworkRequestType::Post: + if (data->multiPartPayload) + { + assert(data->payload.isNull()); + + return accessManager->post(request, + data->multiPartPayload.get()); + } + else + { + return accessManager->post(request, data->payload); + } + case NetworkRequestType::Patch: + if (data->multiPartPayload) + { + assert(data->payload.isNull()); + + return accessManager->sendCustomRequest( + request, "PATCH", data->multiPartPayload.get()); + } + else + { + return NetworkManager::accessManager->sendCustomRequest( + request, "PATCH", data->payload); + } + } + return nullptr; +} + +void NetworkTask::logReply() +{ + auto status = + this->reply_->attribute(QNetworkRequest::HttpStatusCodeAttribute) + .toInt(); + if (this->data_->requestType == NetworkRequestType::Get) + { + qCDebug(chatterinoHTTP).noquote() + << this->data_->typeString() << status + << this->data_->request.url().toString(); + } + else + { + qCDebug(chatterinoHTTP).noquote() + << this->data_->typeString() + << this->data_->request.url().toString() << status + << QString(this->data_->payload); + } +} + +void NetworkTask::writeToCache(const QByteArray &bytes) const +{ + std::ignore = QtConcurrent::run([data = this->data_, bytes] { + QFile cachedFile(getApp()->getPaths().cacheDirectory() + "/" + + data->getHash()); + + if (cachedFile.open(QIODevice::WriteOnly)) + { + cachedFile.write(bytes); + } + }); +} + +void NetworkTask::timeout() +{ + AbandonObject guard(this); + + // prevent abort() from calling finished() + QObject::disconnect(this->reply_, &QNetworkReply::finished, this, + &NetworkTask::finished); + this->reply_->abort(); + + qCDebug(chatterinoHTTP).noquote() + << this->data_->typeString() << "[timed out]" + << this->data_->request.url().toString(); + + this->data_->emitError({NetworkResult::NetworkError::TimeoutError, {}, {}}); + this->data_->emitFinally(); +} + +void NetworkTask::finished() +{ + AbandonObject guard(this); + + if (this->timer_) + { + this->timer_->stop(); + } + + auto *reply = this->reply_; + auto status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute); + + if (reply->error() == QNetworkReply::OperationCanceledError) + { + // Operation cancelled, most likely timed out + qCDebug(chatterinoHTTP).noquote() + << this->data_->typeString() << "[cancelled]" + << this->data_->request.url().toString(); + return; + } + + if (reply->error() != QNetworkReply::NoError) + { + this->logReply(); + this->data_->emitError({reply->error(), status, reply->readAll()}); + this->data_->emitFinally(); + + return; + } + + QByteArray bytes = reply->readAll(); + + if (this->data_->cache) + { + this->writeToCache(bytes); + } + + DebugCount::increase("http request success"); + this->logReply(); + this->data_->emitSuccess({reply->error(), status, bytes}); + this->data_->emitFinally(); +} + +} // namespace chatterino::network::detail diff --git a/src/common/network/NetworkTask.hpp b/src/common/network/NetworkTask.hpp new file mode 100644 index 000000000..d88d0a113 --- /dev/null +++ b/src/common/network/NetworkTask.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include +#include + +#include + +class QNetworkReply; + +namespace chatterino { + +class NetworkData; + +} // namespace chatterino + +namespace chatterino::network::detail { + +class NetworkTask : public QObject +{ + Q_OBJECT + +public: + NetworkTask(std::shared_ptr &&data); + ~NetworkTask() override; + + NetworkTask(const NetworkTask &) = delete; + NetworkTask(NetworkTask &&) = delete; + NetworkTask &operator=(const NetworkTask &) = delete; + NetworkTask &operator=(NetworkTask &&) = delete; + + // NOLINTNEXTLINE(readability-redundant-access-specifiers) +public slots: + void run(); + +private: + QNetworkReply *createReply(); + + void logReply(); + void writeToCache(const QByteArray &bytes) const; + + std::shared_ptr data_; + QNetworkReply *reply_{}; // parent: default (accessManager) + QTimer *timer_{}; // parent: this + + // NOLINTNEXTLINE(readability-redundant-access-specifiers) +private slots: + void timeout(); + void finished(); +}; + +} // namespace chatterino::network::detail diff --git a/src/controllers/accounts/Account.cpp b/src/controllers/accounts/Account.cpp index 9cf9ceefa..1d9f0ab1c 100644 --- a/src/controllers/accounts/Account.cpp +++ b/src/controllers/accounts/Account.cpp @@ -1,4 +1,4 @@ -#include "Account.hpp" +#include "controllers/accounts/Account.hpp" #include diff --git a/src/controllers/accounts/AccountController.cpp b/src/controllers/accounts/AccountController.cpp index 2692b05a9..414cb8b56 100644 --- a/src/controllers/accounts/AccountController.cpp +++ b/src/controllers/accounts/AccountController.cpp @@ -1,30 +1,36 @@ -#include "AccountController.hpp" +#include "controllers/accounts/AccountController.hpp" #include "controllers/accounts/Account.hpp" #include "controllers/accounts/AccountModel.hpp" #include "providers/twitch/TwitchAccount.hpp" +#include "util/SharedPtrElementLess.hpp" namespace chatterino { AccountController::AccountController() : accounts_(SharedPtrElementLess{}) { - this->twitch.accounts.itemInserted.connect([this](const auto &args) { - this->accounts_.insert(std::dynamic_pointer_cast(args.item)); - }); + // These signal connections can safely be ignored since the twitch object + // will always be destroyed before the AccountController + std::ignore = + this->twitch.accounts.itemInserted.connect([this](const auto &args) { + this->accounts_.insert( + std::dynamic_pointer_cast(args.item)); + }); - this->twitch.accounts.itemRemoved.connect([this](const auto &args) { - if (args.caller != this) - { - auto &accs = this->twitch.accounts.raw(); - auto it = std::find(accs.begin(), accs.end(), args.item); - assert(it != accs.end()); + std::ignore = + this->twitch.accounts.itemRemoved.connect([this](const auto &args) { + if (args.caller != this) + { + const auto &accs = this->twitch.accounts.raw(); + auto it = std::find(accs.begin(), accs.end(), args.item); + assert(it != accs.end()); - this->accounts_.removeAt(it - accs.begin(), this); - } - }); + this->accounts_.removeAt(it - accs.begin(), this); + } + }); - this->accounts_.itemRemoved.connect([this](const auto &args) { + std::ignore = this->accounts_.itemRemoved.connect([this](const auto &args) { switch (args.item->getProviderId()) { case ProviderId::Twitch: { @@ -41,7 +47,7 @@ AccountController::AccountController() }); } -void AccountController::initialize(Settings &settings, Paths &paths) +void AccountController::load() { this->twitch.load(); } diff --git a/src/controllers/accounts/AccountController.hpp b/src/controllers/accounts/AccountController.hpp index e1ef3c70a..d84e2b9cc 100644 --- a/src/controllers/accounts/AccountController.hpp +++ b/src/controllers/accounts/AccountController.hpp @@ -1,11 +1,9 @@ #pragma once -#include - #include "common/SignalVector.hpp" -#include "common/Singleton.hpp" #include "providers/twitch/TwitchAccountManager.hpp" -#include "util/SharedPtrElementLess.hpp" + +#include namespace chatterino { @@ -15,14 +13,17 @@ class Paths; class AccountModel; -class AccountController final : public Singleton +class AccountController final { public: AccountController(); AccountModel *createModel(QObject *parent); - virtual void initialize(Settings &settings, Paths &paths) override; + /** + * Load current user & send off a signal to subscribers about any potential changes + */ + void load(); TwitchAccountManager twitch; diff --git a/src/controllers/accounts/AccountModel.cpp b/src/controllers/accounts/AccountModel.cpp index cf2b1c79f..f0598318c 100644 --- a/src/controllers/accounts/AccountModel.cpp +++ b/src/controllers/accounts/AccountModel.cpp @@ -1,4 +1,4 @@ -#include "AccountModel.hpp" +#include "controllers/accounts/AccountModel.hpp" #include "controllers/accounts/Account.hpp" #include "util/StandardItemHelper.hpp" diff --git a/src/controllers/accounts/AccountModel.hpp b/src/controllers/accounts/AccountModel.hpp index e5aea0641..bcc4dbf5d 100644 --- a/src/controllers/accounts/AccountModel.hpp +++ b/src/controllers/accounts/AccountModel.hpp @@ -1,7 +1,6 @@ #pragma once #include "common/SignalVectorModel.hpp" -#include "controllers/accounts/Account.hpp" #include "util/QStringHash.hpp" #include @@ -18,21 +17,20 @@ public: protected: // turn a vector item into a model row - virtual std::shared_ptr getItemFromRow( + std::shared_ptr getItemFromRow( std::vector &row, const std::shared_ptr &original) override; // turns a row in the model into a vector item - virtual void getRowFromItem(const std::shared_ptr &item, - std::vector &row) override; + void getRowFromItem(const std::shared_ptr &item, + std::vector &row) override; - virtual int beforeInsert(const std::shared_ptr &item, - std::vector &row, - int proposedIndex) override; + int beforeInsert(const std::shared_ptr &item, + std::vector &row, + int proposedIndex) override; - virtual void afterRemoved(const std::shared_ptr &item, - std::vector &row, - int index) override; + void afterRemoved(const std::shared_ptr &item, + std::vector &row, int index) override; friend class AccountController; diff --git a/src/controllers/commands/Command.cpp b/src/controllers/commands/Command.cpp index e7113db9c..5ac0feb5c 100644 --- a/src/controllers/commands/Command.cpp +++ b/src/controllers/commands/Command.cpp @@ -1,4 +1,4 @@ -#include "Command.hpp" +#include "controllers/commands/Command.hpp" namespace chatterino { diff --git a/src/controllers/commands/Command.hpp b/src/controllers/commands/Command.hpp index 1b365a23f..08438110e 100644 --- a/src/controllers/commands/Command.hpp +++ b/src/controllers/commands/Command.hpp @@ -2,15 +2,15 @@ #include "util/RapidjsonHelpers.hpp" -#include #include +#include namespace chatterino { struct Command { QString name; QString func; - bool showInMsgContextMenu; + bool showInMsgContextMenu{}; Command() = default; explicit Command(const QString &text); diff --git a/src/controllers/commands/CommandContext.hpp b/src/controllers/commands/CommandContext.hpp new file mode 100644 index 000000000..4ed526645 --- /dev/null +++ b/src/controllers/commands/CommandContext.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include + +#include + +namespace chatterino { + +class Channel; +using ChannelPtr = std::shared_ptr; +class TwitchChannel; + +struct CommandContext { + QStringList words; + + // Can be null + ChannelPtr channel; + + // Can be null if `channel` is null or if `channel` is not a Twitch channel + TwitchChannel *twitchChannel; +}; + +} // namespace chatterino diff --git a/src/controllers/commands/CommandController.cpp b/src/controllers/commands/CommandController.cpp index 702262510..3fe398644 100644 --- a/src/controllers/commands/CommandController.cpp +++ b/src/controllers/commands/CommandController.cpp @@ -1,183 +1,54 @@ -#include "CommandController.hpp" +#include "controllers/commands/CommandController.hpp" #include "Application.hpp" -#include "common/Env.hpp" -#include "common/SignalVector.hpp" +#include "common/Channel.hpp" #include "controllers/accounts/AccountController.hpp" +#include "controllers/commands/builtin/chatterino/Debugging.hpp" +#include "controllers/commands/builtin/Misc.hpp" +#include "controllers/commands/builtin/twitch/AddModerator.hpp" +#include "controllers/commands/builtin/twitch/AddVIP.hpp" +#include "controllers/commands/builtin/twitch/Announce.hpp" +#include "controllers/commands/builtin/twitch/Ban.hpp" +#include "controllers/commands/builtin/twitch/Block.hpp" +#include "controllers/commands/builtin/twitch/ChatSettings.hpp" +#include "controllers/commands/builtin/twitch/Chatters.hpp" +#include "controllers/commands/builtin/twitch/DeleteMessages.hpp" +#include "controllers/commands/builtin/twitch/GetModerators.hpp" +#include "controllers/commands/builtin/twitch/GetVIPs.hpp" +#include "controllers/commands/builtin/twitch/Raid.hpp" +#include "controllers/commands/builtin/twitch/RemoveModerator.hpp" +#include "controllers/commands/builtin/twitch/RemoveVIP.hpp" +#include "controllers/commands/builtin/twitch/SendReply.hpp" +#include "controllers/commands/builtin/twitch/SendWhisper.hpp" +#include "controllers/commands/builtin/twitch/ShieldMode.hpp" +#include "controllers/commands/builtin/twitch/Shoutout.hpp" +#include "controllers/commands/builtin/twitch/StartCommercial.hpp" +#include "controllers/commands/builtin/twitch/Unban.hpp" +#include "controllers/commands/builtin/twitch/UpdateChannel.hpp" +#include "controllers/commands/builtin/twitch/UpdateColor.hpp" +#include "controllers/commands/builtin/twitch/Warn.hpp" #include "controllers/commands/Command.hpp" +#include "controllers/commands/CommandContext.hpp" #include "controllers/commands/CommandModel.hpp" +#include "controllers/plugins/PluginController.hpp" #include "messages/Message.hpp" #include "messages/MessageBuilder.hpp" -#include "messages/MessageElement.hpp" +#include "providers/twitch/TwitchAccount.hpp" +#include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchCommon.hpp" -#include "providers/twitch/TwitchIrcServer.hpp" -#include "providers/twitch/api/Helix.hpp" #include "singletons/Emotes.hpp" #include "singletons/Paths.hpp" -#include "singletons/Settings.hpp" -#include "singletons/Theme.hpp" -#include "singletons/WindowManager.hpp" -#include "util/Clipboard.hpp" #include "util/CombinePath.hpp" -#include "util/FormatTime.hpp" -#include "util/Helpers.hpp" -#include "util/IncognitoBrowser.hpp" -#include "util/Qt.hpp" -#include "util/StreamLink.hpp" -#include "util/Twitch.hpp" -#include "widgets/Window.hpp" -#include "widgets/dialogs/ReplyThreadPopup.hpp" -#include "widgets/dialogs/UserInfoPopup.hpp" -#include "widgets/splits/Split.hpp" +#include "util/QStringHash.hpp" -#include -#include -#include -#include -#include +#include + +#include namespace { + using namespace chatterino; -void sendWhisperMessage(const QString &text) -{ - // (hemirt) pajlada: "we should not be sending whispers through jtv, but - // rather to your own username" - auto app = getApp(); - QString toSend = text.simplified(); - - // This is to make sure that combined emoji go through properly, see - // https://github.com/Chatterino/chatterino2/issues/3384 and - // https://mm2pl.github.io/emoji_rfc.pdf for more details - // Constants used here are defined in TwitchChannel.hpp - toSend.replace(ZERO_WIDTH_JOINER, ESCAPE_TAG); - - app->twitch->sendMessage("jtv", toSend); -} - -bool appendWhisperMessageWordsLocally(const QStringList &words) -{ - auto app = getApp(); - - MessageBuilder b; - - b.emplace(); - b.emplace(app->accounts->twitch.getCurrent()->getUserName(), - MessageElementFlag::Text, MessageColor::Text, - FontStyle::ChatMediumBold); - b.emplace("->", MessageElementFlag::Text, - getApp()->themes->messages.textColors.system); - b.emplace(words[1] + ":", MessageElementFlag::Text, - MessageColor::Text, FontStyle::ChatMediumBold); - - const auto &acc = app->accounts->twitch.getCurrent(); - const auto &accemotes = *acc->accessEmotes(); - const auto &bttvemotes = app->twitch->getBttvEmotes(); - const auto &ffzemotes = app->twitch->getFfzEmotes(); - auto flags = MessageElementFlags(); - auto emote = boost::optional{}; - for (int i = 2; i < words.length(); i++) - { - { // Twitch emote - auto it = accemotes.emotes.find({words[i]}); - if (it != accemotes.emotes.end()) - { - b.emplace(it->second, - MessageElementFlag::TwitchEmote); - continue; - } - } // Twitch emote - - { // bttv/ffz emote - if ((emote = bttvemotes.emote({words[i]}))) - { - flags = MessageElementFlag::BttvEmote; - } - else if ((emote = ffzemotes.emote({words[i]}))) - { - flags = MessageElementFlag::FfzEmote; - } - if (emote) - { - b.emplace(emote.get(), flags); - continue; - } - } // bttv/ffz emote - { // emoji/text - for (auto &variant : app->emotes->emojis.parse(words[i])) - { - constexpr const static struct { - void operator()(EmotePtr emote, MessageBuilder &b) const - { - b.emplace(emote, - MessageElementFlag::EmojiAll); - } - void operator()(const QString &string, - MessageBuilder &b) const - { - auto linkString = b.matchLink(string); - if (linkString.isEmpty()) - { - b.emplace(string, - MessageElementFlag::Text); - } - else - { - b.addLink(string, linkString); - } - } - } visitor; - boost::apply_visitor( - [&b](auto &&arg) { - visitor(arg, b); - }, - variant); - } // emoji/text - } - } - - b->flags.set(MessageFlag::DoNotTriggerNotification); - b->flags.set(MessageFlag::Whisper); - auto messagexD = b.release(); - - app->twitch->whispersChannel->addMessage(messagexD); - - auto overrideFlags = boost::optional(messagexD->flags); - overrideFlags->set(MessageFlag::DoNotLog); - - if (getSettings()->inlineWhispers) - { - app->twitch->forEachChannel( - [&messagexD, overrideFlags](ChannelPtr _channel) { - _channel->addMessage(messagexD, overrideFlags); - }); - } - - return true; -} - -bool appendWhisperMessageStringLocally(const QString &textNoEmoji) -{ - QString text = getApp()->emotes->emojis.replaceShortCodes(textNoEmoji); - QStringList words = text.split(' ', Qt::SkipEmptyParts); - - if (words.length() == 0) - { - return false; - } - - QString commandName = words[0]; - - if (TWITCH_WHISPER_COMMANDS.contains(commandName, Qt::CaseInsensitive)) - { - if (words.length() > 2) - { - return appendWhisperMessageWordsLocally(words); - } - } - return false; -} - using VariableReplacer = std::function; @@ -249,7 +120,8 @@ const std::unordered_map COMMAND_VARS{ [](const auto &altText, const auto &channel, const auto *message) { (void)(channel); //unused (void)(message); //unused - auto uid = getApp()->accounts->twitch.getCurrent()->getUserId(); + auto uid = + getApp()->getAccounts()->twitch.getCurrent()->getUserId(); return uid.isEmpty() ? altText : uid; }, }, @@ -258,7 +130,8 @@ const std::unordered_map COMMAND_VARS{ [](const auto &altText, const auto &channel, const auto *message) { (void)(channel); //unused (void)(message); //unused - auto name = getApp()->accounts->twitch.getCurrent()->getUserName(); + auto name = + getApp()->getAccounts()->twitch.getCurrent()->getUserName(); return name.isEmpty() ? altText : name; }, }, @@ -390,7 +263,7 @@ const std::unordered_map COMMAND_VARS{ namespace chatterino { -void CommandController::initialize(Settings &, Paths &paths) +CommandController::CommandController(const Paths &paths) { // Update commands map when the vector of commands has been updated auto addFirstMatchToMap = [this](auto args) { @@ -418,8 +291,10 @@ void CommandController::initialize(Settings &, Paths &paths) this->maxSpaces_ = maxSpaces; }; - this->items.itemInserted.connect(addFirstMatchToMap); - this->items.itemRemoved.connect(addFirstMatchToMap); + // We can safely ignore these signal connections since items will be destroyed + // before CommandController + std::ignore = this->items.itemInserted.connect(addFirstMatchToMap); + std::ignore = this->items.itemRemoved.connect(addFirstMatchToMap); // Initialize setting manager for commands.json auto path = combinePath(paths.settingsDirectory, "commands.json"); @@ -435,7 +310,7 @@ void CommandController::initialize(Settings &, Paths &paths) // Update the setting when the vector of commands has been updated (most // likely from the settings dialog) - this->items.delayedItemsChanged.connect([this] { + std::ignore = this->items.delayedItemsChanged.connect([this] { this->commandsSetting_->setValue(this->items.raw()); }); @@ -451,750 +326,148 @@ void CommandController::initialize(Settings &, Paths &paths) /// Deprecated commands - auto blockLambda = [](const auto &words, auto channel) { - if (words.size() < 2) - { - channel->addMessage(makeSystemMessage("Usage: /block ")); - return ""; - } + this->registerCommand("/ignore", &commands::ignoreUser); - auto currentUser = getApp()->accounts->twitch.getCurrent(); + this->registerCommand("/unignore", &commands::unignoreUser); - if (currentUser->isAnon()) - { - channel->addMessage( - makeSystemMessage("You must be logged in to block someone!")); - return ""; - } + this->registerCommand("/follow", &commands::follow); - auto target = words.at(1); - stripChannelName(target); - - getHelix()->getUserByName( - target, - [currentUser, channel, target](const HelixUser &targetUser) { - getApp()->accounts->twitch.getCurrent()->blockUser( - targetUser.id, - [channel, target, targetUser] { - channel->addMessage(makeSystemMessage( - QString("You successfully blocked user %1") - .arg(target))); - }, - [channel, target] { - channel->addMessage(makeSystemMessage( - QString("User %1 couldn't be blocked, an unknown " - "error occurred!") - .arg(target))); - }); - }, - [channel, target] { - channel->addMessage( - makeSystemMessage(QString("User %1 couldn't be blocked, no " - "user with that name found!") - .arg(target))); - }); - - return ""; - }; - - auto unblockLambda = [](const auto &words, auto channel) { - if (words.size() < 2) - { - channel->addMessage(makeSystemMessage("Usage: /unblock ")); - return ""; - } - - auto currentUser = getApp()->accounts->twitch.getCurrent(); - - if (currentUser->isAnon()) - { - channel->addMessage( - makeSystemMessage("You must be logged in to unblock someone!")); - return ""; - } - - auto target = words.at(1); - stripChannelName(target); - - getHelix()->getUserByName( - target, - [currentUser, channel, target](const auto &targetUser) { - getApp()->accounts->twitch.getCurrent()->unblockUser( - targetUser.id, - [channel, target, targetUser] { - channel->addMessage(makeSystemMessage( - QString("You successfully unblocked user %1") - .arg(target))); - }, - [channel, target] { - channel->addMessage(makeSystemMessage( - QString("User %1 couldn't be unblocked, an unknown " - "error occurred!") - .arg(target))); - }); - }, - [channel, target] { - channel->addMessage( - makeSystemMessage(QString("User %1 couldn't be unblocked, " - "no user with that name found!") - .arg(target))); - }); - - return ""; - }; - - this->registerCommand( - "/ignore", [blockLambda](const auto &words, auto channel) { - channel->addMessage(makeSystemMessage( - "Ignore command has been renamed to /block, please use it from " - "now on as /ignore is going to be removed soon.")); - blockLambda(words, channel); - return ""; - }); - - this->registerCommand( - "/unignore", [unblockLambda](const auto &words, auto channel) { - channel->addMessage(makeSystemMessage( - "Unignore command has been renamed to /unblock, please use it " - "from now on as /unignore is going to be removed soon.")); - unblockLambda(words, channel); - return ""; - }); - - this->registerCommand("/follow", [](const auto &words, auto channel) { - channel->addMessage(makeSystemMessage( - "Twitch has removed the ability to follow users through " - "third-party applications. For more information, see " - "https://github.com/Chatterino/chatterino2/issues/3076")); - return ""; - }); - - this->registerCommand("/unfollow", [](const auto &words, auto channel) { - channel->addMessage(makeSystemMessage( - "Twitch has removed the ability to unfollow users through " - "third-party applications. For more information, see " - "https://github.com/Chatterino/chatterino2/issues/3076")); - return ""; - }); + this->registerCommand("/unfollow", &commands::unfollow); /// Supported commands - this->registerCommand( - "/debug-args", [](const auto & /*words*/, auto channel) { - QString msg = QApplication::instance()->arguments().join(' '); + this->registerCommand("/debug-args", &commands::listArgs); - channel->addMessage(makeSystemMessage(msg)); + this->registerCommand("/debug-env", &commands::listEnvironmentVariables); - return ""; - }); + this->registerCommand("/uptime", &commands::uptime); - this->registerCommand("/debug-env", [](const auto & /*words*/, - ChannelPtr channel) { - auto env = Env::get(); + this->registerCommand("/block", &commands::blockUser); - QStringList debugMessages{ - "recentMessagesApiUrl: " + env.recentMessagesApiUrl, - "linkResolverUrl: " + env.linkResolverUrl, - "twitchServerHost: " + env.twitchServerHost, - "twitchServerPort: " + QString::number(env.twitchServerPort), - "twitchServerSecure: " + QString::number(env.twitchServerSecure), - }; + this->registerCommand("/unblock", &commands::unblockUser); - for (QString &str : debugMessages) - { - MessageBuilder builder; - builder.emplace(QTime::currentTime()); - builder.emplace(str, MessageElementFlag::Text, - MessageColor::System); - channel->addMessage(builder.release()); - } - return ""; - }); + this->registerCommand("/user", &commands::user); - this->registerCommand("/uptime", [](const auto & /*words*/, auto channel) { - auto *twitchChannel = dynamic_cast(channel.get()); - if (twitchChannel == nullptr) - { - channel->addMessage(makeSystemMessage( - "The /uptime command only works in Twitch Channels")); - return ""; - } + this->registerCommand("/usercard", &commands::openUsercard); - const auto &streamStatus = twitchChannel->accessStreamStatus(); + this->registerCommand("/requests", &commands::requests); - QString messageText = - streamStatus->live ? streamStatus->uptime : "Channel is not live."; + this->registerCommand("/lowtrust", &commands::lowtrust); - channel->addMessage(makeSystemMessage(messageText)); + this->registerCommand("/chatters", &commands::chatters); - return ""; - }); + this->registerCommand("/test-chatters", &commands::testChatters); - this->registerCommand("/block", blockLambda); + this->registerCommand("/mods", &commands::getModerators); - this->registerCommand("/unblock", unblockLambda); + this->registerCommand("/clip", &commands::clip); - this->registerCommand("/user", [](const auto &words, auto channel) { - if (words.size() < 2) - { - channel->addMessage( - makeSystemMessage("Usage: /user [channel]")); - return ""; - } - QString userName = words[1]; - stripUserName(userName); + this->registerCommand("/marker", &commands::marker); - QString channelName = channel->getName(); + this->registerCommand("/streamlink", &commands::streamlink); - if (words.size() > 2) - { - channelName = words[2]; - stripChannelName(channelName); - } - openTwitchUsercard(channelName, userName); + this->registerCommand("/popout", &commands::popout); - return ""; - }); + this->registerCommand("/popup", &commands::popup); - this->registerCommand("/usercard", [](const auto &words, auto channel) { - if (words.size() < 2) - { - channel->addMessage( - makeSystemMessage("Usage: /usercard [channel]")); - return ""; - } + this->registerCommand("/clearmessages", &commands::clearmessages); - QString userName = words[1]; - stripUserName(userName); + this->registerCommand("/settitle", &commands::setTitle); - if (words.size() > 2) - { - QString channelName = words[2]; - stripChannelName(channelName); + this->registerCommand("/setgame", &commands::setGame); - ChannelPtr channelTemp = - getApp()->twitch->getChannelOrEmpty(channelName); + this->registerCommand("/openurl", &commands::openURL); - if (channelTemp->isEmpty()) - { - channel->addMessage(makeSystemMessage( - "A usercard can only be displayed for a channel that is " - "currently opened in Chatterino.")); - return ""; - } + this->registerCommand("/raw", &commands::sendRawMessage); - channel = channelTemp; - } - - auto *userPopup = new UserInfoPopup( - getSettings()->autoCloseUserPopup, - static_cast(&(getApp()->windows->getMainWindow())), - nullptr); - userPopup->setData(userName, channel); - userPopup->move(QCursor::pos()); - userPopup->show(); - return ""; - }); - - this->registerCommand("/requests", [](const QStringList &words, - ChannelPtr channel) { - QString target(words.value(1)); - - if (target.isEmpty()) - { - if (channel->getType() == Channel::Type::Twitch && - !channel->isEmpty()) - { - target = channel->getName(); - } - else - { - channel->addMessage(makeSystemMessage( - "Usage: /requests [channel]. You can also use the command " - "without arguments in any Twitch channel to open its " - "channel points requests queue. Only the broadcaster and " - "moderators have permission to view the queue.")); - return ""; - } - } - - stripChannelName(target); - QDesktopServices::openUrl( - QUrl(QString("https://www.twitch.tv/popout/%1/reward-queue") - .arg(target))); - - return ""; - }); - - this->registerCommand( - "/chatters", [](const auto & /*words*/, auto channel) { - auto twitchChannel = dynamic_cast(channel.get()); - - if (twitchChannel == nullptr) - { - channel->addMessage(makeSystemMessage( - "The /chatters command only works in Twitch Channels")); - return ""; - } - - channel->addMessage(makeSystemMessage( - QString("Chatter count: %1") - .arg(localizeNumbers(twitchChannel->chatterCount())))); - - return ""; - }); - - this->registerCommand("/clip", [](const auto & /*words*/, auto channel) { - if (const auto type = channel->getType(); - type != Channel::Type::Twitch && - type != Channel::Type::TwitchWatching) - { - return ""; - } - - auto *twitchChannel = dynamic_cast(channel.get()); - - twitchChannel->createClip(); - - return ""; - }); - - this->registerCommand("/marker", [](const QStringList &words, - auto channel) { - auto *twitchChannel = dynamic_cast(channel.get()); - if (twitchChannel == nullptr) - { - channel->addMessage(makeSystemMessage( - "The /marker command only works in Twitch channels")); - return ""; - } - - // Avoid Helix calls without Client ID and/or OAuth Token - if (getApp()->accounts->twitch.getCurrent()->isAnon()) - { - channel->addMessage(makeSystemMessage( - "You need to be logged in to create stream markers!")); - return ""; - } - - // Exact same message as in webchat - if (!twitchChannel->isLive()) - { - channel->addMessage(makeSystemMessage( - "You can only add stream markers during live streams. Try " - "again when the channel is live streaming.")); - return ""; - } - - auto arguments = words; - arguments.removeFirst(); - - getHelix()->createStreamMarker( - // Limit for description is 140 characters, webchat just crops description - // if it's >140 characters, so we're doing the same thing - twitchChannel->roomId(), arguments.join(" ").left(140), - [channel, arguments](const HelixStreamMarker &streamMarker) { - channel->addMessage(makeSystemMessage( - QString("Successfully added a stream marker at %1%2") - .arg(formatTime(streamMarker.positionSeconds)) - .arg(streamMarker.description.isEmpty() - ? "" - : QString(": \"%1\"") - .arg(streamMarker.description)))); - }, - [channel](auto error) { - QString errorMessage("Failed to create stream marker - "); - - switch (error) - { - case HelixStreamMarkerError::UserNotAuthorized: { - errorMessage += - "you don't have permission to perform that action."; - } - break; - - case HelixStreamMarkerError::UserNotAuthenticated: { - errorMessage += "you need to re-authenticate."; - } - break; - - // This would most likely happen if the service is down, or if the JSON payload returned has changed format - case HelixStreamMarkerError::Unknown: - default: { - errorMessage += "an unknown error occurred."; - } - break; - } - - channel->addMessage(makeSystemMessage(errorMessage)); - }); - - return ""; - }); - - this->registerCommand("/streamlink", [](const QStringList &words, - ChannelPtr channel) { - QString target(words.value(1)); - - if (target.isEmpty()) - { - if (channel->getType() == Channel::Type::Twitch && - !channel->isEmpty()) - { - target = channel->getName(); - } - else - { - channel->addMessage(makeSystemMessage( - "/streamlink [channel]. Open specified Twitch channel in " - "streamlink. If no channel argument is specified, open the " - "current Twitch channel instead.")); - return ""; - } - } - - stripChannelName(target); - openStreamlinkForChannel(target); - - return ""; - }); - - this->registerCommand("/popout", [](const QStringList &words, - ChannelPtr channel) { - QString target(words.value(1)); - - if (target.isEmpty()) - { - if (channel->getType() == Channel::Type::Twitch && - !channel->isEmpty()) - { - target = channel->getName(); - } - else - { - channel->addMessage(makeSystemMessage( - "Usage: /popout . You can also use the command " - "without arguments in any Twitch channel to open its " - "popout chat.")); - return ""; - } - } - - stripChannelName(target); - QDesktopServices::openUrl( - QUrl(QString("https://www.twitch.tv/popout/%1/chat?popout=") - .arg(target))); - - return ""; - }); - - this->registerCommand("/popup", [](const QStringList &words, - ChannelPtr sourceChannel) { - static const auto *usageMessage = - "Usage: /popup [channel]. Open specified Twitch channel in " - "a new window. If no channel argument is specified, open " - "the currently selected split instead."; - - QString target(words.value(1)); - stripChannelName(target); - - // Popup the current split - if (target.isEmpty()) - { - auto *currentPage = - dynamic_cast(getApp() - ->windows->getMainWindow() - .getNotebook() - .getSelectedPage()); - if (currentPage != nullptr) - { - auto *currentSplit = currentPage->getSelectedSplit(); - if (currentSplit != nullptr) - { - currentSplit->popup(); - - return ""; - } - } - - sourceChannel->addMessage(makeSystemMessage(usageMessage)); - return ""; - } - - // Open channel passed as argument in a popup - auto *app = getApp(); - auto targetChannel = app->twitch->getOrAddChannel(target); - app->windows->openInPopup(targetChannel); - - return ""; - }); - - this->registerCommand("/clearmessages", [](const auto & /*words*/, - ChannelPtr channel) { - auto *currentPage = dynamic_cast( - getApp()->windows->getMainWindow().getNotebook().getSelectedPage()); - - if (auto split = currentPage->getSelectedSplit()) - { - split->getChannelView().clearMessages(); - } - - return ""; - }); - - this->registerCommand("/settitle", [](const QStringList &words, - ChannelPtr channel) { - if (words.size() < 2) - { - channel->addMessage( - makeSystemMessage("Usage: /settitle ")); - return ""; - } - if (auto twitchChannel = dynamic_cast(channel.get())) - { - auto status = twitchChannel->accessStreamStatus(); - auto title = words.mid(1).join(" "); - getHelix()->updateChannel( - twitchChannel->roomId(), "", "", title, - [channel, title](NetworkResult) { - channel->addMessage(makeSystemMessage( - QString("Updated title to %1").arg(title))); - }, - [channel] { - channel->addMessage( - makeSystemMessage("Title update failed! Are you " - "missing the required scope?")); - }); - } - else - { - channel->addMessage(makeSystemMessage( - "Unable to set title of non-Twitch channel.")); - } - return ""; - }); - - this->registerCommand("/setgame", [](const QStringList &words, - const ChannelPtr channel) { - if (words.size() < 2) - { - channel->addMessage( - makeSystemMessage("Usage: /setgame ")); - return ""; - } - if (auto twitchChannel = dynamic_cast(channel.get())) - { - const auto gameName = words.mid(1).join(" "); - - getHelix()->searchGames( - gameName, - [channel, twitchChannel, - gameName](const std::vector &games) { - if (games.empty()) - { - channel->addMessage( - makeSystemMessage("Game not found.")); - return; - } - - auto matchedGame = games.at(0); - - if (games.size() > 1) - { - // NOTE: Improvements could be made with 'fuzzy string matching' code here - // attempt to find the best looking game by comparing exactly with lowercase values - for (const auto &game : games) - { - if (game.name.toLower() == gameName.toLower()) - { - matchedGame = game; - break; - } - } - } - - auto status = twitchChannel->accessStreamStatus(); - getHelix()->updateChannel( - twitchChannel->roomId(), matchedGame.id, "", "", - [channel, games, matchedGame](const NetworkResult &) { - channel->addMessage( - makeSystemMessage(QString("Updated game to %1") - .arg(matchedGame.name))); - }, - [channel] { - channel->addMessage(makeSystemMessage( - "Game update failed! Are you " - "missing the required scope?")); - }); - }, - [channel] { - channel->addMessage( - makeSystemMessage("Failed to look up game.")); - }); - } - else - { - channel->addMessage( - makeSystemMessage("Unable to set game of non-Twitch channel.")); - } - return ""; - }); - - this->registerCommand("/openurl", [](const QStringList &words, - const ChannelPtr channel) { - if (words.size() < 2) - { - channel->addMessage(makeSystemMessage("Usage: /openurl ")); - return ""; - } - - QUrl url = QUrl::fromUserInput(words.mid(1).join(" ")); - if (!url.isValid()) - { - channel->addMessage(makeSystemMessage("Invalid URL specified.")); - return ""; - } - - bool res = false; - if (supportsIncognitoLinks() && getSettings()->openLinksIncognito) - { - res = openLinkIncognito(url.toString(QUrl::FullyEncoded)); - } - else - { - res = QDesktopServices::openUrl(url); - } - - if (!res) - { - channel->addMessage(makeSystemMessage("Could not open URL.")); - } - - return ""; - }); - - this->registerCommand( - "/delete", [](const QStringList &words, ChannelPtr channel) -> QString { - // This is a wrapper over the standard Twitch /delete command - // We use this to ensure the user gets better error messages for missing or malformed arguments - if (words.size() < 2) - { - channel->addMessage( - makeSystemMessage("Usage: /delete - Deletes the " - "specified message.")); - return ""; - } - - auto messageID = words.at(1); - auto uuid = QUuid(messageID); - if (uuid.isNull()) - { - // The message id must be a valid UUID - channel->addMessage(makeSystemMessage( - QString("Invalid msg-id: \"%1\"").arg(messageID))); - return ""; - } - - auto msg = channel->findMessage(messageID); - if (msg != nullptr) - { - if (msg->loginName == channel->getName() && - !channel->isBroadcaster()) - { - channel->addMessage(makeSystemMessage( - "You cannot delete the broadcaster's messages unless " - "you are the broadcaster.")); - return ""; - } - } - - return QString("/delete ") + messageID; - }); - - this->registerCommand("/raw", [](const QStringList &words, ChannelPtr) { - getApp()->twitch->sendRawMessage(words.mid(1).join(" ")); - return ""; - }); - - this->registerCommand( - "/reply", [](const QStringList &words, ChannelPtr channel) { - auto *twitchChannel = dynamic_cast(channel.get()); - if (twitchChannel == nullptr) - { - channel->addMessage(makeSystemMessage( - "The /reply command only works in Twitch channels")); - return ""; - } - - if (words.size() < 3) - { - channel->addMessage( - makeSystemMessage("Usage: /reply ")); - return ""; - } - - QString username = words[1]; - stripChannelName(username); - - auto snapshot = twitchChannel->getMessageSnapshot(); - for (auto it = snapshot.rbegin(); it != snapshot.rend(); ++it) - { - const auto &msg = *it; - if (msg->loginName.compare(username, Qt::CaseInsensitive) == 0) - { - std::shared_ptr thread; - // found most recent message by user - if (msg->replyThread == nullptr) - { - thread = std::make_shared(msg); - twitchChannel->addReplyThread(thread); - } - else - { - thread = msg->replyThread; - } - - QString reply = words.mid(2).join(" "); - twitchChannel->sendReply(reply, thread->rootId()); - return ""; - } - } - - channel->addMessage( - makeSystemMessage("A message from that user wasn't found")); - - return ""; - }); + this->registerCommand("/reply", &commands::sendReply); #ifndef NDEBUG - this->registerCommand( - "/fakemsg", - [](const QStringList &words, ChannelPtr channel) -> QString { - if (words.size() < 2) - { - channel->addMessage(makeSystemMessage( - "Usage: /fakemsg (raw irc text) - injects raw irc text as " - "if it was a message received from TMI")); - return ""; - } - auto ircText = words.mid(1).join(" "); - getApp()->twitch->addFakeMessage(ircText); - return ""; - }); + this->registerCommand("/fakemsg", &commands::injectFakeMessage); + this->registerCommand("/debug-update-to-no-stream", + &commands::injectStreamUpdateNoStream); #endif - this->registerCommand( - "/copy", [](const QStringList &words, ChannelPtr channel) -> QString { - if (words.size() < 2) - { - channel->addMessage( - makeSystemMessage("Usage: /copy - copies provided " - "text to clipboard.")); - return ""; - } - crossPlatformCopy(words.mid(1).join(" ")); - return ""; - }); + this->registerCommand("/copy", &commands::copyToClipboard); + + this->registerCommand("/color", &commands::updateUserColor); + + this->registerCommand("/clear", &commands::deleteAllMessages); + + this->registerCommand("/delete", &commands::deleteOneMessage); + + this->registerCommand("/mod", &commands::addModerator); + + this->registerCommand("/unmod", &commands::removeModerator); + + this->registerCommand("/announce", &commands::sendAnnouncement); + this->registerCommand("/announceblue", &commands::sendAnnouncementBlue); + this->registerCommand("/announcegreen", &commands::sendAnnouncementGreen); + this->registerCommand("/announceorange", &commands::sendAnnouncementOrange); + this->registerCommand("/announcepurple", &commands::sendAnnouncementPurple); + + this->registerCommand("/vip", &commands::addVIP); + + this->registerCommand("/unvip", &commands::removeVIP); + + this->registerCommand("/unban", &commands::unbanUser); + this->registerCommand("/untimeout", &commands::unbanUser); + + this->registerCommand("/raid", &commands::startRaid); + + this->registerCommand("/unraid", &commands::cancelRaid); + + this->registerCommand("/emoteonly", &commands::emoteOnly); + this->registerCommand("/emoteonlyoff", &commands::emoteOnlyOff); + + this->registerCommand("/subscribers", &commands::subscribers); + this->registerCommand("/subscribersoff", &commands::subscribersOff); + + this->registerCommand("/slow", &commands::slow); + this->registerCommand("/slowoff", &commands::slowOff); + + this->registerCommand("/followers", &commands::followers); + this->registerCommand("/followersoff", &commands::followersOff); + + this->registerCommand("/uniquechat", &commands::uniqueChat); + this->registerCommand("/r9kbeta", &commands::uniqueChat); + this->registerCommand("/uniquechatoff", &commands::uniqueChatOff); + this->registerCommand("/r9kbetaoff", &commands::uniqueChatOff); + + this->registerCommand("/timeout", &commands::sendTimeout); + + this->registerCommand("/ban", &commands::sendBan); + this->registerCommand("/banid", &commands::sendBanById); + + this->registerCommand("/warn", &commands::sendWarn); + + for (const auto &cmd : TWITCH_WHISPER_COMMANDS) + { + this->registerCommand(cmd, &commands::sendWhisper); + } + + this->registerCommand("/vips", &commands::getVIPs); + + this->registerCommand("/commercial", &commands::startCommercial); + + this->registerCommand("/unstable-set-user-color", + &commands::unstableSetUserClientSideColor); + + this->registerCommand("/debug-force-image-gc", + &commands::forceImageGarbageCollection); + + this->registerCommand("/debug-force-image-unload", + &commands::forceImageUnload); + + this->registerCommand("/debug-test", &commands::debugTest); + + this->registerCommand("/shield", &commands::shieldModeOn); + this->registerCommand("/shieldoff", &commands::shieldModeOff); + + this->registerCommand("/shoutout", &commands::sendShoutout); + + this->registerCommand("/c2-set-logging-rules", &commands::setLoggingRules); + this->registerCommand("/c2-theme-autoreload", &commands::toggleThemeReload); } void CommandController::save() @@ -1213,7 +486,8 @@ CommandModel *CommandController::createModel(QObject *parent) QString CommandController::execCommand(const QString &textNoEmoji, ChannelPtr channel, bool dryRun) { - QString text = getApp()->emotes->emojis.replaceShortCodes(textNoEmoji); + QString text = + getApp()->getEmotes()->getEmojis()->replaceShortCodes(textNoEmoji); QStringList words = text.split(' ', Qt::SkipEmptyParts); if (words.length() == 0) @@ -1223,32 +497,12 @@ QString CommandController::execCommand(const QString &textNoEmoji, QString commandName = words[0]; - // works in a valid Twitch channel and /whispers, etc... - if (!dryRun && channel->isTwitchChannel()) - { - if (TWITCH_WHISPER_COMMANDS.contains(commandName, Qt::CaseInsensitive)) - { - if (words.length() > 2) - { - appendWhisperMessageWordsLocally(words); - sendWhisperMessage(text); - } - else - { - channel->addMessage( - makeSystemMessage("Usage: /w ")); - } - - return ""; - } - } - { // check if user command exists const auto it = this->userCommands_.find(commandName); if (it != this->userCommands_.end()) { - text = getApp()->emotes->emojis.replaceShortCodes( + text = getApp()->getEmotes()->getEmojis()->replaceShortCodes( this->execCustomCommand(words, it.value(), dryRun, channel)); words = text.split(' ', Qt::SkipEmptyParts); @@ -1262,18 +516,33 @@ QString CommandController::execCommand(const QString &textNoEmoji, } } - // works only in a valid Twitch channel - if (!dryRun && channel->isTwitchChannel()) + if (!dryRun) { // check if command exists const auto it = this->commands_.find(commandName); if (it != this->commands_.end()) { - return it.value()(words, channel); + if (auto *command = std::get_if(&it->second)) + { + return (*command)(words, channel); + } + if (auto *command = + std::get_if(&it->second)) + { + CommandContext ctx{ + words, + channel, + dynamic_cast(channel.get()), + }; + return (*command)(ctx); + } + + return ""; } } - auto maxSpaces = std::min(this->maxSpaces_, words.length() - 1); + // We have checks to ensure words cannot be empty, so this can never wrap around + auto maxSpaces = std::min(this->maxSpaces_, (qsizetype)words.length() - 1); for (int i = 0; i < maxSpaces; ++i) { commandName += ' ' + words[i + 1]; @@ -1287,26 +556,51 @@ QString CommandController::execCommand(const QString &textNoEmoji, if (!dryRun && channel->getType() == Channel::Type::TwitchWhispers) { - channel->addMessage( - makeSystemMessage("Use /w to whisper")); + channel->addSystemMessage("Use /w to whisper"); return ""; } return text; } -void CommandController::registerCommand(QString commandName, - CommandFunction commandFunction) +#ifdef CHATTERINO_HAVE_PLUGINS +bool CommandController::registerPluginCommand(const QString &commandName) { - assert(!this->commands_.contains(commandName)); + if (this->commands_.contains(commandName)) + { + return false; + } - this->commands_[commandName] = commandFunction; + this->commands_[commandName] = [commandName](const CommandContext &ctx) { + return getApp()->getPlugins()->tryExecPluginCommand(commandName, ctx); + }; + this->pluginCommands_.append(commandName); + return true; +} + +bool CommandController::unregisterPluginCommand(const QString &commandName) +{ + if (!this->pluginCommands_.contains(commandName)) + { + return false; + } + this->pluginCommands_.removeAll(commandName); + return this->commands_.erase(commandName) != 0; +} +#endif + +void CommandController::registerCommand(const QString &commandName, + CommandFunctionVariants commandFunction) +{ + assert(this->commands_.count(commandName) == 0); + + this->commands_[commandName] = std::move(commandFunction); this->defaultChatterinoCommandAutoCompletions_.append(commandName); } QString CommandController::execCustomCommand( - const QStringList &words, const Command &command, bool dryRun, + const QStringList &words, const Command &command, bool /* dryRun */, ChannelPtr channel, const Message *message, std::unordered_map context) { @@ -1401,17 +695,7 @@ QString CommandController::execCustomCommand( result = result.mid(1); } - auto res = result.replace("{{", "{"); - - if (dryRun || !appendWhisperMessageStringLocally(res)) - { - return res; - } - else - { - sendWhisperMessage(res); - return ""; - } + return result.replace("{{", "{"); } QStringList CommandController::getDefaultChatterinoCommandList() diff --git a/src/controllers/commands/CommandController.hpp b/src/controllers/commands/CommandController.hpp index f36ea3be6..d730c671d 100644 --- a/src/controllers/commands/CommandController.hpp +++ b/src/controllers/commands/CommandController.hpp @@ -1,27 +1,29 @@ #pragma once -#include "common/ChatterinoSetting.hpp" #include "common/SignalVector.hpp" -#include "common/Singleton.hpp" -#include "controllers/commands/Command.hpp" -#include "providers/twitch/TwitchChannel.hpp" +#include "util/QStringHash.hpp" -#include #include +#include #include #include #include +#include namespace chatterino { class Settings; class Paths; class Channel; +using ChannelPtr = std::shared_ptr; +struct Message; +struct Command; class CommandModel; +struct CommandContext; -class CommandController final : public Singleton +class CommandController final { public: SignalVector items; @@ -30,8 +32,8 @@ public: bool dryRun); QStringList getDefaultChatterinoCommandList(); - virtual void initialize(Settings &, Paths &paths) override; - virtual void save() override; + CommandController(const Paths &paths); + void save(); CommandModel *createModel(QObject *parent); @@ -39,6 +41,15 @@ public: const QStringList &words, const Command &command, bool dryRun, ChannelPtr channel, const Message *message = nullptr, std::unordered_map context = {}); +#ifdef CHATTERINO_HAVE_PLUGINS + bool registerPluginCommand(const QString &commandName); + bool unregisterPluginCommand(const QString &commandName); + + const QStringList &pluginCommands() + { + return this->pluginCommands_; + } +#endif private: void load(Paths &paths); @@ -46,14 +57,20 @@ private: using CommandFunction = std::function; - void registerCommand(QString commandName, CommandFunction commandFunction); + using CommandFunctionWithContext = std::function; + + using CommandFunctionVariants = + std::variant; + + void registerCommand(const QString &commandName, + CommandFunctionVariants commandFunction); // Chatterino commands - QMap commands_; + std::unordered_map commands_; // User-created commands QMap userCommands_; - int maxSpaces_ = 0; + qsizetype maxSpaces_ = 0; std::shared_ptr sm_; // Because the setting manager is not initialized until the initialize @@ -64,6 +81,9 @@ private: commandsSetting_; QStringList defaultChatterinoCommandAutoCompletions_; +#ifdef CHATTERINO_HAVE_PLUGINS + QStringList pluginCommands_; +#endif }; } // namespace chatterino diff --git a/src/controllers/commands/CommandModel.cpp b/src/controllers/commands/CommandModel.cpp index a3117d3ab..1bee92be2 100644 --- a/src/controllers/commands/CommandModel.cpp +++ b/src/controllers/commands/CommandModel.cpp @@ -1,5 +1,7 @@ -#include "CommandModel.hpp" +#include "controllers/commands/CommandModel.hpp" +#include "common/SignalVector.hpp" +#include "controllers/commands/Command.hpp" #include "util/StandardItemHelper.hpp" namespace chatterino { diff --git a/src/controllers/commands/CommandModel.hpp b/src/controllers/commands/CommandModel.hpp index 701a65f3e..7dc458b85 100644 --- a/src/controllers/commands/CommandModel.hpp +++ b/src/controllers/commands/CommandModel.hpp @@ -1,13 +1,13 @@ #pragma once -#include - #include "common/SignalVectorModel.hpp" -#include "controllers/commands/Command.hpp" + +#include namespace chatterino { class CommandController; +struct Command; class CommandModel : public SignalVectorModel { @@ -22,12 +22,12 @@ class CommandModel : public SignalVectorModel protected: // turn a vector item into a model row - virtual Command getItemFromRow(std::vector &row, - const Command &command) override; + Command getItemFromRow(std::vector &row, + const Command &command) override; // turns a row in the model into a vector item - virtual void getRowFromItem(const Command &item, - std::vector &row) override; + void getRowFromItem(const Command &item, + std::vector &row) override; friend class CommandController; }; diff --git a/src/controllers/commands/builtin/Misc.cpp b/src/controllers/commands/builtin/Misc.cpp new file mode 100644 index 000000000..a1adf5b34 --- /dev/null +++ b/src/controllers/commands/builtin/Misc.cpp @@ -0,0 +1,728 @@ +#include "controllers/commands/builtin/Misc.hpp" + +#include "Application.hpp" +#include "common/Channel.hpp" +#include "controllers/accounts/AccountController.hpp" +#include "controllers/commands/CommandContext.hpp" +#include "controllers/userdata/UserDataController.hpp" +#include "providers/twitch/api/Helix.hpp" +#include "providers/twitch/TwitchAccount.hpp" +#include "providers/twitch/TwitchChannel.hpp" +#include "providers/twitch/TwitchIrcServer.hpp" +#include "singletons/Settings.hpp" +#include "singletons/WindowManager.hpp" +#include "util/Clipboard.hpp" +#include "util/FormatTime.hpp" +#include "util/IncognitoBrowser.hpp" +#include "util/StreamLink.hpp" +#include "util/Twitch.hpp" +#include "widgets/dialogs/UserInfoPopup.hpp" +#include "widgets/helper/ChannelView.hpp" +#include "widgets/Notebook.hpp" +#include "widgets/splits/Split.hpp" +#include "widgets/splits/SplitContainer.hpp" +#include "widgets/Window.hpp" + +#include +#include +#include +#include + +#include + +namespace chatterino::commands { + +QString follow(const CommandContext &ctx) +{ + if (ctx.twitchChannel == nullptr) + { + return ""; + } + ctx.channel->addSystemMessage( + "Twitch has removed the ability to follow users through " + "third-party applications. For more information, see " + "https://github.com/Chatterino/chatterino2/issues/3076"); + return ""; +} + +QString unfollow(const CommandContext &ctx) +{ + if (ctx.twitchChannel == nullptr) + { + return ""; + } + ctx.channel->addSystemMessage( + "Twitch has removed the ability to unfollow users through " + "third-party applications. For more information, see " + "https://github.com/Chatterino/chatterino2/issues/3076"); + return ""; +} + +QString uptime(const CommandContext &ctx) +{ + if (ctx.channel == nullptr) + { + return ""; + } + + if (ctx.twitchChannel == nullptr) + { + ctx.channel->addSystemMessage( + "The /uptime command only works in Twitch Channels."); + return ""; + } + + const auto &streamStatus = ctx.twitchChannel->accessStreamStatus(); + + QString messageText = + streamStatus->live ? streamStatus->uptime : "Channel is not live."; + + ctx.channel->addSystemMessage(messageText); + + return ""; +} + +QString user(const CommandContext &ctx) +{ + if (ctx.channel == nullptr) + { + return ""; + } + + if (ctx.words.size() < 2) + { + ctx.channel->addSystemMessage("Usage: /user [channel]"); + return ""; + } + QString userName = ctx.words[1]; + stripUserName(userName); + + QString channelName = ctx.channel->getName(); + + if (ctx.words.size() > 2) + { + channelName = ctx.words[2]; + stripChannelName(channelName); + } + openTwitchUsercard(channelName, userName); + + return ""; +} + +QString requests(const CommandContext &ctx) +{ + if (ctx.channel == nullptr) + { + return ""; + } + + QString target(ctx.words.value(1)); + + if (target.isEmpty()) + { + if (ctx.channel->getType() == Channel::Type::Twitch && + !ctx.channel->isEmpty()) + { + target = ctx.channel->getName(); + } + else + { + ctx.channel->addSystemMessage( + "Usage: /requests [channel]. You can also use the command " + "without arguments in any Twitch channel to open its " + "channel points requests queue. Only the broadcaster and " + "moderators have permission to view the queue."); + return ""; + } + } + + stripChannelName(target); + QDesktopServices::openUrl(QUrl( + QString("https://www.twitch.tv/popout/%1/reward-queue").arg(target))); + + return ""; +} + +QString lowtrust(const CommandContext &ctx) +{ + if (ctx.channel == nullptr) + { + return ""; + } + + QString target(ctx.words.value(1)); + + if (target.isEmpty()) + { + if (ctx.channel->getType() == Channel::Type::Twitch && + !ctx.channel->isEmpty()) + { + target = ctx.channel->getName(); + } + else + { + ctx.channel->addSystemMessage( + "Usage: /lowtrust [channel]. You can also use the command " + "without arguments in any Twitch channel to open its " + "suspicious user activity feed. Only the broadcaster and " + "moderators have permission to view this feed."); + return ""; + } + } + + stripChannelName(target); + QDesktopServices::openUrl(QUrl( + QString("https://www.twitch.tv/popout/moderator/%1/low-trust-users") + .arg(target))); + + return ""; +} + +QString clip(const CommandContext &ctx) +{ + if (ctx.channel == nullptr) + { + return ""; + } + + if (const auto type = ctx.channel->getType(); + type != Channel::Type::Twitch && type != Channel::Type::TwitchWatching) + { + ctx.channel->addSystemMessage( + "The /clip command only works in Twitch Channels."); + return ""; + } + + if (ctx.twitchChannel == nullptr) + { + ctx.channel->addSystemMessage( + "The /clip command only works in Twitch Channels."); + return ""; + } + + ctx.twitchChannel->createClip(); + + return ""; +} + +QString marker(const CommandContext &ctx) +{ + if (ctx.channel == nullptr) + { + return ""; + } + + if (ctx.twitchChannel == nullptr) + { + ctx.channel->addSystemMessage( + "The /marker command only works in Twitch channels."); + return ""; + } + + // Avoid Helix calls without Client ID and/or OAuth Token + if (getApp()->getAccounts()->twitch.getCurrent()->isAnon()) + { + ctx.channel->addSystemMessage( + "You need to be logged in to create stream markers!"); + return ""; + } + + // Exact same message as in webchat + if (!ctx.twitchChannel->isLive()) + { + ctx.channel->addSystemMessage( + "You can only add stream markers during live streams. Try " + "again when the channel is live streaming."); + return ""; + } + + auto arguments = ctx.words; + arguments.removeFirst(); + + getHelix()->createStreamMarker( + // Limit for description is 140 characters, webchat just crops description + // if it's >140 characters, so we're doing the same thing + ctx.twitchChannel->roomId(), arguments.join(" ").left(140), + [channel{ctx.channel}, + arguments](const HelixStreamMarker &streamMarker) { + channel->addSystemMessage( + QString("Successfully added a stream marker at %1%2") + .arg(formatTime(streamMarker.positionSeconds)) + .arg(streamMarker.description.isEmpty() + ? "" + : QString(": \"%1\"") + .arg(streamMarker.description))); + }, + [channel{ctx.channel}](auto error) { + QString errorMessage("Failed to create stream marker - "); + + switch (error) + { + case HelixStreamMarkerError::UserNotAuthorized: { + errorMessage += + "you don't have permission to perform that action."; + } + break; + + case HelixStreamMarkerError::UserNotAuthenticated: { + errorMessage += "you need to re-authenticate."; + } + break; + + // This would most likely happen if the service is down, or if the JSON payload returned has changed format + case HelixStreamMarkerError::Unknown: + default: { + errorMessage += "an unknown error occurred."; + } + break; + } + + channel->addSystemMessage(errorMessage); + }); + + return ""; +} + +QString streamlink(const CommandContext &ctx) +{ + if (ctx.channel == nullptr) + { + return ""; + } + + QString target(ctx.words.value(1)); + + if (target.isEmpty()) + { + if (ctx.channel->getType() == Channel::Type::Twitch && + !ctx.channel->isEmpty()) + { + target = ctx.channel->getName(); + } + else + { + ctx.channel->addSystemMessage( + "/streamlink [channel]. Open specified Twitch channel in " + "streamlink. If no channel argument is specified, open the " + "current Twitch channel instead."); + return ""; + } + } + + stripChannelName(target); + openStreamlinkForChannel(target); + + return ""; +} + +QString popout(const CommandContext &ctx) +{ + if (ctx.channel == nullptr) + { + return ""; + } + + QString target(ctx.words.value(1)); + + if (target.isEmpty()) + { + if (ctx.channel->getType() == Channel::Type::Twitch && + !ctx.channel->isEmpty()) + { + target = ctx.channel->getName(); + } + else + { + ctx.channel->addSystemMessage( + "Usage: /popout . You can also use the command " + "without arguments in any Twitch channel to open its " + "popout chat."); + return ""; + } + } + + stripChannelName(target); + QDesktopServices::openUrl(QUrl( + QString("https://www.twitch.tv/popout/%1/chat?popout=").arg(target))); + + return ""; +} + +QString popup(const CommandContext &ctx) +{ + if (ctx.channel == nullptr) + { + return ""; + } + + static const auto *usageMessage = + "Usage: /popup [channel]. Open specified Twitch channel in " + "a new window. If no channel argument is specified, open " + "the currently selected split instead."; + + QString target(ctx.words.value(1)); + stripChannelName(target); + + // Popup the current split + if (target.isEmpty()) + { + auto *currentPage = + dynamic_cast(getApp() + ->getWindows() + ->getMainWindow() + .getNotebook() + .getSelectedPage()); + if (currentPage != nullptr) + { + auto *currentSplit = currentPage->getSelectedSplit(); + if (currentSplit != nullptr) + { + currentSplit->popup(); + + return ""; + } + } + + ctx.channel->addSystemMessage(usageMessage); + return ""; + } + + // Open channel passed as argument in a popup + auto targetChannel = getApp()->getTwitch()->getOrAddChannel(target); + getApp()->getWindows()->openInPopup(targetChannel); + + return ""; +} + +QString clearmessages(const CommandContext &ctx) +{ + (void)ctx; + + auto *currentPage = getApp() + ->getWindows() + ->getLastSelectedWindow() + ->getNotebook() + .getSelectedPage(); + + if (auto *split = currentPage->getSelectedSplit()) + { + split->getChannelView().clearMessages(); + } + + return ""; +} + +QString openURL(const CommandContext &ctx) +{ + /** + * The /openurl command + * Takes a positional argument as the URL to open + * + * Accepts the option --private or --no-private (or --incognito or --no-incognito). + * These options will force the URL to be opened in private or non-private mode, regardless of the + * default incognito mode setting. + * + * Examples: + * - /openurl https://twitch.tv/forsen + * with the setting "Open links in incognito/private mode" enabled + * Opens https://twitch.tv/forsen in private mode + * - /openurl https://twitch.tv/forsen + * with the setting "Open links in incognito/private mode" disabled + * Opens https://twitch.tv/forsen in normal mode + * - /openurl https://twitch.tv/forsen --private + * with the setting "Open links in incognito/private mode" disabled + * Opens https://twitch.tv/forsen in private mode + * - /openurl https://twitch.tv/forsen --no-private + * with the setting "Open links in incognito/private mode" enabled + * Opens https://twitch.tv/forsen in normal mode + */ + if (ctx.channel == nullptr) + { + return ""; + } + + QCommandLineParser parser; + parser.setOptionsAfterPositionalArgumentsMode( + QCommandLineParser::ParseAsPositionalArguments); + parser.addPositionalArgument("URL", "The URL to open"); + QCommandLineOption privateModeOption( + { + "private", + "incognito", + }, + "Force private mode. Cannot be used together with --no-private"); + QCommandLineOption noPrivateModeOption( + { + "no-private", + "no-incognito", + }, + "Force non-private mode. Cannot be used together with --private"); + parser.addOptions({ + privateModeOption, + noPrivateModeOption, + }); + parser.parse(ctx.words); + + const auto &positionalArguments = parser.positionalArguments(); + if (positionalArguments.isEmpty()) + { + ctx.channel->addSystemMessage( + "Usage: /openurl [--incognito/--no-incognito]"); + return ""; + } + auto urlString = parser.positionalArguments().join(' '); + + QUrl url = QUrl::fromUserInput(urlString); + if (!url.isValid()) + { + ctx.channel->addSystemMessage("Invalid URL specified."); + return ""; + } + + auto preferPrivateMode = getSettings()->openLinksIncognito.getValue(); + auto forcePrivateMode = parser.isSet(privateModeOption); + auto forceNonPrivateMode = parser.isSet(noPrivateModeOption); + + if (forcePrivateMode && forceNonPrivateMode) + { + ctx.channel->addSystemMessage( + "Error: /openurl may only be called with --incognito or " + "--no-incognito, not both at the same time."); + return ""; + } + + bool usePrivateMode = false; + + if (forceNonPrivateMode) + { + usePrivateMode = false; + } + else if (supportsIncognitoLinks() && + (forcePrivateMode || preferPrivateMode)) + { + usePrivateMode = true; + } + + bool res = false; + if (usePrivateMode) + { + res = openLinkIncognito(url.toString(QUrl::FullyEncoded)); + } + else + { + res = QDesktopServices::openUrl(url); + } + + if (!res) + { + ctx.channel->addSystemMessage("Could not open URL."); + } + + return ""; +} + +QString sendRawMessage(const CommandContext &ctx) +{ + if (ctx.channel == nullptr) + { + return ""; + } + + if (ctx.channel->isTwitchChannel()) + { + getApp()->getTwitch()->sendRawMessage(ctx.words.mid(1).join(" ")); + } + else + { + // other code down the road handles this for IRC + return ctx.words.join(" "); + } + return ""; +} + +QString injectFakeMessage(const CommandContext &ctx) +{ + if (ctx.channel == nullptr) + { + return ""; + } + + if (!ctx.channel->isTwitchChannel()) + { + ctx.channel->addSystemMessage( + "The /fakemsg command only works in Twitch channels."); + return ""; + } + + if (ctx.words.size() < 2) + { + ctx.channel->addSystemMessage( + "Usage: /fakemsg (raw irc text) - injects raw irc text as " + "if it was a message received from TMI"); + return ""; + } + + auto ircText = ctx.words.mid(1).join(" "); + getApp()->getTwitch()->addFakeMessage(ircText); + + return ""; +} + +QString injectStreamUpdateNoStream(const CommandContext &ctx) +{ + /** + * /debug-update-to-no-stream makes the current channel mimic going offline + */ + if (ctx.channel == nullptr) + { + return ""; + } + if (ctx.twitchChannel == nullptr) + { + ctx.channel->addSystemMessage( + "The /debug-update-to-no-stream command only " + "works in Twitch channels"); + return ""; + } + + ctx.twitchChannel->updateStreamStatus(std::nullopt, false); + return ""; +} + +QString copyToClipboard(const CommandContext &ctx) +{ + if (ctx.channel == nullptr) + { + return ""; + } + + if (ctx.words.size() < 2) + { + ctx.channel->addSystemMessage("Usage: /copy - copies provided " + "text to clipboard."); + return ""; + } + + crossPlatformCopy(ctx.words.mid(1).join(" ")); + return ""; +} + +QString unstableSetUserClientSideColor(const CommandContext &ctx) +{ + if (ctx.channel == nullptr) + { + return ""; + } + + if (ctx.twitchChannel == nullptr) + { + ctx.channel->addSystemMessage( + "The /unstable-set-user-color command only " + "works in Twitch channels."); + return ""; + } + if (ctx.words.size() < 2) + { + ctx.channel->addSystemMessage( + QString("Usage: %1 [color]").arg(ctx.words.at(0))); + return ""; + } + + auto userID = ctx.words.at(1); + + auto color = ctx.words.value(2); + + getApp()->getUserData()->setUserColor(userID, color); + + return ""; +} + +QString openUsercard(const CommandContext &ctx) +{ + auto channel = ctx.channel; + + if (channel == nullptr) + { + return ""; + } + + if (ctx.words.size() < 2) + { + channel->addSystemMessage("Usage: /usercard [channel] or " + "/usercard id: [channel]"); + return ""; + } + + QString userName = ctx.words[1]; + stripUserName(userName); + + if (ctx.words.size() > 2) + { + QString channelName = ctx.words[2]; + stripChannelName(channelName); + + ChannelPtr channelTemp = + getApp()->getTwitch()->getChannelOrEmpty(channelName); + + if (channelTemp->isEmpty()) + { + channel->addSystemMessage( + "A usercard can only be displayed for a channel that is " + "currently opened in Chatterino."); + return ""; + } + + channel = channelTemp; + } + + // try to link to current split if possible + Split *currentSplit = nullptr; + auto *currentPage = dynamic_cast(getApp() + ->getWindows() + ->getMainWindow() + .getNotebook() + .getSelectedPage()); + if (currentPage != nullptr) + { + currentSplit = currentPage->getSelectedSplit(); + } + + auto differentChannel = + currentSplit != nullptr && currentSplit->getChannel() != channel; + if (differentChannel || currentSplit == nullptr) + { + // not possible to use current split, try searching for one + const auto ¬ebook = + getApp()->getWindows()->getMainWindow().getNotebook(); + auto count = notebook.getPageCount(); + for (int i = 0; i < count; i++) + { + auto *page = notebook.getPageAt(i); + auto *container = dynamic_cast(page); + assert(container != nullptr); + for (auto *split : container->getSplits()) + { + if (split->getChannel() == channel) + { + currentSplit = split; + break; + } + } + } + + // This would have crashed either way. + assert(currentSplit != nullptr && + "something went HORRIBLY wrong with the /usercard " + "command. It couldn't find a split for a channel which " + "should be open."); + } + + auto *userPopup = + new UserInfoPopup(getSettings()->autoCloseUserPopup, currentSplit); + userPopup->setData(userName, channel); + userPopup->moveTo(QCursor::pos(), widgets::BoundsChecking::CursorPosition); + userPopup->show(); + return ""; +} + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/Misc.hpp b/src/controllers/commands/builtin/Misc.hpp new file mode 100644 index 000000000..7d1589266 --- /dev/null +++ b/src/controllers/commands/builtin/Misc.hpp @@ -0,0 +1,33 @@ +#pragma once + +class QString; + +namespace chatterino { + +struct CommandContext; + +} // namespace chatterino + +namespace chatterino::commands { + +QString follow(const CommandContext &ctx); +QString unfollow(const CommandContext &ctx); +QString uptime(const CommandContext &ctx); +QString user(const CommandContext &ctx); +QString requests(const CommandContext &ctx); +QString lowtrust(const CommandContext &ctx); +QString clip(const CommandContext &ctx); +QString marker(const CommandContext &ctx); +QString streamlink(const CommandContext &ctx); +QString popout(const CommandContext &ctx); +QString popup(const CommandContext &ctx); +QString clearmessages(const CommandContext &ctx); +QString openURL(const CommandContext &ctx); +QString sendRawMessage(const CommandContext &ctx); +QString injectFakeMessage(const CommandContext &ctx); +QString injectStreamUpdateNoStream(const CommandContext &ctx); +QString copyToClipboard(const CommandContext &ctx); +QString unstableSetUserClientSideColor(const CommandContext &ctx); +QString openUsercard(const CommandContext &ctx); + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/chatterino/Debugging.cpp b/src/controllers/commands/builtin/chatterino/Debugging.cpp new file mode 100644 index 000000000..2984aa9e9 --- /dev/null +++ b/src/controllers/commands/builtin/chatterino/Debugging.cpp @@ -0,0 +1,147 @@ +#include "controllers/commands/builtin/chatterino/Debugging.hpp" + +#include "common/Channel.hpp" +#include "common/Env.hpp" +#include "common/Literals.hpp" +#include "controllers/commands/CommandContext.hpp" +#include "messages/Image.hpp" +#include "messages/MessageBuilder.hpp" +#include "messages/MessageElement.hpp" +#include "singletons/Theme.hpp" +#include "util/PostToThread.hpp" + +#include +#include +#include + +namespace chatterino::commands { + +using namespace literals; + +QString setLoggingRules(const CommandContext &ctx) +{ + if (ctx.words.size() < 2) + { + ctx.channel->addSystemMessage( + "Usage: /c2-set-logging-rules . To enable debug logging " + "for all categories from chatterino, use " + "'chatterino.*.debug=true'. For the format on the rules, see " + "https://doc.qt.io/qt-6/" + "qloggingcategory.html#configuring-categories"); + return {}; + } + + auto filterRules = ctx.words.mid(1).join('\n'); + + QLoggingCategory::setFilterRules(filterRules); + + auto message = + QStringLiteral("Updated filter rules to '%1'.").arg(filterRules); + + if (!qgetenv("QT_LOGGING_RULES").isEmpty()) + { + message += QStringLiteral( + " Warning: Logging rules were previously set by the " + "QT_LOGGING_RULES environment variable. This might cause " + "interference - see: " + "https://doc.qt.io/qt-6/qloggingcategory.html#setFilterRules"); + } + + ctx.channel->addSystemMessage(message); + return {}; +} + +QString toggleThemeReload(const CommandContext &ctx) +{ + if (getTheme()->isAutoReloading()) + { + getTheme()->setAutoReload(false); + ctx.channel->addSystemMessage(u"Disabled theme auto reloading."_s); + return {}; + } + + getTheme()->setAutoReload(true); + ctx.channel->addSystemMessage(u"Auto reloading theme every %1 ms."_s.arg( + Theme::AUTO_RELOAD_INTERVAL_MS)); + return {}; +} + +QString listEnvironmentVariables(const CommandContext &ctx) +{ + const auto &channel = ctx.channel; + if (channel == nullptr) + { + return ""; + } + + auto env = Env::get(); + + QStringList debugMessages{ + "recentMessagesApiUrl: " + env.recentMessagesApiUrl, + "linkResolverUrl: " + env.linkResolverUrl, + "twitchServerHost: " + env.twitchServerHost, + "twitchServerPort: " + QString::number(env.twitchServerPort), + "twitchServerSecure: " + QString::number(env.twitchServerSecure), + }; + + for (QString &str : debugMessages) + { + MessageBuilder builder; + builder.emplace(QTime::currentTime()); + builder.emplace(str, MessageElementFlag::Text, + MessageColor::System); + channel->addMessage(builder.release(), MessageContext::Original); + } + return ""; +} + +QString listArgs(const CommandContext &ctx) +{ + const auto &channel = ctx.channel; + if (channel == nullptr) + { + return ""; + } + + QString msg = QApplication::instance()->arguments().join(' '); + + channel->addSystemMessage(msg); + + return ""; +} + +QString forceImageGarbageCollection(const CommandContext &ctx) +{ + (void)ctx; + + runInGuiThread([] { + auto &iep = ImageExpirationPool::instance(); + iep.freeOld(); + }); + return ""; +} + +QString forceImageUnload(const CommandContext &ctx) +{ + (void)ctx; + + runInGuiThread([] { + auto &iep = ImageExpirationPool::instance(); + iep.freeAll(); + }); + return ""; +} + +QString debugTest(const CommandContext &ctx) +{ + if (!ctx.channel) + { + return ""; + } + + ctx.channel->addSystemMessage("debug-test called"); + + return ""; +} + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/chatterino/Debugging.hpp b/src/controllers/commands/builtin/chatterino/Debugging.hpp new file mode 100644 index 000000000..0cfa448ec --- /dev/null +++ b/src/controllers/commands/builtin/chatterino/Debugging.hpp @@ -0,0 +1,27 @@ +#pragma once + +class QString; + +namespace chatterino { + +struct CommandContext; + +} // namespace chatterino + +namespace chatterino::commands { + +QString setLoggingRules(const CommandContext &ctx); + +QString toggleThemeReload(const CommandContext &ctx); + +QString listEnvironmentVariables(const CommandContext &ctx); + +QString listArgs(const CommandContext &ctx); + +QString forceImageGarbageCollection(const CommandContext &ctx); + +QString forceImageUnload(const CommandContext &ctx); + +QString debugTest(const CommandContext &ctx); + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/AddModerator.cpp b/src/controllers/commands/builtin/twitch/AddModerator.cpp new file mode 100644 index 000000000..18d4a76aa --- /dev/null +++ b/src/controllers/commands/builtin/twitch/AddModerator.cpp @@ -0,0 +1,127 @@ +#include "controllers/commands/builtin/twitch/AddModerator.hpp" + +#include "Application.hpp" +#include "controllers/accounts/AccountController.hpp" +#include "controllers/commands/CommandContext.hpp" +#include "providers/twitch/api/Helix.hpp" +#include "providers/twitch/TwitchAccount.hpp" +#include "providers/twitch/TwitchChannel.hpp" +#include "util/Twitch.hpp" + +namespace chatterino::commands { + +QString addModerator(const CommandContext &ctx) +{ + if (ctx.channel == nullptr) + { + return ""; + } + + if (ctx.twitchChannel == nullptr) + { + ctx.channel->addSystemMessage( + "The /mod command only works in Twitch channels."); + return ""; + } + if (ctx.words.size() < 2) + { + ctx.channel->addSystemMessage( + "Usage: \"/mod \" - Grant moderator status to a " + "user. Use \"/mods\" to list the moderators of this channel."); + return ""; + } + + auto currentUser = getApp()->getAccounts()->twitch.getCurrent(); + if (currentUser->isAnon()) + { + ctx.channel->addSystemMessage("You must be logged in to mod someone!"); + return ""; + } + + auto target = ctx.words.at(1); + stripChannelName(target); + + getHelix()->getUserByName( + target, + [twitchChannel{ctx.twitchChannel}, + channel{ctx.channel}](const HelixUser &targetUser) { + getHelix()->addChannelModerator( + twitchChannel->roomId(), targetUser.id, + [channel, targetUser] { + channel->addSystemMessage( + QString("You have added %1 as a moderator of this " + "channel.") + .arg(targetUser.displayName)); + }, + [channel, targetUser](auto error, auto message) { + QString errorMessage = + QString("Failed to add channel moderator - "); + + using Error = HelixAddChannelModeratorError; + + switch (error) + { + case Error::UserMissingScope: { + // TODO(pajlada): Phrase MISSING_REQUIRED_SCOPE + errorMessage += "Missing required scope. " + "Re-login with your " + "account and try again."; + } + break; + + case Error::UserNotAuthorized: { + // TODO(pajlada): Phrase MISSING_PERMISSION + errorMessage += "You don't have permission to " + "perform that action."; + } + break; + + case Error::Ratelimited: { + errorMessage += + "You are being ratelimited by Twitch. Try " + "again in a few seconds."; + } + break; + + case Error::TargetIsVIP: { + errorMessage += + QString("%1 is currently a VIP, \"/unvip\" " + "them and " + "retry this command.") + .arg(targetUser.displayName); + } + break; + + case Error::TargetAlreadyModded: { + // Equivalent irc error + errorMessage = + QString("%1 is already a moderator of this " + "channel.") + .arg(targetUser.displayName); + } + break; + + case Error::Forwarded: { + errorMessage += message; + } + break; + + case Error::Unknown: + default: { + errorMessage += "An unknown error has occurred."; + } + break; + } + channel->addSystemMessage(errorMessage); + }); + }, + [channel{ctx.channel}, target] { + // Equivalent error from IRC + channel->addSystemMessage( + QString("Invalid username: %1").arg(target)); + }); + + return ""; +} + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/AddModerator.hpp b/src/controllers/commands/builtin/twitch/AddModerator.hpp new file mode 100644 index 000000000..722ad724b --- /dev/null +++ b/src/controllers/commands/builtin/twitch/AddModerator.hpp @@ -0,0 +1,16 @@ +#pragma once + +class QString; + +namespace chatterino { + +struct CommandContext; + +} // namespace chatterino + +namespace chatterino::commands { + +/// /mod +QString addModerator(const CommandContext &ctx); + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/AddVIP.cpp b/src/controllers/commands/builtin/twitch/AddVIP.cpp new file mode 100644 index 000000000..0c36f98b0 --- /dev/null +++ b/src/controllers/commands/builtin/twitch/AddVIP.cpp @@ -0,0 +1,108 @@ +#include "controllers/commands/builtin/twitch/AddVIP.hpp" + +#include "Application.hpp" +#include "controllers/accounts/AccountController.hpp" +#include "controllers/commands/CommandContext.hpp" +#include "providers/twitch/api/Helix.hpp" +#include "providers/twitch/TwitchAccount.hpp" +#include "providers/twitch/TwitchChannel.hpp" +#include "util/Twitch.hpp" + +namespace chatterino::commands { + +QString addVIP(const CommandContext &ctx) +{ + if (ctx.channel == nullptr) + { + return ""; + } + + if (ctx.twitchChannel == nullptr) + { + ctx.channel->addSystemMessage( + "The /vip command only works in Twitch channels."); + return ""; + } + if (ctx.words.size() < 2) + { + ctx.channel->addSystemMessage( + "Usage: \"/vip \" - Grant VIP status to a user. Use " + "\"/vips\" to list the VIPs of this channel."); + return ""; + } + + auto currentUser = getApp()->getAccounts()->twitch.getCurrent(); + if (currentUser->isAnon()) + { + ctx.channel->addSystemMessage("You must be logged in to VIP someone!"); + return ""; + } + + auto target = ctx.words.at(1); + stripChannelName(target); + + getHelix()->getUserByName( + target, + [twitchChannel{ctx.twitchChannel}, + channel{ctx.channel}](const HelixUser &targetUser) { + getHelix()->addChannelVIP( + twitchChannel->roomId(), targetUser.id, + [channel, targetUser] { + channel->addSystemMessage( + QString("You have added %1 as a VIP of this channel.") + .arg(targetUser.displayName)); + }, + [channel, targetUser](auto error, auto message) { + QString errorMessage = QString("Failed to add VIP - "); + + using Error = HelixAddChannelVIPError; + + switch (error) + { + case Error::UserMissingScope: { + // TODO(pajlada): Phrase MISSING_REQUIRED_SCOPE + errorMessage += "Missing required scope. " + "Re-login with your " + "account and try again."; + } + break; + + case Error::UserNotAuthorized: { + // TODO(pajlada): Phrase MISSING_PERMISSION + errorMessage += "You don't have permission to " + "perform that action."; + } + break; + + case Error::Ratelimited: { + errorMessage += + "You are being ratelimited by Twitch. Try " + "again in a few seconds."; + } + break; + + case Error::Forwarded: { + // These are actually the IRC equivalents, so we can ditch the prefix + errorMessage = message; + } + break; + + case Error::Unknown: + default: { + errorMessage += "An unknown error has occurred."; + } + break; + } + channel->addSystemMessage(errorMessage); + }); + }, + [channel{ctx.channel}, target] { + // Equivalent error from IRC + channel->addSystemMessage( + QString("Invalid username: %1").arg(target)); + }); + + return ""; +} + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/AddVIP.hpp b/src/controllers/commands/builtin/twitch/AddVIP.hpp new file mode 100644 index 000000000..3d956cc42 --- /dev/null +++ b/src/controllers/commands/builtin/twitch/AddVIP.hpp @@ -0,0 +1,16 @@ +#pragma once + +class QString; + +namespace chatterino { + +struct CommandContext; + +} // namespace chatterino + +namespace chatterino::commands { + +/// /vip +QString addVIP(const CommandContext &ctx); + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/Announce.cpp b/src/controllers/commands/builtin/twitch/Announce.cpp new file mode 100644 index 000000000..250b239f4 --- /dev/null +++ b/src/controllers/commands/builtin/twitch/Announce.cpp @@ -0,0 +1,128 @@ +#include "controllers/commands/builtin/twitch/Announce.hpp" + +#include "Application.hpp" +#include "controllers/accounts/AccountController.hpp" +#include "controllers/commands/CommandContext.hpp" +#include "providers/twitch/api/Helix.hpp" +#include "providers/twitch/TwitchAccount.hpp" +#include "providers/twitch/TwitchChannel.hpp" + +namespace { +using namespace chatterino; + +QString sendAnnouncementColor(const CommandContext &ctx, + const HelixAnnouncementColor color) +{ + if (ctx.channel == nullptr) + { + return ""; + } + + if (ctx.twitchChannel == nullptr) + { + ctx.channel->addSystemMessage( + "This command can only be used in Twitch channels."); + return ""; + } + + QString colorStr = ""; + if (color != HelixAnnouncementColor::Primary) + { + colorStr = qmagicenum::enumNameString(color).toLower(); + } + + if (ctx.words.size() < 2) + { + QString usageMsg; + if (color == HelixAnnouncementColor::Primary) + { + usageMsg = "Usage: /announce - Call attention to your " + "message with a highlight."; + } + else + { + usageMsg = + QString("Usage: /announce%1 - Call attention to your " + "message with a %1 highlight.") + .arg(colorStr); + } + ctx.channel->addSystemMessage(usageMsg); + return ""; + } + + auto user = getApp()->getAccounts()->twitch.getCurrent(); + if (user->isAnon()) + { + ctx.channel->addSystemMessage( + QString("You must be logged in to use the /announce%1 command.") + .arg(colorStr)); + return ""; + } + + getHelix()->sendChatAnnouncement( + ctx.twitchChannel->roomId(), user->getUserId(), + ctx.words.mid(1).join(" "), color, + []() { + // do nothing. + }, + [channel{ctx.channel}](auto error, auto message) { + using Error = HelixSendChatAnnouncementError; + QString errorMessage = QString("Failed to send announcement - "); + + switch (error) + { + case Error::UserMissingScope: { + // TODO(pajlada): Phrase MISSING_REQUIRED_SCOPE + errorMessage += + "Missing required scope. Re-login with your " + "account and try again."; + } + break; + + case Error::Forwarded: { + errorMessage += message; + } + break; + + case Error::Unknown: + default: { + errorMessage += "An unknown error has occurred."; + } + break; + } + + channel->addSystemMessage(errorMessage); + }); + return ""; +} + +} // namespace + +namespace chatterino::commands { + +QString sendAnnouncement(const CommandContext &ctx) +{ + return sendAnnouncementColor(ctx, HelixAnnouncementColor::Primary); +} + +QString sendAnnouncementBlue(const CommandContext &ctx) +{ + return sendAnnouncementColor(ctx, HelixAnnouncementColor::Blue); +} + +QString sendAnnouncementGreen(const CommandContext &ctx) +{ + return sendAnnouncementColor(ctx, HelixAnnouncementColor::Green); +} + +QString sendAnnouncementOrange(const CommandContext &ctx) +{ + return sendAnnouncementColor(ctx, HelixAnnouncementColor::Orange); +} + +QString sendAnnouncementPurple(const CommandContext &ctx) +{ + return sendAnnouncementColor(ctx, HelixAnnouncementColor::Purple); +} + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/Announce.hpp b/src/controllers/commands/builtin/twitch/Announce.hpp new file mode 100644 index 000000000..898ea0e32 --- /dev/null +++ b/src/controllers/commands/builtin/twitch/Announce.hpp @@ -0,0 +1,28 @@ +#pragma once + +class QString; + +namespace chatterino { + +struct CommandContext; + +} // namespace chatterino + +namespace chatterino::commands { + +/// /announce +QString sendAnnouncement(const CommandContext &ctx); + +/// /announceblue +QString sendAnnouncementBlue(const CommandContext &ctx); + +/// /announcegreen +QString sendAnnouncementGreen(const CommandContext &ctx); + +/// /announceorange +QString sendAnnouncementOrange(const CommandContext &ctx); + +/// /announcepurple +QString sendAnnouncementPurple(const CommandContext &ctx); + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/Ban.cpp b/src/controllers/commands/builtin/twitch/Ban.cpp new file mode 100644 index 000000000..819231524 --- /dev/null +++ b/src/controllers/commands/builtin/twitch/Ban.cpp @@ -0,0 +1,381 @@ +#include "controllers/commands/builtin/twitch/Ban.hpp" + +#include "Application.hpp" +#include "common/QLogging.hpp" +#include "controllers/accounts/AccountController.hpp" +#include "controllers/commands/CommandContext.hpp" +#include "controllers/commands/common/ChannelAction.hpp" +#include "providers/twitch/api/Helix.hpp" +#include "providers/twitch/TwitchAccount.hpp" +#include "providers/twitch/TwitchChannel.hpp" + +namespace { + +using namespace chatterino; + +QString formatBanTimeoutError(const char *operation, HelixBanUserError error, + const QString &message, const QString &userTarget) +{ + using Error = HelixBanUserError; + + QString errorMessage = QString("Failed to %1 user - ").arg(operation); + + switch (error) + { + case Error::ConflictingOperation: { + errorMessage += "There was a conflicting ban operation on " + "this user. Please try again."; + } + break; + + case Error::Forwarded: { + errorMessage += message; + } + break; + + case Error::Ratelimited: { + errorMessage += "You are being ratelimited by Twitch. Try " + "again in a few seconds."; + } + break; + + case Error::TargetBanned: { + // Equivalent IRC error + errorMessage += QString("%1 is already banned in this channel.") + .arg(userTarget); + } + break; + + case Error::CannotBanUser: { + // We can't provide the identical error as in IRC, + // because we don't have enough information about the user. + // The messages from IRC are formatted like this: + // "You cannot {op} moderator {mod} unless you are the owner of this channel." + // "You cannot {op} the broadcaster." + errorMessage += + QString("You cannot %1 %2.").arg(operation, userTarget); + } + break; + + case Error::UserMissingScope: { + // TODO(pajlada): Phrase MISSING_REQUIRED_SCOPE + errorMessage += "Missing required scope. " + "Re-login with your " + "account and try again."; + } + break; + + case Error::UserNotAuthorized: { + // TODO(pajlada): Phrase MISSING_PERMISSION + errorMessage += "You don't have permission to " + "perform that action."; + } + break; + + case Error::Unknown: { + errorMessage += "An unknown error has occurred."; + } + break; + } + return errorMessage; +} + +void banUserByID(const ChannelPtr &channel, const QString &channelID, + const QString &sourceUserID, const QString &targetUserID, + const QString &reason, const QString &displayName) +{ + getHelix()->banUser( + channelID, sourceUserID, targetUserID, std::nullopt, reason, + [] { + // No response for bans, they're emitted over pubsub/IRC instead + }, + [channel, displayName](auto error, auto message) { + auto errorMessage = + formatBanTimeoutError("ban", error, message, displayName); + channel->addSystemMessage(errorMessage); + }); +} + +void timeoutUserByID(const ChannelPtr &channel, const QString &channelID, + const QString &sourceUserID, const QString &targetUserID, + int duration, const QString &reason, + const QString &displayName) +{ + getHelix()->banUser( + channelID, sourceUserID, targetUserID, duration, reason, + [] { + // No response for timeouts, they're emitted over pubsub/IRC instead + }, + [channel, displayName](auto error, auto message) { + auto errorMessage = + formatBanTimeoutError("timeout", error, message, displayName); + channel->addSystemMessage(errorMessage); + }); +} + +} // namespace + +namespace chatterino::commands { + +QString sendBan(const CommandContext &ctx) +{ + const auto command = QStringLiteral("/ban"); + const auto usage = QStringLiteral( + R"(Usage: "/ban [options...] [reason]" - Permanently prevent a user from chatting via their username. Reason is optional and will be shown to the target user and other moderators. Options: --channel to override which channel the ban takes place in (can be specified multiple times).)"); + const auto actions = parseChannelAction(ctx, command, usage, false, true); + + if (!actions.has_value()) + { + if (ctx.channel != nullptr) + { + ctx.channel->addSystemMessage(actions.error()); + } + else + { + qCWarning(chatterinoCommands) + << "Error parsing command:" << actions.error(); + } + + return ""; + } + + assert(!actions.value().empty()); + + auto currentUser = getApp()->getAccounts()->twitch.getCurrent(); + if (currentUser->isAnon()) + { + ctx.channel->addSystemMessage("You must be logged in to ban someone!"); + return ""; + } + + for (const auto &action : actions.value()) + { + const auto &reason = action.reason; + + QStringList userLoginsToFetch; + QStringList userIDs; + if (action.target.id.isEmpty()) + { + assert(!action.target.login.isEmpty() && + "Ban Action target username AND user ID may not be " + "empty at the same time"); + userLoginsToFetch.append(action.target.login); + } + else + { + // For hydration + userIDs.append(action.target.id); + } + if (action.channel.id.isEmpty()) + { + assert(!action.channel.login.isEmpty() && + "Ban Action channel username AND user ID may not be " + "empty at the same time"); + userLoginsToFetch.append(action.channel.login); + } + else + { + // For hydration + userIDs.append(action.channel.id); + } + + if (!userLoginsToFetch.isEmpty()) + { + // At least 1 user ID needs to be resolved before we can take action + // userIDs is filled up with the data we already have to hydrate the action channel & action target + getHelix()->fetchUsers( + userIDs, userLoginsToFetch, + [channel{ctx.channel}, actionChannel{action.channel}, + actionTarget{action.target}, currentUser, reason, + userLoginsToFetch](const auto &users) mutable { + if (!actionChannel.hydrateFrom(users)) + { + channel->addSystemMessage( + QString("Failed to ban, bad channel name: %1") + .arg(actionChannel.login)); + return; + } + if (!actionTarget.hydrateFrom(users)) + { + channel->addSystemMessage( + QString("Failed to ban, bad target name: %1") + .arg(actionTarget.login)); + return; + } + + banUserByID(channel, actionChannel.id, + currentUser->getUserId(), actionTarget.id, + reason, actionTarget.displayName); + }, + [channel{ctx.channel}, userLoginsToFetch] { + channel->addSystemMessage( + QString("Failed to ban, bad username(s): %1") + .arg(userLoginsToFetch.join(", "))); + }); + } + else + { + // If both IDs are available, we do no hydration & just use the id as the display name + banUserByID(ctx.channel, action.channel.id, + currentUser->getUserId(), action.target.id, reason, + action.target.id); + } + } + + return ""; +} + +QString sendBanById(const CommandContext &ctx) +{ + const auto &words = ctx.words; + const auto &channel = ctx.channel; + const auto *twitchChannel = ctx.twitchChannel; + + if (channel == nullptr) + { + return ""; + } + if (twitchChannel == nullptr) + { + channel->addSystemMessage( + "The /banid command only works in Twitch channels."); + return ""; + } + + const auto *usageStr = + "Usage: \"/banid [reason]\" - Permanently prevent a user " + "from chatting via their user ID. Reason is optional and will be " + "shown to the target user and other moderators."; + if (words.size() < 2) + { + channel->addSystemMessage(usageStr); + return ""; + } + + auto currentUser = getApp()->getAccounts()->twitch.getCurrent(); + if (currentUser->isAnon()) + { + channel->addSystemMessage("You must be logged in to ban someone!"); + return ""; + } + + auto target = words.at(1); + auto reason = words.mid(2).join(' '); + + banUserByID(channel, twitchChannel->roomId(), currentUser->getUserId(), + target, reason, target); + + return ""; +} + +QString sendTimeout(const CommandContext &ctx) +{ + const auto command = QStringLiteral("/timeout"); + const auto usage = QStringLiteral( + R"(Usage: "/timeout [options...] [duration][time unit] [reason]" - Temporarily prevent a user from chatting. Duration (optional, default=10 minutes) must be a positive integer; time unit (optional, default=s) must be one of s, m, h, d, w; maximum duration is 2 weeks. Combinations like 1d2h are also allowed. Reason is optional and will be shown to the target user and other moderators. Use "/untimeout" to remove a timeout. Options: --channel to override which channel the timeout takes place in (can be specified multiple times).)"); + const auto actions = parseChannelAction(ctx, command, usage, true, true); + + if (!actions.has_value()) + { + if (ctx.channel != nullptr) + { + ctx.channel->addSystemMessage(actions.error()); + } + else + { + qCWarning(chatterinoCommands) + << "Error parsing command:" << actions.error(); + } + + return ""; + } + + assert(!actions.value().empty()); + + auto currentUser = getApp()->getAccounts()->twitch.getCurrent(); + if (currentUser->isAnon()) + { + ctx.channel->addSystemMessage( + "You must be logged in to timeout someone!"); + return ""; + } + + for (const auto &action : actions.value()) + { + const auto &reason = action.reason; + + QStringList userLoginsToFetch; + QStringList userIDs; + if (action.target.id.isEmpty()) + { + assert(!action.target.login.isEmpty() && + "Timeout Action target username AND user ID may not be " + "empty at the same time"); + userLoginsToFetch.append(action.target.login); + } + else + { + // For hydration + userIDs.append(action.target.id); + } + if (action.channel.id.isEmpty()) + { + assert(!action.channel.login.isEmpty() && + "Timeout Action channel username AND user ID may not be " + "empty at the same time"); + userLoginsToFetch.append(action.channel.login); + } + else + { + // For hydration + userIDs.append(action.channel.id); + } + + if (!userLoginsToFetch.isEmpty()) + { + // At least 1 user ID needs to be resolved before we can take action + // userIDs is filled up with the data we already have to hydrate the action channel & action target + getHelix()->fetchUsers( + userIDs, userLoginsToFetch, + [channel{ctx.channel}, duration{action.duration}, + actionChannel{action.channel}, actionTarget{action.target}, + currentUser, reason, + userLoginsToFetch](const auto &users) mutable { + if (!actionChannel.hydrateFrom(users)) + { + channel->addSystemMessage( + QString("Failed to timeout, bad channel name: %1") + .arg(actionChannel.login)); + return; + } + if (!actionTarget.hydrateFrom(users)) + { + channel->addSystemMessage( + QString("Failed to timeout, bad target name: %1") + .arg(actionTarget.login)); + return; + } + + timeoutUserByID(channel, actionChannel.id, + currentUser->getUserId(), actionTarget.id, + duration, reason, actionTarget.displayName); + }, + [channel{ctx.channel}, userLoginsToFetch] { + channel->addSystemMessage( + QString("Failed to timeout, bad username(s): %1") + .arg(userLoginsToFetch.join(", "))); + }); + } + else + { + // If both IDs are available, we do no hydration & just use the id as the display name + timeoutUserByID(ctx.channel, action.channel.id, + currentUser->getUserId(), action.target.id, + action.duration, reason, action.target.id); + } + } + + return ""; +} + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/Ban.hpp b/src/controllers/commands/builtin/twitch/Ban.hpp new file mode 100644 index 000000000..9ba724910 --- /dev/null +++ b/src/controllers/commands/builtin/twitch/Ban.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include + +namespace chatterino { + +struct CommandContext; + +} // namespace chatterino + +namespace chatterino::commands { + +/// /ban +QString sendBan(const CommandContext &ctx); +/// /banid +QString sendBanById(const CommandContext &ctx); + +/// /timeout +QString sendTimeout(const CommandContext &ctx); + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/Block.cpp b/src/controllers/commands/builtin/twitch/Block.cpp new file mode 100644 index 000000000..7e0423d44 --- /dev/null +++ b/src/controllers/commands/builtin/twitch/Block.cpp @@ -0,0 +1,163 @@ +#include "controllers/commands/builtin/twitch/Block.hpp" + +#include "Application.hpp" +#include "common/Channel.hpp" +#include "controllers/accounts/AccountController.hpp" +#include "controllers/commands/CommandContext.hpp" +#include "providers/twitch/api/Helix.hpp" +#include "providers/twitch/TwitchAccount.hpp" +#include "util/Twitch.hpp" + +namespace { + +using namespace chatterino; + +} // namespace + +namespace chatterino::commands { + +QString blockUser(const CommandContext &ctx) +{ + if (ctx.channel == nullptr) + { + return ""; + } + + if (ctx.twitchChannel == nullptr) + { + ctx.channel->addSystemMessage( + "The /block command only works in Twitch channels."); + return ""; + } + + if (ctx.words.size() < 2) + { + ctx.channel->addSystemMessage("Usage: /block "); + return ""; + } + + auto currentUser = getApp()->getAccounts()->twitch.getCurrent(); + + if (currentUser->isAnon()) + { + ctx.channel->addSystemMessage( + "You must be logged in to block someone!"); + return ""; + } + + auto target = ctx.words.at(1); + stripChannelName(target); + + getHelix()->getUserByName( + target, + [currentUser, channel{ctx.channel}, + target](const HelixUser &targetUser) { + getApp()->getAccounts()->twitch.getCurrent()->blockUser( + targetUser.id, nullptr, + [channel, target, targetUser] { + channel->addSystemMessage( + QString("You successfully blocked user %1") + .arg(target)); + }, + [channel, target] { + channel->addSystemMessage( + QString("User %1 couldn't be blocked, an unknown " + "error occurred!") + .arg(target)); + }); + }, + [channel{ctx.channel}, target] { + channel->addSystemMessage(QString("User %1 couldn't be blocked, no " + "user with that name found!") + .arg(target)); + }); + + return ""; +} + +QString ignoreUser(const CommandContext &ctx) +{ + if (ctx.channel == nullptr) + { + return ""; + } + + ctx.channel->addSystemMessage( + "Ignore command has been renamed to /block, please use it from " + "now on as /ignore is going to be removed soon."); + + return blockUser(ctx); +} + +QString unblockUser(const CommandContext &ctx) +{ + if (ctx.channel == nullptr) + { + return ""; + } + + if (ctx.twitchChannel == nullptr) + { + ctx.channel->addSystemMessage( + "The /unblock command only works in Twitch channels."); + return ""; + } + + if (ctx.words.size() < 2) + { + ctx.channel->addSystemMessage("Usage: /unblock "); + return ""; + } + + auto currentUser = getApp()->getAccounts()->twitch.getCurrent(); + + if (currentUser->isAnon()) + { + ctx.channel->addSystemMessage( + "You must be logged in to unblock someone!"); + return ""; + } + + auto target = ctx.words.at(1); + stripChannelName(target); + + getHelix()->getUserByName( + target, + [currentUser, channel{ctx.channel}, target](const auto &targetUser) { + getApp()->getAccounts()->twitch.getCurrent()->unblockUser( + targetUser.id, nullptr, + [channel, target, targetUser] { + channel->addSystemMessage( + QString("You successfully unblocked user %1") + .arg(target)); + }, + [channel, target] { + channel->addSystemMessage( + QString("User %1 couldn't be unblocked, an unknown " + "error occurred!") + .arg(target)); + }); + }, + [channel{ctx.channel}, target] { + channel->addSystemMessage(QString("User %1 couldn't be unblocked, " + "no user with that name found!") + .arg(target)); + }); + + return ""; +} + +QString unignoreUser(const CommandContext &ctx) +{ + if (ctx.channel == nullptr) + { + return ""; + } + + ctx.channel->addSystemMessage( + "Unignore command has been renamed to /unblock, please use it " + "from now on as /unignore is going to be removed soon."); + return unblockUser(ctx); +} + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/Block.hpp b/src/controllers/commands/builtin/twitch/Block.hpp new file mode 100644 index 000000000..75ea3d0d4 --- /dev/null +++ b/src/controllers/commands/builtin/twitch/Block.hpp @@ -0,0 +1,25 @@ +#pragma once + +class QString; + +namespace chatterino { + +struct CommandContext; + +} // namespace chatterino + +namespace chatterino::commands { + +/// /block +QString blockUser(const CommandContext &ctx); + +/// /ignore +QString ignoreUser(const CommandContext &ctx); + +/// /unblock +QString unblockUser(const CommandContext &ctx); + +/// /unignore +QString unignoreUser(const CommandContext &ctx); + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/ChatSettings.cpp b/src/controllers/commands/builtin/twitch/ChatSettings.cpp new file mode 100644 index 000000000..cc0f311a9 --- /dev/null +++ b/src/controllers/commands/builtin/twitch/ChatSettings.cpp @@ -0,0 +1,432 @@ +#include "controllers/commands/builtin/twitch/ChatSettings.hpp" + +#include "Application.hpp" +#include "controllers/accounts/AccountController.hpp" +#include "controllers/commands/CommandContext.hpp" +#include "providers/twitch/api/Helix.hpp" +#include "providers/twitch/TwitchAccount.hpp" +#include "providers/twitch/TwitchChannel.hpp" +#include "util/FormatTime.hpp" +#include "util/Helpers.hpp" + +namespace { + +using namespace chatterino; + +QString formatError(const HelixUpdateChatSettingsError error, + const QString &message, int durationUnitMultiplier = 1) +{ + static const QRegularExpression invalidRange("(\\d+) through (\\d+)"); + + QString errorMessage = QString("Failed to update - "); + using Error = HelixUpdateChatSettingsError; + switch (error) + { + case Error::UserMissingScope: { + // TODO(pajlada): Phrase MISSING_REQUIRED_SCOPE + errorMessage += "Missing required scope. " + "Re-login with your " + "account and try again."; + } + break; + + case Error::UserNotAuthorized: + case Error::Forbidden: { + // TODO(pajlada): Phrase MISSING_PERMISSION + errorMessage += "You don't have permission to " + "perform that action."; + } + break; + + case Error::Ratelimited: { + errorMessage += "You are being ratelimited by Twitch. Try " + "again in a few seconds."; + } + break; + + case Error::OutOfRange: { + QRegularExpressionMatch matched = invalidRange.match(message); + if (matched.hasMatch()) + { + auto from = matched.captured(1).toInt(); + auto to = matched.captured(2).toInt(); + errorMessage += + QString("The duration is out of the valid range: " + "%1 through %2.") + .arg(from == 0 + ? "0s" + : formatTime(from * durationUnitMultiplier), + to == 0 ? "0s" + : formatTime(to * durationUnitMultiplier)); + } + else + { + errorMessage += message; + } + } + break; + + case Error::Forwarded: { + errorMessage = message; + } + break; + + case Error::Unknown: + default: { + errorMessage += "An unknown error has occurred."; + } + break; + } + return errorMessage; +} + +// Do nothing as we'll receive a message from IRC about updates +auto successCallback = [](auto result) {}; + +auto failureCallback = [](ChannelPtr channel, int durationUnitMultiplier = 1) { + return [channel, durationUnitMultiplier](const auto &error, + const QString &message) { + channel->addSystemMessage( + formatError(error, message, durationUnitMultiplier)); + }; +}; + +const auto P_NOT_LOGGED_IN = + QStringLiteral("You must be logged in to update chat settings!"); + +} // namespace + +namespace chatterino::commands { + +QString emoteOnly(const CommandContext &ctx) +{ + auto currentUser = getApp()->getAccounts()->twitch.getCurrent(); + if (currentUser->isAnon()) + { + ctx.channel->addSystemMessage(P_NOT_LOGGED_IN); + return ""; + } + + if (ctx.twitchChannel == nullptr) + { + ctx.channel->addSystemMessage( + "The /emoteonly command only works in Twitch channels."); + return ""; + } + + if (ctx.twitchChannel->accessRoomModes()->emoteOnly) + { + ctx.channel->addSystemMessage( + "This room is already in emote-only mode."); + return ""; + } + + getHelix()->updateEmoteMode(ctx.twitchChannel->roomId(), + currentUser->getUserId(), true, successCallback, + failureCallback(ctx.channel)); + + return ""; +} + +QString emoteOnlyOff(const CommandContext &ctx) +{ + auto currentUser = getApp()->getAccounts()->twitch.getCurrent(); + if (currentUser->isAnon()) + { + ctx.channel->addSystemMessage(P_NOT_LOGGED_IN); + return ""; + } + if (ctx.twitchChannel == nullptr) + { + ctx.channel->addSystemMessage( + "The /emoteonlyoff command only works in Twitch channels."); + return ""; + } + + if (!ctx.twitchChannel->accessRoomModes()->emoteOnly) + { + ctx.channel->addSystemMessage("This room is not in emote-only mode."); + return ""; + } + + getHelix()->updateEmoteMode(ctx.twitchChannel->roomId(), + currentUser->getUserId(), false, + successCallback, failureCallback(ctx.channel)); + + return ""; +} + +QString subscribers(const CommandContext &ctx) +{ + auto currentUser = getApp()->getAccounts()->twitch.getCurrent(); + if (currentUser->isAnon()) + { + ctx.channel->addSystemMessage(P_NOT_LOGGED_IN); + return ""; + } + + if (ctx.twitchChannel == nullptr) + { + ctx.channel->addSystemMessage( + "The /subscribers command only works in Twitch channels."); + return ""; + } + + if (ctx.twitchChannel->accessRoomModes()->submode) + { + ctx.channel->addSystemMessage( + "This room is already in subscribers-only mode."); + return ""; + } + + getHelix()->updateSubscriberMode( + ctx.twitchChannel->roomId(), currentUser->getUserId(), true, + successCallback, failureCallback(ctx.channel)); + + return ""; +} + +QString subscribersOff(const CommandContext &ctx) +{ + auto currentUser = getApp()->getAccounts()->twitch.getCurrent(); + if (currentUser->isAnon()) + { + ctx.channel->addSystemMessage(P_NOT_LOGGED_IN); + return ""; + } + + if (ctx.twitchChannel == nullptr) + { + ctx.channel->addSystemMessage( + "The /subscribersoff command only works in Twitch channels."); + return ""; + } + + if (!ctx.twitchChannel->accessRoomModes()->submode) + { + ctx.channel->addSystemMessage( + "This room is not in subscribers-only mode."); + return ""; + } + + getHelix()->updateSubscriberMode( + ctx.twitchChannel->roomId(), currentUser->getUserId(), false, + successCallback, failureCallback(ctx.channel)); + + return ""; +} + +QString slow(const CommandContext &ctx) +{ + auto currentUser = getApp()->getAccounts()->twitch.getCurrent(); + if (currentUser->isAnon()) + { + ctx.channel->addSystemMessage(P_NOT_LOGGED_IN); + return ""; + } + + if (ctx.twitchChannel == nullptr) + { + ctx.channel->addSystemMessage( + "The /slow command only works in Twitch channels."); + return ""; + } + + int duration = 30; + if (ctx.words.length() >= 2) + { + bool ok = false; + duration = ctx.words.at(1).toInt(&ok); + if (!ok || duration <= 0) + { + ctx.channel->addSystemMessage( + "Usage: \"/slow [duration]\" - Enables slow mode (limit how " + "often users may send messages). Duration (optional, " + "default=30) must be a positive number of seconds. Use " + "\"slowoff\" to disable."); + return ""; + } + } + + if (ctx.twitchChannel->accessRoomModes()->slowMode == duration) + { + ctx.channel->addSystemMessage( + QString("This room is already in %1-second slow mode.") + .arg(duration)); + return ""; + } + + getHelix()->updateSlowMode(ctx.twitchChannel->roomId(), + currentUser->getUserId(), duration, + successCallback, failureCallback(ctx.channel)); + + return ""; +} + +QString slowOff(const CommandContext &ctx) +{ + auto currentUser = getApp()->getAccounts()->twitch.getCurrent(); + if (currentUser->isAnon()) + { + ctx.channel->addSystemMessage(P_NOT_LOGGED_IN); + return ""; + } + + if (ctx.twitchChannel == nullptr) + { + ctx.channel->addSystemMessage( + "The /slowoff command only works in Twitch channels."); + return ""; + } + + if (ctx.twitchChannel->accessRoomModes()->slowMode <= 0) + { + ctx.channel->addSystemMessage("This room is not in slow mode."); + return ""; + } + + getHelix()->updateSlowMode(ctx.twitchChannel->roomId(), + currentUser->getUserId(), std::nullopt, + successCallback, failureCallback(ctx.channel)); + + return ""; +} + +QString followers(const CommandContext &ctx) +{ + auto currentUser = getApp()->getAccounts()->twitch.getCurrent(); + if (currentUser->isAnon()) + { + ctx.channel->addSystemMessage(P_NOT_LOGGED_IN); + return ""; + } + + if (ctx.twitchChannel == nullptr) + { + ctx.channel->addSystemMessage( + "The /followers command only works in Twitch channels."); + return ""; + } + + int duration = 0; + if (ctx.words.length() >= 2) + { + auto parsed = parseDurationToSeconds(ctx.words.mid(1).join(' '), 60); + duration = (int)(parsed / 60); + // -1 / 60 == 0 => use parsed + if (parsed < 0) + { + ctx.channel->addSystemMessage( + "Usage: \"/followers [duration]\" - Enables followers-only " + "mode (only users who have followed for 'duration' may chat). " + "Examples: \"30m\", \"1 week\", \"5 days 12 hours\". Must be " + "less than 3 months."); + return ""; + } + } + + if (ctx.twitchChannel->accessRoomModes()->followerOnly == duration) + { + ctx.channel->addSystemMessage( + QString("This room is already in %1 followers-only mode.") + .arg(formatTime(duration * 60))); + return ""; + } + + getHelix()->updateFollowerMode( + ctx.twitchChannel->roomId(), currentUser->getUserId(), duration, + successCallback, failureCallback(ctx.channel, 60)); + + return ""; +} + +QString followersOff(const CommandContext &ctx) +{ + auto currentUser = getApp()->getAccounts()->twitch.getCurrent(); + if (currentUser->isAnon()) + { + ctx.channel->addSystemMessage(P_NOT_LOGGED_IN); + return ""; + } + + if (ctx.twitchChannel == nullptr) + { + ctx.channel->addSystemMessage( + "The /followersoff command only works in Twitch channels."); + return ""; + } + + if (ctx.twitchChannel->accessRoomModes()->followerOnly < 0) + { + ctx.channel->addSystemMessage( + "This room is not in followers-only mode. "); + return ""; + } + + getHelix()->updateFollowerMode( + ctx.twitchChannel->roomId(), currentUser->getUserId(), std::nullopt, + successCallback, failureCallback(ctx.channel)); + + return ""; +} + +QString uniqueChat(const CommandContext &ctx) +{ + auto currentUser = getApp()->getAccounts()->twitch.getCurrent(); + if (currentUser->isAnon()) + { + ctx.channel->addSystemMessage(P_NOT_LOGGED_IN); + return ""; + } + + if (ctx.twitchChannel == nullptr) + { + ctx.channel->addSystemMessage( + "The /uniquechat command only works in Twitch channels."); + return ""; + } + + if (ctx.twitchChannel->accessRoomModes()->r9k) + { + ctx.channel->addSystemMessage( + "This room is already in unique-chat mode."); + return ""; + } + + getHelix()->updateUniqueChatMode( + ctx.twitchChannel->roomId(), currentUser->getUserId(), true, + successCallback, failureCallback(ctx.channel)); + + return ""; +} + +QString uniqueChatOff(const CommandContext &ctx) +{ + auto currentUser = getApp()->getAccounts()->twitch.getCurrent(); + if (currentUser->isAnon()) + { + ctx.channel->addSystemMessage(P_NOT_LOGGED_IN); + return ""; + } + + if (ctx.twitchChannel == nullptr) + { + ctx.channel->addSystemMessage( + "The /uniquechatoff command only works in Twitch channels."); + return ""; + } + + if (!ctx.twitchChannel->accessRoomModes()->r9k) + { + ctx.channel->addSystemMessage("This room is not in unique-chat mode."); + return ""; + } + + getHelix()->updateUniqueChatMode( + ctx.twitchChannel->roomId(), currentUser->getUserId(), false, + successCallback, failureCallback(ctx.channel)); + + return ""; +} + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/ChatSettings.hpp b/src/controllers/commands/builtin/twitch/ChatSettings.hpp new file mode 100644 index 000000000..9362de12c --- /dev/null +++ b/src/controllers/commands/builtin/twitch/ChatSettings.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include + +namespace chatterino { + +struct CommandContext; + +namespace commands { + + QString emoteOnly(const CommandContext &ctx); + QString emoteOnlyOff(const CommandContext &ctx); + + QString subscribers(const CommandContext &ctx); + QString subscribersOff(const CommandContext &ctx); + + QString slow(const CommandContext &ctx); + QString slowOff(const CommandContext &ctx); + + QString followers(const CommandContext &ctx); + QString followersOff(const CommandContext &ctx); + + QString uniqueChat(const CommandContext &ctx); + QString uniqueChatOff(const CommandContext &ctx); + +} // namespace commands + +} // namespace chatterino diff --git a/src/controllers/commands/builtin/twitch/Chatters.cpp b/src/controllers/commands/builtin/twitch/Chatters.cpp new file mode 100644 index 000000000..e2f2932c7 --- /dev/null +++ b/src/controllers/commands/builtin/twitch/Chatters.cpp @@ -0,0 +1,139 @@ +#include "controllers/commands/builtin/twitch/Chatters.hpp" + +#include "Application.hpp" +#include "common/Channel.hpp" +#include "common/Env.hpp" +#include "common/Literals.hpp" +#include "controllers/accounts/AccountController.hpp" +#include "controllers/commands/CommandContext.hpp" +#include "messages/MessageBuilder.hpp" +#include "messages/MessageElement.hpp" +#include "providers/twitch/api/Helix.hpp" +#include "providers/twitch/TwitchAccount.hpp" +#include "providers/twitch/TwitchChannel.hpp" +#include "singletons/Theme.hpp" + +#include +#include +#include + +namespace { + +using namespace chatterino; + +QString formatChattersError(HelixGetChattersError error, const QString &message) +{ + using Error = HelixGetChattersError; + + QString errorMessage = QString("Failed to get chatter count - "); + + switch (error) + { + case Error::Forwarded: { + errorMessage += message; + } + break; + + case Error::UserMissingScope: { + errorMessage += "Missing required scope. " + "Re-login with your " + "account and try again."; + } + break; + + case Error::UserNotAuthorized: { + errorMessage += "You must have moderator permissions to " + "use this command."; + } + break; + + case Error::Unknown: { + errorMessage += "An unknown error has occurred."; + } + break; + } + return errorMessage; +} + +} // namespace + +namespace chatterino::commands { + +QString chatters(const CommandContext &ctx) +{ + if (ctx.channel == nullptr) + { + return ""; + } + + if (ctx.twitchChannel == nullptr) + { + ctx.channel->addSystemMessage( + "The /chatters command only works in Twitch Channels."); + return ""; + } + + // Refresh chatter list via helix api for mods + getHelix()->getChatters( + ctx.twitchChannel->roomId(), + getApp()->getAccounts()->twitch.getCurrent()->getUserId(), 1, + [channel{ctx.channel}](auto result) { + channel->addSystemMessage(QString("Chatter count: %1.") + .arg(localizeNumbers(result.total))); + }, + [channel{ctx.channel}](auto error, auto message) { + auto errorMessage = formatChattersError(error, message); + channel->addSystemMessage(errorMessage); + }); + + return ""; +} + +QString testChatters(const CommandContext &ctx) +{ + if (ctx.channel == nullptr) + { + return ""; + } + + if (ctx.twitchChannel == nullptr) + { + ctx.channel->addSystemMessage( + "The /test-chatters command only works in Twitch Channels."); + return ""; + } + + getHelix()->getChatters( + ctx.twitchChannel->roomId(), + getApp()->getAccounts()->twitch.getCurrent()->getUserId(), 5000, + [channel{ctx.channel}, twitchChannel{ctx.twitchChannel}](auto result) { + QStringList entries; + for (const auto &username : result.chatters) + { + entries << username; + } + + QString prefix = "Chatters "; + + if (result.total > 5000) + { + prefix += QString("(5000/%1):").arg(result.total); + } + else + { + prefix += QString("(%1):").arg(result.total); + } + + channel->addMessage(MessageBuilder::makeListOfUsersMessage( + prefix, entries, twitchChannel), + MessageContext::Original); + }, + [channel{ctx.channel}](auto error, auto message) { + auto errorMessage = formatChattersError(error, message); + channel->addSystemMessage(errorMessage); + }); + + return ""; +} + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/Chatters.hpp b/src/controllers/commands/builtin/twitch/Chatters.hpp new file mode 100644 index 000000000..25b34bab9 --- /dev/null +++ b/src/controllers/commands/builtin/twitch/Chatters.hpp @@ -0,0 +1,17 @@ +#pragma once + +class QString; + +namespace chatterino { + +struct CommandContext; + +} // namespace chatterino + +namespace chatterino::commands { + +QString chatters(const CommandContext &ctx); + +QString testChatters(const CommandContext &ctx); + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/DeleteMessages.cpp b/src/controllers/commands/builtin/twitch/DeleteMessages.cpp new file mode 100644 index 000000000..ab3b61a8e --- /dev/null +++ b/src/controllers/commands/builtin/twitch/DeleteMessages.cpp @@ -0,0 +1,161 @@ +#include "controllers/commands/builtin/twitch/DeleteMessages.hpp" + +#include "Application.hpp" +#include "common/Channel.hpp" +#include "common/network/NetworkResult.hpp" +#include "controllers/accounts/AccountController.hpp" +#include "controllers/commands/CommandContext.hpp" +#include "messages/Message.hpp" +#include "messages/MessageBuilder.hpp" +#include "providers/twitch/api/Helix.hpp" +#include "providers/twitch/TwitchAccount.hpp" +#include "providers/twitch/TwitchChannel.hpp" + +#include + +namespace { + +using namespace chatterino; + +QString deleteMessages(TwitchChannel *twitchChannel, const QString &messageID) +{ + const auto *commandName = messageID.isEmpty() ? "/clear" : "/delete"; + + auto user = getApp()->getAccounts()->twitch.getCurrent(); + + // Avoid Helix calls without Client ID and/or OAuth Token + if (user->isAnon()) + { + twitchChannel->addSystemMessage( + QString("You must be logged in to use the %1 command.") + .arg(commandName)); + return ""; + } + + getHelix()->deleteChatMessages( + twitchChannel->roomId(), user->getUserId(), messageID, + []() { + // Success handling, we do nothing: IRC/pubsub-edge will dispatch the correct + // events to update state for us. + }, + [twitchChannel, messageID](auto error, auto message) { + QString errorMessage = QString("Failed to delete chat messages - "); + + switch (error) + { + case HelixDeleteChatMessagesError::UserMissingScope: { + errorMessage += + "Missing required scope. Re-login with your " + "account and try again."; + } + break; + + case HelixDeleteChatMessagesError::UserNotAuthorized: { + errorMessage += + "you don't have permission to perform that action."; + } + break; + + case HelixDeleteChatMessagesError::MessageUnavailable: { + // Override default message prefix to match with IRC message format + errorMessage = + QString("The message %1 does not exist, was deleted, " + "or is too old to be deleted.") + .arg(messageID); + } + break; + + case HelixDeleteChatMessagesError::UserNotAuthenticated: { + errorMessage += "you need to re-authenticate."; + } + break; + + case HelixDeleteChatMessagesError::Forwarded: { + errorMessage += message; + } + break; + + case HelixDeleteChatMessagesError::Unknown: + default: { + errorMessage += "An unknown error has occurred."; + } + break; + } + + twitchChannel->addSystemMessage(errorMessage); + }); + + return ""; +} + +} // namespace + +namespace chatterino::commands { + +QString deleteAllMessages(const CommandContext &ctx) +{ + if (ctx.channel == nullptr) + { + return ""; + } + + if (ctx.twitchChannel == nullptr) + { + ctx.channel->addSystemMessage( + "The /clear command only works in Twitch channels."); + return ""; + } + + return deleteMessages(ctx.twitchChannel, QString()); +} + +QString deleteOneMessage(const CommandContext &ctx) +{ + if (ctx.channel == nullptr) + { + return ""; + } + + // This is a wrapper over the Helix delete messages endpoint + // We use this to ensure the user gets better error messages for missing or malformed arguments + if (ctx.twitchChannel == nullptr) + { + ctx.channel->addSystemMessage( + "The /delete command only works in Twitch channels."); + return ""; + } + + if (ctx.words.size() < 2) + { + ctx.channel->addSystemMessage("Usage: /delete - Deletes the " + "specified message."); + return ""; + } + + auto messageID = ctx.words.at(1); + auto uuid = QUuid(messageID); + if (uuid.isNull()) + { + // The message id must be a valid UUID + ctx.channel->addSystemMessage( + QString("Invalid msg-id: \"%1\"").arg(messageID)); + return ""; + } + + auto msg = ctx.channel->findMessage(messageID); + if (msg != nullptr) + { + if (msg->loginName == ctx.channel->getName() && + !ctx.channel->isBroadcaster()) + { + ctx.channel->addSystemMessage( + "You cannot delete the broadcaster's messages unless " + "you are the broadcaster."); + return ""; + } + } + + return deleteMessages(ctx.twitchChannel, messageID); +} + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/DeleteMessages.hpp b/src/controllers/commands/builtin/twitch/DeleteMessages.hpp new file mode 100644 index 000000000..24daae930 --- /dev/null +++ b/src/controllers/commands/builtin/twitch/DeleteMessages.hpp @@ -0,0 +1,19 @@ +#pragma once + +class QString; + +namespace chatterino { + +struct CommandContext; + +} // namespace chatterino + +namespace chatterino::commands { + +/// /clear +QString deleteAllMessages(const CommandContext &ctx); + +/// /delete +QString deleteOneMessage(const CommandContext &ctx); + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/GetModerators.cpp b/src/controllers/commands/builtin/twitch/GetModerators.cpp new file mode 100644 index 000000000..ea0bfdf3b --- /dev/null +++ b/src/controllers/commands/builtin/twitch/GetModerators.cpp @@ -0,0 +1,92 @@ +#include "controllers/commands/builtin/twitch/GetModerators.hpp" + +#include "common/Channel.hpp" +#include "controllers/commands/CommandContext.hpp" +#include "messages/MessageBuilder.hpp" +#include "providers/twitch/api/Helix.hpp" +#include "providers/twitch/TwitchChannel.hpp" + +namespace { + +using namespace chatterino; + +QString formatModsError(HelixGetModeratorsError error, const QString &message) +{ + using Error = HelixGetModeratorsError; + + QString errorMessage = QString("Failed to get moderators - "); + + switch (error) + { + case Error::Forwarded: { + errorMessage += message; + } + break; + + case Error::UserMissingScope: { + errorMessage += "Missing required scope. " + "Re-login with your " + "account and try again."; + } + break; + + case Error::UserNotAuthorized: { + errorMessage += + "Due to Twitch restrictions, " + "this command can only be used by the broadcaster. " + "To see the list of mods you must use the Twitch website."; + } + break; + + case Error::Unknown: { + errorMessage += "An unknown error has occurred."; + } + break; + } + return errorMessage; +} + +} // namespace + +namespace chatterino::commands { + +QString getModerators(const CommandContext &ctx) +{ + if (ctx.channel == nullptr) + { + return ""; + } + + if (ctx.twitchChannel == nullptr) + { + ctx.channel->addSystemMessage( + "The /mods command only works in Twitch Channels."); + return ""; + } + + getHelix()->getModerators( + ctx.twitchChannel->roomId(), 500, + [channel{ctx.channel}, twitchChannel{ctx.twitchChannel}](auto result) { + if (result.empty()) + { + channel->addSystemMessage( + "This channel does not have any moderators."); + return; + } + + // TODO: sort results? + + channel->addMessage(MessageBuilder::makeListOfUsersMessage( + "The moderators of this channel are", + result, twitchChannel), + MessageContext::Original); + }, + [channel{ctx.channel}](auto error, auto message) { + auto errorMessage = formatModsError(error, message); + channel->addSystemMessage(errorMessage); + }); + + return ""; +} + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/GetModerators.hpp b/src/controllers/commands/builtin/twitch/GetModerators.hpp new file mode 100644 index 000000000..517533246 --- /dev/null +++ b/src/controllers/commands/builtin/twitch/GetModerators.hpp @@ -0,0 +1,16 @@ +#pragma once + +class QString; + +namespace chatterino { + +struct CommandContext; + +} // namespace chatterino + +namespace chatterino::commands { + +/// /mods +QString getModerators(const CommandContext &ctx); + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/GetVIPs.cpp b/src/controllers/commands/builtin/twitch/GetVIPs.cpp new file mode 100644 index 000000000..ced19ff09 --- /dev/null +++ b/src/controllers/commands/builtin/twitch/GetVIPs.cpp @@ -0,0 +1,121 @@ +#include "controllers/commands/builtin/twitch/GetVIPs.hpp" + +#include "Application.hpp" +#include "controllers/accounts/AccountController.hpp" +#include "controllers/commands/CommandContext.hpp" +#include "messages/MessageBuilder.hpp" +#include "providers/twitch/api/Helix.hpp" +#include "providers/twitch/TwitchAccount.hpp" +#include "providers/twitch/TwitchChannel.hpp" + +namespace { + +using namespace chatterino; + +QString formatGetVIPsError(HelixListVIPsError error, const QString &message) +{ + using Error = HelixListVIPsError; + + QString errorMessage = QString("Failed to list VIPs - "); + + switch (error) + { + case Error::Forwarded: { + errorMessage += message; + } + break; + + case Error::Ratelimited: { + errorMessage += "You are being ratelimited by Twitch. Try " + "again in a few seconds."; + } + break; + + case Error::UserMissingScope: { + // TODO(pajlada): Phrase MISSING_REQUIRED_SCOPE + errorMessage += "Missing required scope. " + "Re-login with your " + "account and try again."; + } + break; + + case Error::UserNotAuthorized: { + // TODO(pajlada): Phrase MISSING_PERMISSION + errorMessage += "You don't have permission to " + "perform that action."; + } + break; + + case Error::UserNotBroadcaster: { + errorMessage += + "Due to Twitch restrictions, " + "this command can only be used by the broadcaster. " + "To see the list of VIPs you must use the Twitch website."; + } + break; + + case Error::Unknown: { + errorMessage += "An unknown error has occurred."; + } + break; + } + return errorMessage; +} + +} // namespace + +namespace chatterino::commands { + +QString getVIPs(const CommandContext &ctx) +{ + if (ctx.channel == nullptr) + { + return ""; + } + + if (ctx.twitchChannel == nullptr) + { + ctx.channel->addSystemMessage( + "The /vips command only works in Twitch channels."); + return ""; + } + + auto currentUser = getApp()->getAccounts()->twitch.getCurrent(); + if (currentUser->isAnon()) + { + ctx.channel->addSystemMessage( + "Due to Twitch restrictions, " // + "this command can only be used by the broadcaster. " + "To see the list of VIPs you must use the " + "Twitch website."); + return ""; + } + + getHelix()->getChannelVIPs( + ctx.twitchChannel->roomId(), + [channel{ctx.channel}, twitchChannel{ctx.twitchChannel}]( + const std::vector &vipList) { + if (vipList.empty()) + { + channel->addSystemMessage( + "This channel does not have any VIPs."); + return; + } + + auto messagePrefix = QString("The VIPs of this channel are"); + + // TODO: sort results? + + channel->addMessage(MessageBuilder::makeListOfUsersMessage( + messagePrefix, vipList, twitchChannel), + MessageContext::Original); + }, + [channel{ctx.channel}](auto error, auto message) { + auto errorMessage = formatGetVIPsError(error, message); + channel->addSystemMessage(errorMessage); + }); + + return ""; +} + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/GetVIPs.hpp b/src/controllers/commands/builtin/twitch/GetVIPs.hpp new file mode 100644 index 000000000..018537590 --- /dev/null +++ b/src/controllers/commands/builtin/twitch/GetVIPs.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include + +namespace chatterino { + +struct CommandContext; + +} // namespace chatterino + +namespace chatterino::commands { + +/// /vips +QString getVIPs(const CommandContext &ctx); + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/Raid.cpp b/src/controllers/commands/builtin/twitch/Raid.cpp new file mode 100644 index 000000000..1a54f17f3 --- /dev/null +++ b/src/controllers/commands/builtin/twitch/Raid.cpp @@ -0,0 +1,215 @@ +#include "controllers/commands/builtin/twitch/Raid.hpp" + +#include "Application.hpp" +#include "controllers/accounts/AccountController.hpp" +#include "controllers/commands/CommandContext.hpp" +#include "providers/twitch/api/Helix.hpp" +#include "providers/twitch/TwitchAccount.hpp" +#include "providers/twitch/TwitchChannel.hpp" +#include "util/Twitch.hpp" + +namespace { + +using namespace chatterino; + +QString formatStartRaidError(HelixStartRaidError error, const QString &message) +{ + QString errorMessage = QString("Failed to start a raid - "); + + using Error = HelixStartRaidError; + + switch (error) + { + case Error::UserMissingScope: { + // TODO(pajlada): Phrase MISSING_REQUIRED_SCOPE + errorMessage += "Missing required scope. " + "Re-login with your " + "account and try again."; + } + break; + + case Error::UserNotAuthorized: { + errorMessage += "You must be the broadcaster " + "to start a raid."; + } + break; + + case Error::CantRaidYourself: { + errorMessage += "A channel cannot raid itself."; + } + break; + + case Error::Ratelimited: { + errorMessage += "You are being ratelimited " + "by Twitch. Try " + "again in a few seconds."; + } + break; + + case Error::Forwarded: { + errorMessage += message; + } + break; + + case Error::Unknown: + default: { + errorMessage += "An unknown error has occurred."; + } + break; + } + + return errorMessage; +} + +QString formatCancelRaidError(HelixCancelRaidError error, + const QString &message) +{ + QString errorMessage = QString("Failed to cancel the raid - "); + + using Error = HelixCancelRaidError; + + switch (error) + { + case Error::UserMissingScope: { + // TODO(pajlada): Phrase MISSING_REQUIRED_SCOPE + errorMessage += "Missing required scope. " + "Re-login with your " + "account and try again."; + } + break; + + case Error::UserNotAuthorized: { + errorMessage += "You must be the broadcaster " + "to cancel the raid."; + } + break; + + case Error::NoRaidPending: { + errorMessage += "You don't have an active raid."; + } + break; + + case Error::Ratelimited: { + errorMessage += "You are being ratelimited by Twitch. Try " + "again in a few seconds."; + } + break; + + case Error::Forwarded: { + errorMessage += message; + } + break; + + case Error::Unknown: + default: { + errorMessage += "An unknown error has occurred."; + } + break; + } + + return errorMessage; +} + +} // namespace + +namespace chatterino::commands { + +QString startRaid(const CommandContext &ctx) +{ + if (ctx.channel == nullptr) + { + return ""; + } + + if (ctx.twitchChannel == nullptr) + { + ctx.channel->addSystemMessage( + "The /raid command only works in Twitch channels."); + return ""; + } + + if (ctx.words.size() < 2) + { + ctx.channel->addSystemMessage( + "Usage: \"/raid \" - Raid a user. " + "Only the broadcaster can start a raid."); + return ""; + } + + auto currentUser = getApp()->getAccounts()->twitch.getCurrent(); + if (currentUser->isAnon()) + { + ctx.channel->addSystemMessage("You must be logged in to start a raid!"); + return ""; + } + + auto target = ctx.words.at(1); + stripChannelName(target); + + getHelix()->getUserByName( + target, + [twitchChannel{ctx.twitchChannel}, + channel{ctx.channel}](const HelixUser &targetUser) { + getHelix()->startRaid( + twitchChannel->roomId(), targetUser.id, + [] { + // do nothing + }, + [channel, targetUser](auto error, auto message) { + auto errorMessage = formatStartRaidError(error, message); + channel->addSystemMessage(errorMessage); + }); + }, + [channel{ctx.channel}, target] { + // Equivalent error from IRC + channel->addSystemMessage( + QString("Invalid username: %1").arg(target)); + }); + + return ""; +} + +QString cancelRaid(const CommandContext &ctx) +{ + if (ctx.channel == nullptr) + { + return ""; + } + + if (ctx.twitchChannel == nullptr) + { + ctx.channel->addSystemMessage( + "The /unraid command only works in Twitch channels."); + return ""; + } + + if (ctx.words.size() != 1) + { + ctx.channel->addSystemMessage( + "Usage: \"/unraid\" - Cancel the current raid. " + "Only the broadcaster can cancel the raid."); + return ""; + } + + auto currentUser = getApp()->getAccounts()->twitch.getCurrent(); + if (currentUser->isAnon()) + { + ctx.channel->addSystemMessage( + "You must be logged in to cancel the raid!"); + return ""; + } + + getHelix()->cancelRaid( + ctx.twitchChannel->roomId(), + [] { + // do nothing + }, + [channel{ctx.channel}](auto error, auto message) { + auto errorMessage = formatCancelRaidError(error, message); + channel->addSystemMessage(errorMessage); + }); + + return ""; +} + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/Raid.hpp b/src/controllers/commands/builtin/twitch/Raid.hpp new file mode 100644 index 000000000..38d37644d --- /dev/null +++ b/src/controllers/commands/builtin/twitch/Raid.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include + +namespace chatterino { + +struct CommandContext; + +} // namespace chatterino + +namespace chatterino::commands { + +/// /raid +QString startRaid(const CommandContext &ctx); + +/// /unraid +QString cancelRaid(const CommandContext &ctx); + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/RemoveModerator.cpp b/src/controllers/commands/builtin/twitch/RemoveModerator.cpp new file mode 100644 index 000000000..8b8da22ca --- /dev/null +++ b/src/controllers/commands/builtin/twitch/RemoveModerator.cpp @@ -0,0 +1,119 @@ +#include "controllers/commands/builtin/twitch/RemoveModerator.hpp" + +#include "Application.hpp" +#include "controllers/accounts/AccountController.hpp" +#include "controllers/commands/CommandContext.hpp" +#include "providers/twitch/api/Helix.hpp" +#include "providers/twitch/TwitchAccount.hpp" +#include "providers/twitch/TwitchChannel.hpp" +#include "util/Twitch.hpp" + +namespace chatterino::commands { + +QString removeModerator(const CommandContext &ctx) +{ + if (ctx.channel == nullptr) + { + return ""; + } + + if (ctx.twitchChannel == nullptr) + { + ctx.channel->addSystemMessage( + "The /unmod command only works in Twitch channels."); + return ""; + } + if (ctx.words.size() < 2) + { + ctx.channel->addSystemMessage( + "Usage: \"/unmod \" - Revoke moderator status from a " + "user. Use \"/mods\" to list the moderators of this channel."); + return ""; + } + + auto currentUser = getApp()->getAccounts()->twitch.getCurrent(); + if (currentUser->isAnon()) + { + ctx.channel->addSystemMessage( + "You must be logged in to unmod someone!"); + return ""; + } + + auto target = ctx.words.at(1); + stripChannelName(target); + + getHelix()->getUserByName( + target, + [twitchChannel{ctx.twitchChannel}, + channel{ctx.channel}](const HelixUser &targetUser) { + getHelix()->removeChannelModerator( + twitchChannel->roomId(), targetUser.id, + [channel, targetUser] { + channel->addSystemMessage( + QString("You have removed %1 as a moderator of " + "this channel.") + .arg(targetUser.displayName)); + }, + [channel, targetUser](auto error, auto message) { + QString errorMessage = + QString("Failed to remove channel moderator - "); + + using Error = HelixRemoveChannelModeratorError; + + switch (error) + { + case Error::UserMissingScope: { + // TODO(pajlada): Phrase MISSING_REQUIRED_SCOPE + errorMessage += "Missing required scope. " + "Re-login with your " + "account and try again."; + } + break; + + case Error::UserNotAuthorized: { + // TODO(pajlada): Phrase MISSING_PERMISSION + errorMessage += "You don't have permission to " + "perform that action."; + } + break; + + case Error::Ratelimited: { + errorMessage += + "You are being ratelimited by Twitch. Try " + "again in a few seconds."; + } + break; + + case Error::TargetNotModded: { + // Equivalent irc error + errorMessage += + QString("%1 is not a moderator of this " + "channel.") + .arg(targetUser.displayName); + } + break; + + case Error::Forwarded: { + errorMessage += message; + } + break; + + case Error::Unknown: + default: { + errorMessage += "An unknown error has occurred."; + } + break; + } + channel->addSystemMessage(errorMessage); + }); + }, + [channel{ctx.channel}, target] { + // Equivalent error from IRC + channel->addSystemMessage( + QString("Invalid username: %1").arg(target)); + }); + + return ""; +} + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/RemoveModerator.hpp b/src/controllers/commands/builtin/twitch/RemoveModerator.hpp new file mode 100644 index 000000000..9b6894dc7 --- /dev/null +++ b/src/controllers/commands/builtin/twitch/RemoveModerator.hpp @@ -0,0 +1,16 @@ +#pragma once + +class QString; + +namespace chatterino { + +struct CommandContext; + +} // namespace chatterino + +namespace chatterino::commands { + +/// /unmod +QString removeModerator(const CommandContext &ctx); + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/RemoveVIP.cpp b/src/controllers/commands/builtin/twitch/RemoveVIP.cpp new file mode 100644 index 000000000..9a682a20e --- /dev/null +++ b/src/controllers/commands/builtin/twitch/RemoveVIP.cpp @@ -0,0 +1,111 @@ +#include "controllers/commands/builtin/twitch/RemoveVIP.hpp" + +#include "Application.hpp" +#include "common/Channel.hpp" +#include "controllers/accounts/AccountController.hpp" +#include "controllers/commands/CommandContext.hpp" +#include "messages/MessageBuilder.hpp" +#include "providers/twitch/api/Helix.hpp" +#include "providers/twitch/TwitchAccount.hpp" +#include "providers/twitch/TwitchChannel.hpp" +#include "util/Twitch.hpp" + +namespace chatterino::commands { + +QString removeVIP(const CommandContext &ctx) +{ + if (ctx.channel == nullptr) + { + return ""; + } + + if (ctx.twitchChannel == nullptr) + { + ctx.channel->addSystemMessage( + "The /unvip command only works in Twitch channels."); + return ""; + } + if (ctx.words.size() < 2) + { + ctx.channel->addSystemMessage( + "Usage: \"/unvip \" - Revoke VIP status from a user. " + "Use \"/vips\" to list the VIPs of this channel."); + return ""; + } + + auto currentUser = getApp()->getAccounts()->twitch.getCurrent(); + if (currentUser->isAnon()) + { + ctx.channel->addSystemMessage( + "You must be logged in to UnVIP someone!"); + return ""; + } + + auto target = ctx.words.at(1); + stripChannelName(target); + + getHelix()->getUserByName( + target, + [twitchChannel{ctx.twitchChannel}, + channel{ctx.channel}](const HelixUser &targetUser) { + getHelix()->removeChannelVIP( + twitchChannel->roomId(), targetUser.id, + [channel, targetUser] { + channel->addSystemMessage( + QString("You have removed %1 as a VIP of this channel.") + .arg(targetUser.displayName)); + }, + [channel, targetUser](auto error, auto message) { + QString errorMessage = QString("Failed to remove VIP - "); + + using Error = HelixRemoveChannelVIPError; + + switch (error) + { + case Error::UserMissingScope: { + // TODO(pajlada): Phrase MISSING_REQUIRED_SCOPE + errorMessage += "Missing required scope. " + "Re-login with your " + "account and try again."; + } + break; + + case Error::UserNotAuthorized: { + // TODO(pajlada): Phrase MISSING_PERMISSION + errorMessage += "You don't have permission to " + "perform that action."; + } + break; + + case Error::Ratelimited: { + errorMessage += + "You are being ratelimited by Twitch. Try " + "again in a few seconds."; + } + break; + + case Error::Forwarded: { + // These are actually the IRC equivalents, so we can ditch the prefix + errorMessage = message; + } + break; + + case Error::Unknown: + default: { + errorMessage += "An unknown error has occurred."; + } + break; + } + channel->addSystemMessage(errorMessage); + }); + }, + [channel{ctx.channel}, target] { + // Equivalent error from IRC + channel->addSystemMessage( + QString("Invalid username: %1").arg(target)); + }); + + return ""; +} + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/RemoveVIP.hpp b/src/controllers/commands/builtin/twitch/RemoveVIP.hpp new file mode 100644 index 000000000..ec66c62e7 --- /dev/null +++ b/src/controllers/commands/builtin/twitch/RemoveVIP.hpp @@ -0,0 +1,16 @@ +#pragma once + +class QString; + +namespace chatterino { + +struct CommandContext; + +} // namespace chatterino + +namespace chatterino::commands { + +/// /unvip +QString removeVIP(const CommandContext &ctx); + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/SendReply.cpp b/src/controllers/commands/builtin/twitch/SendReply.cpp new file mode 100644 index 000000000..80deb0154 --- /dev/null +++ b/src/controllers/commands/builtin/twitch/SendReply.cpp @@ -0,0 +1,59 @@ +#include "controllers/commands/builtin/twitch/SendReply.hpp" + +#include "controllers/commands/CommandContext.hpp" +#include "messages/Message.hpp" +#include "messages/MessageThread.hpp" +#include "providers/twitch/TwitchChannel.hpp" +#include "util/Twitch.hpp" + +namespace chatterino::commands { + +QString sendReply(const CommandContext &ctx) +{ + if (ctx.channel == nullptr) + { + return ""; + } + + if (ctx.twitchChannel == nullptr) + { + ctx.channel->addSystemMessage( + "The /reply command only works in Twitch channels."); + return ""; + } + + if (ctx.words.size() < 3) + { + ctx.channel->addSystemMessage("Usage: /reply "); + return ""; + } + + QString username = ctx.words[1]; + stripChannelName(username); + + auto snapshot = ctx.twitchChannel->getMessageSnapshot(); + for (auto it = snapshot.rbegin(); it != snapshot.rend(); ++it) + { + const auto &msg = *it; + if (msg->loginName.compare(username, Qt::CaseInsensitive) == 0) + { + // found most recent message by user + if (msg->replyThread == nullptr) + { + // prepare thread if one does not exist + auto thread = std::make_shared(msg); + ctx.twitchChannel->addReplyThread(thread); + } + + QString reply = ctx.words.mid(2).join(" "); + ctx.twitchChannel->sendReply(reply, msg->id); + return ""; + } + } + + ctx.channel->addSystemMessage("A message from that user wasn't found."); + + return ""; +} + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/SendReply.hpp b/src/controllers/commands/builtin/twitch/SendReply.hpp new file mode 100644 index 000000000..0909ae047 --- /dev/null +++ b/src/controllers/commands/builtin/twitch/SendReply.hpp @@ -0,0 +1,15 @@ +#pragma once + +class QString; + +namespace chatterino { + +struct CommandContext; + +} // namespace chatterino + +namespace chatterino::commands { + +QString sendReply(const CommandContext &ctx); + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/SendWhisper.cpp b/src/controllers/commands/builtin/twitch/SendWhisper.cpp new file mode 100644 index 000000000..cd099ac23 --- /dev/null +++ b/src/controllers/commands/builtin/twitch/SendWhisper.cpp @@ -0,0 +1,245 @@ +#include "controllers/commands/builtin/twitch/SendWhisper.hpp" + +#include "Application.hpp" +#include "common/LinkParser.hpp" +#include "controllers/accounts/AccountController.hpp" +#include "controllers/commands/CommandContext.hpp" +#include "messages/Message.hpp" +#include "messages/MessageBuilder.hpp" +#include "messages/MessageElement.hpp" +#include "providers/bttv/BttvEmotes.hpp" +#include "providers/ffz/FfzEmotes.hpp" +#include "providers/twitch/api/Helix.hpp" +#include "providers/twitch/TwitchAccount.hpp" +#include "providers/twitch/TwitchIrcServer.hpp" +#include "singletons/Emotes.hpp" +#include "singletons/Settings.hpp" +#include "singletons/StreamerMode.hpp" +#include "singletons/Theme.hpp" +#include "util/Twitch.hpp" + +namespace { + +using namespace chatterino; + +QString formatWhisperError(HelixWhisperError error, const QString &message) +{ + using Error = HelixWhisperError; + + QString errorMessage = "Failed to send whisper - "; + + switch (error) + { + case Error::NoVerifiedPhone: { + errorMessage += "Due to Twitch restrictions, you are now " + "required to have a verified phone number " + "to send whispers. You can add a phone " + "number in Twitch settings. " + "https://www.twitch.tv/settings/security"; + }; + break; + + case Error::RecipientBlockedUser: { + errorMessage += "The recipient doesn't allow whispers " + "from strangers or you directly."; + }; + break; + + case Error::WhisperSelf: { + errorMessage += "You cannot whisper yourself."; + }; + break; + + case Error::Forwarded: { + errorMessage += message; + } + break; + + case Error::Ratelimited: { + errorMessage += "You may only whisper a maximum of 40 " + "unique recipients per day. Within the " + "per day limit, you may whisper a " + "maximum of 3 whispers per second and " + "a maximum of 100 whispers per minute."; + } + break; + + case Error::UserMissingScope: { + // TODO(pajlada): Phrase MISSING_REQUIRED_SCOPE + errorMessage += "Missing required scope. " + "Re-login with your " + "account and try again."; + } + break; + + case Error::UserNotAuthorized: { + // TODO(pajlada): Phrase MISSING_PERMISSION + errorMessage += "You don't have permission to " + "perform that action."; + } + break; + + case Error::Unknown: { + errorMessage += "An unknown error has occurred."; + } + break; + } + + return errorMessage; +} + +bool appendWhisperMessageWordsLocally(const QStringList &words) +{ + auto *app = getApp(); + + MessageBuilder b; + + b.emplace(); + b.emplace( + app->getAccounts()->twitch.getCurrent()->getUserName(), + MessageElementFlag::Text, MessageColor::Text, + FontStyle::ChatMediumBold); + b.emplace("->", MessageElementFlag::Text, + getApp()->getThemes()->messages.textColors.system); + b.emplace(words[1] + ":", MessageElementFlag::Text, + MessageColor::Text, FontStyle::ChatMediumBold); + + const auto &acc = app->getAccounts()->twitch.getCurrent(); + const auto &accemotes = *acc->accessEmotes(); + const auto *bttvemotes = app->getBttvEmotes(); + const auto *ffzemotes = app->getFfzEmotes(); + auto flags = MessageElementFlags(); + auto emote = std::optional{}; + for (int i = 2; i < words.length(); i++) + { + { // Twitch emote + auto it = accemotes.emotes.find({words[i]}); + if (it != accemotes.emotes.end()) + { + b.emplace(it->second, + MessageElementFlag::TwitchEmote); + continue; + } + } // Twitch emote + + { // bttv/ffz emote + if ((emote = bttvemotes->emote({words[i]}))) + { + flags = MessageElementFlag::BttvEmote; + } + else if ((emote = ffzemotes->emote({words[i]}))) + { + flags = MessageElementFlag::FfzEmote; + } + // TODO: Load 7tv global emotes + if (emote) + { + b.emplace(*emote, flags); + continue; + } + } // bttv/ffz emote + { // emoji/text + for (auto &variant : app->getEmotes()->getEmojis()->parse(words[i])) + { + constexpr const static struct { + void operator()(EmotePtr emote, MessageBuilder &b) const + { + b.emplace(emote, + MessageElementFlag::EmojiAll); + } + void operator()(const QString &string, + MessageBuilder &b) const + { + auto link = linkparser::parse(string); + if (link) + { + b.addLink(*link, string); + } + else + { + b.emplace(string, + MessageElementFlag::Text); + } + } + } visitor; + boost::apply_visitor( + [&b](auto &&arg) { + visitor(arg, b); + }, + variant); + } // emoji/text + } + } + + b->flags.set(MessageFlag::DoNotTriggerNotification); + b->flags.set(MessageFlag::Whisper); + auto messagexD = b.release(); + + getApp()->getTwitch()->getWhispersChannel()->addMessage( + messagexD, MessageContext::Original); + + if (getSettings()->inlineWhispers && + !(getSettings()->streamerModeSuppressInlineWhispers && + getApp()->getStreamerMode()->isEnabled())) + { + app->getTwitch()->forEachChannel([&messagexD](ChannelPtr _channel) { + _channel->addMessage(messagexD, MessageContext::Repost); + }); + } + + return true; +} + +} // namespace + +namespace chatterino::commands { + +QString sendWhisper(const CommandContext &ctx) +{ + if (ctx.channel == nullptr) + { + return ""; + } + + if (ctx.words.size() < 3) + { + ctx.channel->addSystemMessage("Usage: /w "); + return ""; + } + + auto currentUser = getApp()->getAccounts()->twitch.getCurrent(); + if (currentUser->isAnon()) + { + ctx.channel->addSystemMessage( + "You must be logged in to send a whisper!"); + return ""; + } + auto target = ctx.words.at(1); + stripChannelName(target); + auto message = ctx.words.mid(2).join(' '); + if (ctx.channel->isTwitchChannel()) + { + getHelix()->getUserByName( + target, + [channel{ctx.channel}, currentUser, target, message, + words{ctx.words}](const auto &targetUser) { + getHelix()->sendWhisper( + currentUser->getUserId(), targetUser.id, message, + [words] { + appendWhisperMessageWordsLocally(words); + }, + [channel, target, targetUser](auto error, auto message) { + auto errorMessage = formatWhisperError(error, message); + channel->addSystemMessage(errorMessage); + }); + }, + [channel{ctx.channel}] { + channel->addSystemMessage("No user matching that username."); + }); + return ""; + } + + return ""; +} + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/SendWhisper.hpp b/src/controllers/commands/builtin/twitch/SendWhisper.hpp new file mode 100644 index 000000000..1e882a936 --- /dev/null +++ b/src/controllers/commands/builtin/twitch/SendWhisper.hpp @@ -0,0 +1,15 @@ +#pragma once + +class QString; + +namespace chatterino { + +struct CommandContext; + +} // namespace chatterino + +namespace chatterino::commands { + +QString sendWhisper(const CommandContext &ctx); + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/ShieldMode.cpp b/src/controllers/commands/builtin/twitch/ShieldMode.cpp new file mode 100644 index 000000000..93c3d9c68 --- /dev/null +++ b/src/controllers/commands/builtin/twitch/ShieldMode.cpp @@ -0,0 +1,94 @@ +#include "controllers/commands/builtin/twitch/ShieldMode.hpp" + +#include "Application.hpp" +#include "controllers/accounts/AccountController.hpp" +#include "controllers/commands/CommandContext.hpp" +#include "providers/twitch/api/Helix.hpp" +#include "providers/twitch/TwitchAccount.hpp" +#include "providers/twitch/TwitchChannel.hpp" + +namespace chatterino::commands { + +QString toggleShieldMode(const CommandContext &ctx, bool isActivating) +{ + const QString command = + isActivating ? QStringLiteral("/shield") : QStringLiteral("/shieldoff"); + + if (ctx.twitchChannel == nullptr) + { + ctx.channel->addSystemMessage( + QStringLiteral("The %1 command only works in Twitch channels.") + .arg(command)); + return {}; + } + + auto user = getApp()->getAccounts()->twitch.getCurrent(); + + // Avoid Helix calls without Client ID and/or OAuth Token + if (user->isAnon()) + { + ctx.channel->addSystemMessage( + QStringLiteral("You must be logged in to use the %1 command.") + .arg(command)); + return {}; + } + + getHelix()->updateShieldMode( + ctx.twitchChannel->roomId(), user->getUserId(), isActivating, + [channel = ctx.channel](const auto &res) { + if (!res.isActive) + { + channel->addSystemMessage("Shield mode was deactivated."); + return; + } + + channel->addSystemMessage("Shield mode was activated."); + }, + [channel = ctx.channel](const auto error, const auto &message) { + using Error = HelixUpdateShieldModeError; + QString errorMessage = "Failed to update shield mode - "; + + switch (error) + { + case Error::UserMissingScope: { + errorMessage += + "Missing required scope. Re-login with your " + "account and try again."; + } + break; + + case Error::MissingPermission: { + errorMessage += "You must be a moderator of the channel."; + } + break; + + case Error::Forwarded: { + errorMessage += message; + } + break; + + case Error::Unknown: + default: { + errorMessage += + QString("An unknown error has occurred (%1).") + .arg(message); + } + break; + } + channel->addSystemMessage(errorMessage); + }); + + return {}; +} + +QString shieldModeOn(const CommandContext &ctx) +{ + return toggleShieldMode(ctx, true); +} + +QString shieldModeOff(const CommandContext &ctx) +{ + return toggleShieldMode(ctx, false); +} + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/ShieldMode.hpp b/src/controllers/commands/builtin/twitch/ShieldMode.hpp new file mode 100644 index 000000000..fad2694e8 --- /dev/null +++ b/src/controllers/commands/builtin/twitch/ShieldMode.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include + +namespace chatterino { + +struct CommandContext; + +} // namespace chatterino + +namespace chatterino::commands { + +QString shieldModeOn(const CommandContext &ctx); +QString shieldModeOff(const CommandContext &ctx); + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/Shoutout.cpp b/src/controllers/commands/builtin/twitch/Shoutout.cpp new file mode 100644 index 000000000..7a79b5a3e --- /dev/null +++ b/src/controllers/commands/builtin/twitch/Shoutout.cpp @@ -0,0 +1,111 @@ +#include "controllers/commands/builtin/twitch/Shoutout.hpp" + +#include "Application.hpp" +#include "controllers/accounts/AccountController.hpp" +#include "controllers/commands/CommandContext.hpp" +#include "providers/twitch/api/Helix.hpp" +#include "providers/twitch/TwitchAccount.hpp" +#include "providers/twitch/TwitchChannel.hpp" +#include "util/Twitch.hpp" + +namespace chatterino::commands { + +QString sendShoutout(const CommandContext &ctx) +{ + auto *twitchChannel = ctx.twitchChannel; + auto channel = ctx.channel; + const auto *words = &ctx.words; + + if (twitchChannel == nullptr) + { + channel->addSystemMessage( + "The /shoutout command only works in Twitch channels."); + return ""; + } + + auto currentUser = getApp()->getAccounts()->twitch.getCurrent(); + if (currentUser->isAnon()) + { + channel->addSystemMessage("You must be logged in to send shoutout."); + return ""; + } + + if (words->size() < 2) + { + channel->addSystemMessage("Usage: \"/shoutout \" - Sends a " + "shoutout to the specified twitch user"); + return ""; + } + + auto target = words->at(1); + stripChannelName(target); + + using Error = HelixSendShoutoutError; + + getHelix()->getUserByName( + target, + [twitchChannel, channel, currentUser](const auto targetUser) { + getHelix()->sendShoutout( + twitchChannel->roomId(), targetUser.id, + currentUser->getUserId(), + [channel, targetUser]() { + channel->addSystemMessage( + QString("Sent shoutout to %1").arg(targetUser.login)); + }, + [channel](auto error, auto message) { + QString errorMessage = "Failed to send shoutout - "; + + switch (error) + { + case Error::UserNotAuthorized: { + errorMessage += "You don't have permission to " + "perform that action."; + } + break; + + case Error::UserMissingScope: { + errorMessage += "Missing required scope. " + "Re-login with your " + "account and try again."; + } + break; + + case Error::Ratelimited: { + errorMessage += + "You are being ratelimited by Twitch. " + "Try again in a few seconds."; + } + break; + + case Error::UserIsBroadcaster: { + errorMessage += "The broadcaster may not give " + "themselves a Shoutout."; + } + break; + + case Error::BroadcasterNotLive: { + errorMessage += + "The broadcaster is not streaming live or " + "does not have one or more viewers."; + } + break; + + case Error::Unknown: { + errorMessage += message; + } + break; + } + + channel->addSystemMessage(errorMessage); + }); + }, + [channel, target] { + // Equivalent error from IRC + channel->addSystemMessage( + QString("Invalid username: %1").arg(target)); + }); + + return ""; +} + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/Shoutout.hpp b/src/controllers/commands/builtin/twitch/Shoutout.hpp new file mode 100644 index 000000000..ffeec03f8 --- /dev/null +++ b/src/controllers/commands/builtin/twitch/Shoutout.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include + +namespace chatterino { + +struct CommandContext; + +} // namespace chatterino + +namespace chatterino::commands { + +QString sendShoutout(const CommandContext &ctx); + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/StartCommercial.cpp b/src/controllers/commands/builtin/twitch/StartCommercial.cpp new file mode 100644 index 000000000..8e6aa120c --- /dev/null +++ b/src/controllers/commands/builtin/twitch/StartCommercial.cpp @@ -0,0 +1,133 @@ +#include "controllers/commands/builtin/twitch/StartCommercial.hpp" + +#include "Application.hpp" +#include "controllers/accounts/AccountController.hpp" +#include "controllers/commands/CommandContext.hpp" +#include "providers/twitch/api/Helix.hpp" +#include "providers/twitch/TwitchAccount.hpp" +#include "providers/twitch/TwitchChannel.hpp" + +namespace { + +using namespace chatterino; + +QString formatStartCommercialError(HelixStartCommercialError error, + const QString &message) +{ + using Error = HelixStartCommercialError; + + QString errorMessage = "Failed to start commercial - "; + + switch (error) + { + case Error::UserMissingScope: { + errorMessage += "Missing required scope. Re-login with your " + "account and try again."; + } + break; + + case Error::TokenMustMatchBroadcaster: { + errorMessage += "Only the broadcaster of the channel can run " + "commercials."; + } + break; + + case Error::BroadcasterNotStreaming: { + errorMessage += "You must be streaming live to run " + "commercials."; + } + break; + + case Error::MissingLengthParameter: { + errorMessage += "Command must include a desired commercial break " + "length that is greater than zero."; + } + break; + + case Error::Ratelimited: { + errorMessage += "You must wait until your cooldown period " + "expires before you can run another " + "commercial."; + } + break; + + case Error::Forwarded: { + errorMessage += message; + } + break; + + case Error::Unknown: + default: { + errorMessage += + QString("An unknown error has occurred (%1).").arg(message); + } + break; + } + + return errorMessage; +} + +} // namespace + +namespace chatterino::commands { + +QString startCommercial(const CommandContext &ctx) +{ + if (ctx.channel == nullptr) + { + return ""; + } + + if (ctx.twitchChannel == nullptr) + { + ctx.channel->addSystemMessage( + "The /commercial command only works in Twitch channels."); + return ""; + } + + const auto *usageStr = "Usage: \"/commercial \" - Starts a " + "commercial with the " + "specified duration for the current " + "channel. Valid length options " + "are 30, 60, 90, 120, 150, and 180 seconds."; + + if (ctx.words.size() < 2) + { + ctx.channel->addSystemMessage(usageStr); + return ""; + } + + auto user = getApp()->getAccounts()->twitch.getCurrent(); + + // Avoid Helix calls without Client ID and/or OAuth Token + if (user->isAnon()) + { + ctx.channel->addSystemMessage( + "You must be logged in to use the /commercial command."); + return ""; + } + + auto broadcasterID = ctx.twitchChannel->roomId(); + auto length = ctx.words.at(1).toInt(); + + getHelix()->startCommercial( + broadcasterID, length, + [channel{ctx.channel}](auto response) { + channel->addSystemMessage( + QString("Starting %1 second long commercial break. " + "Keep in mind you are still " + "live and not all viewers will receive a " + "commercial. " + "You may run another commercial in %2 seconds.") + .arg(response.length) + .arg(response.retryAfter)); + }, + [channel{ctx.channel}](auto error, auto message) { + auto errorMessage = formatStartCommercialError(error, message); + channel->addSystemMessage(errorMessage); + }); + + return ""; +} + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/StartCommercial.hpp b/src/controllers/commands/builtin/twitch/StartCommercial.hpp new file mode 100644 index 000000000..3b1d550fc --- /dev/null +++ b/src/controllers/commands/builtin/twitch/StartCommercial.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include + +namespace chatterino { + +struct CommandContext; + +} // namespace chatterino + +namespace chatterino::commands { + +/// /commercial +QString startCommercial(const CommandContext &ctx); + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/Unban.cpp b/src/controllers/commands/builtin/twitch/Unban.cpp new file mode 100644 index 000000000..35f29f5de --- /dev/null +++ b/src/controllers/commands/builtin/twitch/Unban.cpp @@ -0,0 +1,196 @@ +#include "controllers/commands/builtin/twitch/Unban.hpp" + +#include "Application.hpp" +#include "common/Channel.hpp" +#include "common/QLogging.hpp" +#include "controllers/accounts/AccountController.hpp" +#include "controllers/commands/CommandContext.hpp" +#include "controllers/commands/common/ChannelAction.hpp" +#include "providers/twitch/api/Helix.hpp" +#include "providers/twitch/TwitchAccount.hpp" + +namespace { + +using namespace chatterino; + +void unbanUserByID(const ChannelPtr &channel, const QString &channelID, + const QString &sourceUserID, const QString &targetUserID, + const QString &displayName) +{ + getHelix()->unbanUser( + channelID, sourceUserID, targetUserID, + [] { + // No response for unbans, they're emitted over pubsub/IRC instead + }, + [channel, displayName](auto error, auto message) { + using Error = HelixUnbanUserError; + + QString errorMessage = QString("Failed to unban user - "); + + switch (error) + { + case Error::ConflictingOperation: { + errorMessage += "There was a conflicting ban operation on " + "this user. Please try again."; + } + break; + + case Error::Forwarded: { + errorMessage += message; + } + break; + + case Error::Ratelimited: { + errorMessage += "You are being ratelimited by Twitch. Try " + "again in a few seconds."; + } + break; + + case Error::TargetNotBanned: { + // Equivalent IRC error + errorMessage = + QString("%1 is not banned from this channel.") + .arg(displayName); + } + break; + + case Error::UserMissingScope: { + // TODO(pajlada): Phrase MISSING_REQUIRED_SCOPE + errorMessage += "Missing required scope. " + "Re-login with your " + "account and try again."; + } + break; + + case Error::UserNotAuthorized: { + // TODO(pajlada): Phrase MISSING_PERMISSION + errorMessage += "You don't have permission to " + "perform that action."; + } + break; + + case Error::Unknown: { + errorMessage += "An unknown error has occurred."; + } + break; + } + + channel->addSystemMessage(errorMessage); + }); +} + +} // namespace + +namespace chatterino::commands { + +QString unbanUser(const CommandContext &ctx) +{ + const auto command = ctx.words.at(0).toLower(); + const auto usage = + QStringLiteral( + R"(Usage: "%1 - Removes a ban on a user. Options: --channel to override which channel the unban takes place in (can be specified multiple times).)") + .arg(command); + const auto actions = parseChannelAction(ctx, command, usage, false, false); + if (!actions.has_value()) + { + if (ctx.channel != nullptr) + { + ctx.channel->addSystemMessage(actions.error()); + } + else + { + qCWarning(chatterinoCommands) + << "Error parsing command:" << actions.error(); + } + + return ""; + } + + assert(!actions.value().empty()); + + auto currentUser = getApp()->getAccounts()->twitch.getCurrent(); + if (currentUser->isAnon()) + { + ctx.channel->addSystemMessage( + "You must be logged in to unban someone!"); + return ""; + } + + for (const auto &action : actions.value()) + { + const auto &reason = action.reason; + + QStringList userLoginsToFetch; + QStringList userIDs; + if (action.target.id.isEmpty()) + { + assert(!action.target.login.isEmpty() && + "Unban Action target username AND user ID may not be " + "empty at the same time"); + userLoginsToFetch.append(action.target.login); + } + else + { + // For hydration + userIDs.append(action.target.id); + } + if (action.channel.id.isEmpty()) + { + assert(!action.channel.login.isEmpty() && + "Unban Action channel username AND user ID may not be " + "empty at the same time"); + userLoginsToFetch.append(action.channel.login); + } + else + { + // For hydration + userIDs.append(action.channel.id); + } + + if (!userLoginsToFetch.isEmpty()) + { + // At least 1 user ID needs to be resolved before we can take action + // userIDs is filled up with the data we already have to hydrate the action channel & action target + getHelix()->fetchUsers( + userIDs, userLoginsToFetch, + [channel{ctx.channel}, actionChannel{action.channel}, + actionTarget{action.target}, currentUser, reason, + userLoginsToFetch](const auto &users) mutable { + if (!actionChannel.hydrateFrom(users)) + { + channel->addSystemMessage( + QString("Failed to timeout, bad channel name: %1") + .arg(actionChannel.login)); + return; + } + if (!actionTarget.hydrateFrom(users)) + { + channel->addSystemMessage( + QString("Failed to timeout, bad target name: %1") + .arg(actionTarget.login)); + return; + } + + unbanUserByID(channel, actionChannel.id, + currentUser->getUserId(), actionTarget.id, + actionTarget.displayName); + }, + [channel{ctx.channel}, userLoginsToFetch] { + channel->addSystemMessage( + QString("Failed to timeout, bad username(s): %1") + .arg(userLoginsToFetch.join(", "))); + }); + } + else + { + // If both IDs are available, we do no hydration & just use the id as the display name + unbanUserByID(ctx.channel, action.channel.id, + currentUser->getUserId(), action.target.id, + action.target.id); + } + } + + return ""; +} + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/Unban.hpp b/src/controllers/commands/builtin/twitch/Unban.hpp new file mode 100644 index 000000000..4c32f09b7 --- /dev/null +++ b/src/controllers/commands/builtin/twitch/Unban.hpp @@ -0,0 +1,16 @@ +#pragma once + +class QString; + +namespace chatterino { + +struct CommandContext; + +} // namespace chatterino + +namespace chatterino::commands { + +/// /unban +QString unbanUser(const CommandContext &ctx); + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/UpdateChannel.cpp b/src/controllers/commands/builtin/twitch/UpdateChannel.cpp new file mode 100644 index 000000000..767f60899 --- /dev/null +++ b/src/controllers/commands/builtin/twitch/UpdateChannel.cpp @@ -0,0 +1,169 @@ +#include "controllers/commands/builtin/twitch/UpdateChannel.hpp" + +#include "common/network/NetworkResult.hpp" +#include "controllers/commands/CommandContext.hpp" +#include "providers/twitch/api/Helix.hpp" +#include "providers/twitch/TwitchChannel.hpp" + +namespace { + +using namespace chatterino; + +QString formatUpdateChannelError(const char *updateType, + HelixUpdateChannelError error, + const QString &message) +{ + using Error = HelixUpdateChannelError; + + QString errorMessage = QString("Failed to set %1 - ").arg(updateType); + + switch (error) + { + case Error::UserMissingScope: { + errorMessage += "Missing required scope. " + "Re-login with your " + "account and try again."; + } + break; + + case Error::UserNotAuthorized: { + errorMessage += QString("You must be the broadcaster " + "to set the %1.") + .arg(updateType); + } + break; + + case Error::Ratelimited: { + errorMessage += "You are being ratelimited by Twitch. Try " + "again in a few seconds."; + } + break; + + case Error::Forwarded: { + errorMessage += message; + } + break; + + case Error::Unknown: + default: { + errorMessage += + QString("An unknown error has occurred (%1).").arg(message); + } + break; + } + + return errorMessage; +} + +} // namespace + +namespace chatterino::commands { + +QString setTitle(const CommandContext &ctx) +{ + if (ctx.channel == nullptr) + { + return ""; + } + + if (ctx.words.size() < 2) + { + ctx.channel->addSystemMessage("Usage: /settitle "); + return ""; + } + + if (ctx.twitchChannel == nullptr) + { + ctx.channel->addSystemMessage( + "Unable to set title of non-Twitch channel."); + return ""; + } + + auto title = ctx.words.mid(1).join(" "); + + getHelix()->updateChannel( + ctx.twitchChannel->roomId(), "", "", title, + [channel{ctx.channel}, title](const auto &result) { + (void)result; + + channel->addSystemMessage( + QString("Updated title to %1").arg(title)); + }, + [channel{ctx.channel}](auto error, auto message) { + auto errorMessage = + formatUpdateChannelError("title", error, message); + channel->addSystemMessage(errorMessage); + }); + + return ""; +} + +QString setGame(const CommandContext &ctx) +{ + if (ctx.channel == nullptr) + { + return ""; + } + + if (ctx.words.size() < 2) + { + ctx.channel->addSystemMessage("Usage: /setgame "); + return ""; + } + + if (ctx.twitchChannel == nullptr) + { + ctx.channel->addSystemMessage( + "Unable to set game of non-Twitch channel."); + return ""; + } + + const auto gameName = ctx.words.mid(1).join(" "); + + getHelix()->searchGames( + gameName, + [channel{ctx.channel}, twitchChannel{ctx.twitchChannel}, + gameName](const std::vector &games) { + if (games.empty()) + { + channel->addSystemMessage("Game not found."); + return; + } + + auto matchedGame = games.at(0); + + if (games.size() > 1) + { + // NOTE: Improvements could be made with 'fuzzy string matching' code here + // attempt to find the best looking game by comparing exactly with lowercase values + for (const auto &game : games) + { + if (game.name.toLower() == gameName.toLower()) + { + matchedGame = game; + break; + } + } + } + + auto status = twitchChannel->accessStreamStatus(); + getHelix()->updateChannel( + twitchChannel->roomId(), matchedGame.id, "", "", + [channel, games, matchedGame](const NetworkResult &) { + channel->addSystemMessage( + QString("Updated game to %1").arg(matchedGame.name)); + }, + [channel](auto error, auto message) { + auto errorMessage = + formatUpdateChannelError("game", error, message); + channel->addSystemMessage(errorMessage); + }); + }, + [channel{ctx.channel}] { + channel->addSystemMessage("Failed to look up game."); + }); + + return ""; +} + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/UpdateChannel.hpp b/src/controllers/commands/builtin/twitch/UpdateChannel.hpp new file mode 100644 index 000000000..2a085b49c --- /dev/null +++ b/src/controllers/commands/builtin/twitch/UpdateChannel.hpp @@ -0,0 +1,16 @@ +#pragma once + +class QString; + +namespace chatterino { + +struct CommandContext; + +} // namespace chatterino + +namespace chatterino::commands { + +QString setTitle(const CommandContext &ctx); +QString setGame(const CommandContext &ctx); + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/UpdateColor.cpp b/src/controllers/commands/builtin/twitch/UpdateColor.cpp new file mode 100644 index 000000000..15680c276 --- /dev/null +++ b/src/controllers/commands/builtin/twitch/UpdateColor.cpp @@ -0,0 +1,99 @@ +#include "controllers/commands/builtin/twitch/UpdateColor.hpp" + +#include "Application.hpp" +#include "common/Channel.hpp" +#include "controllers/accounts/AccountController.hpp" +#include "controllers/commands/CommandContext.hpp" +#include "messages/MessageBuilder.hpp" +#include "providers/twitch/api/Helix.hpp" +#include "providers/twitch/TwitchAccount.hpp" +#include "providers/twitch/TwitchChannel.hpp" +#include "util/Twitch.hpp" + +namespace chatterino::commands { + +QString updateUserColor(const CommandContext &ctx) +{ + if (ctx.channel == nullptr) + { + return ""; + } + + if (!ctx.channel->isTwitchChannel()) + { + ctx.channel->addSystemMessage( + "The /color command only works in Twitch channels."); + return ""; + } + auto user = getApp()->getAccounts()->twitch.getCurrent(); + + // Avoid Helix calls without Client ID and/or OAuth Token + if (user->isAnon()) + { + ctx.channel->addSystemMessage( + "You must be logged in to use the /color command."); + return ""; + } + + auto colorString = ctx.words.value(1); + + if (colorString.isEmpty()) + { + ctx.channel->addSystemMessage( + QString("Usage: /color - Color must be one of Twitch's " + "supported colors (%1) or a hex code (#000000) if you " + "have Turbo or Prime.") + .arg(VALID_HELIX_COLORS.join(", "))); + return ""; + } + + cleanHelixColorName(colorString); + + getHelix()->updateUserChatColor( + user->getUserId(), colorString, + [colorString, channel{ctx.channel}] { + QString successMessage = + QString("Your color has been changed to %1.").arg(colorString); + channel->addSystemMessage(successMessage); + }, + [colorString, channel{ctx.channel}](auto error, auto message) { + QString errorMessage = + QString("Failed to change color to %1 - ").arg(colorString); + + switch (error) + { + case HelixUpdateUserChatColorError::UserMissingScope: { + errorMessage += + "Missing required scope. Re-login with your " + "account and try again."; + } + break; + + case HelixUpdateUserChatColorError::InvalidColor: { + errorMessage += QString("Color must be one of Twitch's " + "supported colors (%1) or a " + "hex code (#000000) if you " + "have Turbo or Prime.") + .arg(VALID_HELIX_COLORS.join(", ")); + } + break; + + case HelixUpdateUserChatColorError::Forwarded: { + errorMessage += message + "."; + } + break; + + case HelixUpdateUserChatColorError::Unknown: + default: { + errorMessage += "An unknown error has occurred."; + } + break; + } + + channel->addSystemMessage(errorMessage); + }); + + return ""; +} + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/UpdateColor.hpp b/src/controllers/commands/builtin/twitch/UpdateColor.hpp new file mode 100644 index 000000000..c4c3bdaf0 --- /dev/null +++ b/src/controllers/commands/builtin/twitch/UpdateColor.hpp @@ -0,0 +1,15 @@ +#pragma once + +class QString; + +namespace chatterino { + +struct CommandContext; + +} // namespace chatterino + +namespace chatterino::commands { + +QString updateUserColor(const CommandContext &ctx); + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/Warn.cpp b/src/controllers/commands/builtin/twitch/Warn.cpp new file mode 100644 index 000000000..2a3cb1fa5 --- /dev/null +++ b/src/controllers/commands/builtin/twitch/Warn.cpp @@ -0,0 +1,197 @@ +#include "controllers/commands/builtin/twitch/Warn.hpp" + +#include "Application.hpp" +#include "common/Channel.hpp" +#include "common/QLogging.hpp" +#include "controllers/accounts/AccountController.hpp" +#include "controllers/commands/CommandContext.hpp" +#include "controllers/commands/common/ChannelAction.hpp" +#include "providers/twitch/api/Helix.hpp" +#include "providers/twitch/TwitchAccount.hpp" + +namespace { + +using namespace chatterino; + +void warnUserByID(const ChannelPtr &channel, const QString &channelID, + const QString &sourceUserID, const QString &targetUserID, + const QString &reason, const QString &displayName) +{ + using Error = HelixWarnUserError; + + getHelix()->warnUser( + channelID, sourceUserID, targetUserID, reason, + [] { + // No response for warns, they're emitted over pubsub instead + }, + [channel, displayName](auto error, auto message) { + QString errorMessage = QString("Failed to warn user - "); + switch (error) + { + case Error::ConflictingOperation: { + errorMessage += "There was a conflicting warn operation on " + "this user. Please try again."; + } + break; + + case Error::Forwarded: { + errorMessage += message; + } + break; + + case Error::Ratelimited: { + errorMessage += "You are being ratelimited by Twitch. Try " + "again in a few seconds."; + } + break; + + case Error::CannotWarnUser: { + errorMessage += + QString("You cannot warn %1.").arg(displayName); + } + break; + + case Error::UserMissingScope: { + // TODO(pajlada): Phrase MISSING_REQUIRED_SCOPE + errorMessage += "Missing required scope. " + "Re-login with your " + "account and try again."; + } + break; + + case Error::UserNotAuthorized: { + // TODO(pajlada): Phrase MISSING_PERMISSION + errorMessage += "You don't have permission to " + "perform that action."; + } + break; + + case Error::Unknown: { + errorMessage += "An unknown error has occurred."; + } + break; + } + + channel->addSystemMessage(errorMessage); + }); +} + +} // namespace + +namespace chatterino::commands { + +QString sendWarn(const CommandContext &ctx) +{ + const auto command = QStringLiteral("/warn"); + const auto usage = QStringLiteral( + R"(Usage: "/warn [options...] " - Warn a user via their username. Reason is required and will be shown to the target user and other moderators. Options: --channel to override which channel the warn takes place in (can be specified multiple times).)"); + const auto actions = parseChannelAction(ctx, command, usage, false, true); + + if (!actions.has_value()) + { + if (ctx.channel != nullptr) + { + ctx.channel->addSystemMessage(actions.error()); + } + else + { + qCWarning(chatterinoCommands) + << "Error parsing command:" << actions.error(); + } + + return ""; + } + + assert(!actions.value().empty()); + + auto currentUser = getApp()->getAccounts()->twitch.getCurrent(); + if (currentUser->isAnon()) + { + ctx.channel->addSystemMessage("You must be logged in to warn someone!"); + return ""; + } + + for (const auto &action : actions.value()) + { + const auto &reason = action.reason; + if (reason.isEmpty()) + { + ctx.channel->addSystemMessage( + "Failed to warn, you must specify a reason"); + break; + } + + QStringList userLoginsToFetch; + QStringList userIDs; + if (action.target.id.isEmpty()) + { + assert(!action.target.login.isEmpty() && + "Warn Action target username AND user ID may not be " + "empty at the same time"); + userLoginsToFetch.append(action.target.login); + } + else + { + // For hydration + userIDs.append(action.target.id); + } + if (action.channel.id.isEmpty()) + { + assert(!action.channel.login.isEmpty() && + "Warn Action channel username AND user ID may not be " + "empty at the same time"); + userLoginsToFetch.append(action.channel.login); + } + else + { + // For hydration + userIDs.append(action.channel.id); + } + + if (!userLoginsToFetch.isEmpty()) + { + // At least 1 user ID needs to be resolved before we can take action + // userIDs is filled up with the data we already have to hydrate the action channel & action target + getHelix()->fetchUsers( + userIDs, userLoginsToFetch, + [channel{ctx.channel}, actionChannel{action.channel}, + actionTarget{action.target}, currentUser, reason, + userLoginsToFetch](const auto &users) mutable { + if (!actionChannel.hydrateFrom(users)) + { + channel->addSystemMessage( + QString("Failed to warn, bad channel name: %1") + .arg(actionChannel.login)); + return; + } + if (!actionTarget.hydrateFrom(users)) + { + channel->addSystemMessage( + QString("Failed to warn, bad target name: %1") + .arg(actionTarget.login)); + return; + } + + warnUserByID(channel, actionChannel.id, + currentUser->getUserId(), actionTarget.id, + reason, actionTarget.displayName); + }, + [channel{ctx.channel}, userLoginsToFetch] { + channel->addSystemMessage( + QString("Failed to warn, bad username(s): %1") + .arg(userLoginsToFetch.join(", "))); + }); + } + else + { + // If both IDs are available, we do no hydration & just use the id as the display name + warnUserByID(ctx.channel, action.channel.id, + currentUser->getUserId(), action.target.id, reason, + action.target.id); + } + } + + return ""; +} + +} // namespace chatterino::commands diff --git a/src/controllers/commands/builtin/twitch/Warn.hpp b/src/controllers/commands/builtin/twitch/Warn.hpp new file mode 100644 index 000000000..42c78f564 --- /dev/null +++ b/src/controllers/commands/builtin/twitch/Warn.hpp @@ -0,0 +1,16 @@ +#pragma once + +class QString; + +namespace chatterino { + +struct CommandContext; + +} // namespace chatterino + +namespace chatterino::commands { + +/// /warn +QString sendWarn(const CommandContext &ctx); + +} // namespace chatterino::commands diff --git a/src/controllers/commands/common/ChannelAction.cpp b/src/controllers/commands/common/ChannelAction.cpp new file mode 100644 index 000000000..487385920 --- /dev/null +++ b/src/controllers/commands/common/ChannelAction.cpp @@ -0,0 +1,184 @@ +#include "controllers/commands/common/ChannelAction.hpp" + +#include "controllers/commands/CommandContext.hpp" +#include "providers/twitch/api/Helix.hpp" +#include "providers/twitch/TwitchChannel.hpp" +#include "util/Helpers.hpp" +#include "util/Twitch.hpp" + +#include +#include + +#include +#include +#include +#include + +namespace chatterino::commands { + +bool IncompleteHelixUser::hydrateFrom(const std::vector &users) +{ + // Find user in list based on our id or login + auto resolvedIt = + std::find_if(users.begin(), users.end(), [this](const auto &user) { + if (!this->login.isEmpty()) + { + return user.login.compare(this->login, Qt::CaseInsensitive) == + 0; + } + if (!this->id.isEmpty()) + { + return user.id.compare(this->id, Qt::CaseInsensitive) == 0; + } + return false; + }); + if (resolvedIt == users.end()) + { + return false; + } + const auto &resolved = *resolvedIt; + this->id = resolved.id; + this->login = resolved.login; + this->displayName = resolved.displayName; + return true; +} + +std::ostream &operator<<(std::ostream &os, const IncompleteHelixUser &u) +{ + os << "{id:" << u.id.toStdString() << ", login:" << u.login.toStdString() + << ", displayName:" << u.displayName.toStdString() << '}'; + return os; +} + +void PrintTo(const PerformChannelAction &a, std::ostream *os) +{ + *os << "{channel:" << a.channel << ", target:" << a.target + << ", reason:" << a.reason.toStdString() + << ", duration:" << std::to_string(a.duration) << '}'; +} + +nonstd::expected, QString> parseChannelAction( + const CommandContext &ctx, const QString &command, const QString &usage, + bool withDuration, bool withReason) +{ + if (ctx.channel == nullptr) + { + // A ban action must be performed with a channel as a context + return nonstd::make_unexpected( + "A " % command % + " action must be performed with a channel as a context"); + } + + QCommandLineParser parser; + parser.setOptionsAfterPositionalArgumentsMode( + QCommandLineParser::ParseAsPositionalArguments); + parser.addPositionalArgument("username", "The name of the user to ban"); + if (withDuration) + { + parser.addPositionalArgument("duration", "Duration of the action"); + } + if (withReason) + { + parser.addPositionalArgument("reason", "The optional ban reason"); + } + QCommandLineOption channelOption( + "channel", "Override which channel(s) to perform the action in", + "channel"); + parser.addOptions({ + channelOption, + }); + parser.parse(ctx.words); + + auto positionalArguments = parser.positionalArguments(); + if (positionalArguments.isEmpty()) + { + return nonstd::make_unexpected("Missing target - " % usage); + } + + auto [targetUserName, targetUserID] = + parseUserNameOrID(positionalArguments.takeFirst()); + + PerformChannelAction base{ + .target = + IncompleteHelixUser{ + .id = targetUserID, + .login = targetUserName, + .displayName = "", + }, + .duration = 0, + }; + + if (withDuration) + { + if (positionalArguments.isEmpty()) + { + base.duration = 10 * 60; // 10 min + } + else + { + auto durationStr = positionalArguments.takeFirst(); + base.duration = (int)parseDurationToSeconds(durationStr); + if (base.duration <= 0) + { + return nonstd::make_unexpected("Invalid duration - " % usage); + } + if (withReason) + { + base.reason = positionalArguments.join(' '); + } + } + } + else + { + if (withReason) + { + base.reason = positionalArguments.join(' '); + } + } + + std::vector actions; + + auto overrideChannels = parser.values(channelOption); + if (overrideChannels.isEmpty()) + { + if (ctx.twitchChannel == nullptr) + { + return nonstd::make_unexpected( + "The " % command % " command only works in Twitch channels"); + } + + actions.push_back(PerformChannelAction{ + .channel = + { + .id = ctx.twitchChannel->roomId(), + .login = ctx.twitchChannel->getName(), + .displayName = ctx.twitchChannel->getDisplayName(), + }, + .target = base.target, + .reason = base.reason, + .duration = base.duration, + }); + } + else + { + for (const auto &overrideChannelTarget : overrideChannels) + { + auto [channelUserName, channelUserID] = + parseUserNameOrID(overrideChannelTarget); + actions.push_back(PerformChannelAction{ + .channel = + { + .id = channelUserID, + .login = channelUserName, + }, + .target = base.target, + .reason = base.reason, + .duration = base.duration, + }); + } + } + + return actions; +} + +} // namespace chatterino::commands diff --git a/src/controllers/commands/common/ChannelAction.hpp b/src/controllers/commands/common/ChannelAction.hpp new file mode 100644 index 000000000..fd80eeb79 --- /dev/null +++ b/src/controllers/commands/common/ChannelAction.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include +#include + +#include +#include +#include + +namespace chatterino { + +struct CommandContext; +struct HelixUser; + +} // namespace chatterino + +namespace chatterino::commands { + +struct IncompleteHelixUser { + QString id; + QString login; + QString displayName; + + bool hydrateFrom(const std::vector &users); + + bool operator==(const IncompleteHelixUser &other) const + { + return std::tie(this->id, this->login, this->displayName) == + std::tie(other.id, other.login, other.displayName); + } +}; + +struct PerformChannelAction { + // Channel to perform the action in + IncompleteHelixUser channel; + // Target to perform the action on + IncompleteHelixUser target; + QString reason; + int duration{}; + + bool operator==(const PerformChannelAction &other) const + { + return std::tie(this->channel, this->target, this->reason, + this->duration) == std::tie(other.channel, other.target, + other.reason, + other.duration); + } +}; + +std::ostream &operator<<(std::ostream &os, const IncompleteHelixUser &u); +// gtest printer +// NOLINTNEXTLINE(readability-identifier-naming) +void PrintTo(const PerformChannelAction &a, std::ostream *os); + +nonstd::expected, QString> parseChannelAction( + const CommandContext &ctx, const QString &command, const QString &usage, + bool withDuration, bool withReason); + +} // namespace chatterino::commands diff --git a/src/controllers/completion/CompletionModel.cpp b/src/controllers/completion/CompletionModel.cpp new file mode 100644 index 000000000..ef19688c0 --- /dev/null +++ b/src/controllers/completion/CompletionModel.cpp @@ -0,0 +1,34 @@ +#include "controllers/completion/CompletionModel.hpp" + +#include "controllers/completion/sources/Source.hpp" + +namespace chatterino { + +CompletionModel::CompletionModel(QObject *parent) + : GenericListModel(parent) +{ +} + +void CompletionModel::setSource(std::unique_ptr source) +{ + this->source_ = std::move(source); +} + +bool CompletionModel::hasSource() const +{ + return this->source_ != nullptr; +} + +void CompletionModel::updateResults(const QString &query, size_t maxCount) +{ + if (this->source_) + { + this->source_->update(query); + + // Copy results to this model + this->clear(); + this->source_->addToListModel(*this, maxCount); + } +} + +} // namespace chatterino diff --git a/src/controllers/completion/CompletionModel.hpp b/src/controllers/completion/CompletionModel.hpp new file mode 100644 index 000000000..f787a965b --- /dev/null +++ b/src/controllers/completion/CompletionModel.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include "widgets/listview/GenericListModel.hpp" + +#include +#include + +namespace chatterino { + +namespace completion { + class Source; +} // namespace completion + +/// @brief Represents the kind of completion occurring +enum class CompletionKind { + Emote, + User, +}; + +/// @brief CompletionModel is a GenericListModel intended to provide completion +/// suggestions to an InputCompletionPopup. The popup can determine the appropriate +/// source based on the current input and the user's preferences. +class CompletionModel final : public GenericListModel +{ +public: + explicit CompletionModel(QObject *parent); + + /// @brief Sets the Source for subsequent queries + /// @param source Source to use + void setSource(std::unique_ptr source); + + /// @return Whether the model has a source set + bool hasSource() const; + + /// @brief Updates the model based on the completion query + /// @param query Completion query + /// @param maxCount Maximum number of results. Zero indicates unlimited. + void updateResults(const QString &query, size_t maxCount = 0); + +private: + std::unique_ptr source_{}; +}; + +}; // namespace chatterino diff --git a/src/controllers/completion/TabCompletionModel.cpp b/src/controllers/completion/TabCompletionModel.cpp new file mode 100644 index 000000000..585fe3a08 --- /dev/null +++ b/src/controllers/completion/TabCompletionModel.cpp @@ -0,0 +1,172 @@ +#include "controllers/completion/TabCompletionModel.hpp" + +#include "Application.hpp" +#include "common/Channel.hpp" +#include "controllers/completion/sources/CommandSource.hpp" +#include "controllers/completion/sources/EmoteSource.hpp" +#include "controllers/completion/sources/UnifiedSource.hpp" +#include "controllers/completion/sources/UserSource.hpp" +#include "controllers/completion/strategies/ClassicEmoteStrategy.hpp" +#include "controllers/completion/strategies/ClassicUserStrategy.hpp" +#include "controllers/completion/strategies/CommandStrategy.hpp" +#include "controllers/completion/strategies/SmartEmoteStrategy.hpp" +#include "controllers/plugins/LuaUtilities.hpp" +#include "controllers/plugins/Plugin.hpp" +#include "controllers/plugins/PluginController.hpp" +#include "singletons/Settings.hpp" + +namespace chatterino { + +TabCompletionModel::TabCompletionModel(Channel &channel, QObject *parent) + : QStringListModel(parent) + , channel_(channel) +{ +} + +void TabCompletionModel::updateResults(const QString &query, + const QString &fullTextContent, + int cursorPosition, bool isFirstWord) +{ + this->updateSourceFromQuery(query); + + if (this->source_) + { + this->source_->update(query); + + // Copy results to this model + QStringList results; +#ifdef CHATTERINO_HAVE_PLUGINS + // Try plugins first + bool done{}; + std::tie(done, results) = + getApp()->getPlugins()->updateCustomCompletions( + query, fullTextContent, cursorPosition, isFirstWord); + if (done) + { + this->setStringList(results); + return; + } +#endif + this->source_->addToStringList(results, 0, isFirstWord); + this->setStringList(results); + } +} + +void TabCompletionModel::updateSourceFromQuery(const QString &query) +{ + auto deducedKind = this->deduceSourceKind(query); + if (!deducedKind) + { + // unable to determine what kind of completion is occurring + this->source_ = nullptr; + return; + } + + // Build source for new query + this->source_ = this->buildSource(*deducedKind); +} + +std::optional + TabCompletionModel::deduceSourceKind(const QString &query) const +{ + if (query.length() < 2 || !this->channel_.isTwitchChannel()) + { + return std::nullopt; + } + + // Check for cases where we can definitively say what kind of completion is taking place. + + if (query.startsWith('@')) + { + return SourceKind::User; + } + else if (query.startsWith(':')) + { + return SourceKind::Emote; + } + else if (query.startsWith('/') || query.startsWith('.')) + { + return SourceKind::Command; + } + + // At this point, we note that emotes can be completed without using a : + // Therefore, we must also consider that the user could be completing an emote + // OR a mention depending on their completion settings. + + if (getSettings()->userCompletionOnlyWithAt) + { + // All kinds but user are possible + return SourceKind::EmoteCommand; + } + + // Any kind is possible + return SourceKind::EmoteUserCommand; +} + +std::unique_ptr TabCompletionModel::buildSource( + SourceKind kind) const +{ + switch (kind) + { + case SourceKind::Emote: { + return this->buildEmoteSource(); + } + case SourceKind::User: { + return this->buildUserSource(true); // Completing with @ + } + case SourceKind::Command: { + return this->buildCommandSource(); + } + case SourceKind::EmoteCommand: { + std::vector> sources; + sources.push_back(this->buildEmoteSource()); + sources.push_back(this->buildCommandSource()); + + return std::make_unique( + std::move(sources)); + } + case SourceKind::EmoteUserCommand: { + std::vector> sources; + sources.push_back(this->buildEmoteSource()); + sources.push_back( + this->buildUserSource(false)); // Not completing with @ + sources.push_back(this->buildCommandSource()); + + return std::make_unique( + std::move(sources)); + } + default: + return nullptr; + } +} + +std::unique_ptr TabCompletionModel::buildEmoteSource() const +{ + if (getSettings()->useSmartEmoteCompletion) + { + return std::make_unique( + &this->channel_, + std::make_unique()); + } + + return std::make_unique( + &this->channel_, + std::make_unique()); +} + +std::unique_ptr TabCompletionModel::buildUserSource( + bool prependAt) const +{ + return std::make_unique( + &this->channel_, std::make_unique(), + nullptr, prependAt); +} + +std::unique_ptr TabCompletionModel::buildCommandSource() + const +{ + return std::make_unique( + std::make_unique(true)); +} + +} // namespace chatterino diff --git a/src/controllers/completion/TabCompletionModel.hpp b/src/controllers/completion/TabCompletionModel.hpp new file mode 100644 index 000000000..56cf313ed --- /dev/null +++ b/src/controllers/completion/TabCompletionModel.hpp @@ -0,0 +1,73 @@ +#pragma once + +#include "controllers/completion/sources/Source.hpp" + +#include +#include +#include + +#include + +namespace chatterino { + +class Channel; + +/// @brief TabCompletionModel is a QStringListModel intended to provide tab +/// completion to a ResizingTextInput. The model automatically selects a completion +/// source based on the current query before updating the results. +class TabCompletionModel : public QStringListModel +{ +public: + /// @brief Initializes a new TabCompletionModel bound to a Channel. + /// The reference to the Channel must live as long as the TabCompletionModel. + /// @param channel Channel reference + /// @param parent Model parent + explicit TabCompletionModel(Channel &channel, QObject *parent); + + /// @brief Updates the model based on the completion query + /// @param query Completion query + /// @param fullTextContent Full text of the input, used by plugins for contextual completion + /// @param cursorPosition Number of characters behind the cursor from the + /// beginning of fullTextContent, also used by plugins + /// @param isFirstWord Whether the completion is the first word in the input + void updateResults(const QString &query, const QString &fullTextContent, + int cursorPosition, bool isFirstWord = false); + +private: + enum class SourceKind { + // Known to be an emote, i.e. started with : + Emote, + // Known to be a username, i.e. started with @ + User, + // Known to be a command, i.e. started with / or . + Command, + // Emote or command without : or / . + EmoteCommand, + // Emote, user, or command without :, @, / . + EmoteUserCommand + }; + + /// @brief Updates the internal completion source based on the current query. + /// The completion source will only change if the deduced completion kind + /// changes (see deduceSourceKind). + /// @param query Completion query + void updateSourceFromQuery(const QString &query); + + /// @brief Attempts to deduce the source kind from the current query. If the + /// bound Channel is not a TwitchChannel or if the query is too short, no + /// query type will be deduced to prevent completions. + /// @param query Completion query + /// @return An optional SourceKind deduced from the query + std::optional deduceSourceKind(const QString &query) const; + + std::unique_ptr buildSource(SourceKind kind) const; + + std::unique_ptr buildEmoteSource() const; + std::unique_ptr buildUserSource(bool prependAt) const; + std::unique_ptr buildCommandSource() const; + + Channel &channel_; + std::unique_ptr source_{}; +}; + +} // namespace chatterino diff --git a/src/controllers/completion/sources/CommandSource.cpp b/src/controllers/completion/sources/CommandSource.cpp new file mode 100644 index 000000000..29deff932 --- /dev/null +++ b/src/controllers/completion/sources/CommandSource.cpp @@ -0,0 +1,107 @@ +#include "controllers/completion/sources/CommandSource.hpp" + +#include "Application.hpp" +#include "controllers/commands/Command.hpp" +#include "controllers/commands/CommandController.hpp" +#include "controllers/completion/sources/Helpers.hpp" +#include "providers/twitch/TwitchCommon.hpp" + +namespace chatterino::completion { + +namespace { + + void addCommand(const QString &command, std::vector &out) + { + if (command.startsWith('/') || command.startsWith('.')) + { + out.push_back({ + .name = command.mid(1), + .prefix = command.at(0), + }); + } + else + { + out.push_back({ + .name = command, + .prefix = "", + }); + } + } + +} // namespace + +CommandSource::CommandSource(std::unique_ptr strategy, + ActionCallback callback) + : strategy_(std::move(strategy)) + , callback_(std::move(callback)) +{ + this->initializeItems(); +} + +void CommandSource::update(const QString &query) +{ + this->output_.clear(); + if (this->strategy_) + { + this->strategy_->apply(this->items_, this->output_, query); + } +} + +void CommandSource::addToListModel(GenericListModel &model, + size_t maxCount) const +{ + addVecToListModel(this->output_, model, maxCount, + [this](const CommandItem &command) { + return std::make_unique( + nullptr, command.name, this->callback_); + }); +} + +void CommandSource::addToStringList(QStringList &list, size_t maxCount, + bool /* isFirstWord */) const +{ + addVecToStringList(this->output_, list, maxCount, + [](const CommandItem &command) { + return command.prefix + command.name + " "; + }); +} + +void CommandSource::initializeItems() +{ + std::vector commands; + +#ifdef CHATTERINO_HAVE_PLUGINS + for (const auto &command : getApp()->getCommands()->pluginCommands()) + { + addCommand(command, commands); + } +#endif + + // Custom Chatterino commands + for (const auto &command : getApp()->getCommands()->items) + { + addCommand(command.name, commands); + } + + // Default Chatterino commands + auto x = getApp()->getCommands()->getDefaultChatterinoCommandList(); + for (const auto &command : x) + { + addCommand(command, commands); + } + + // Default Twitch commands + for (const auto &command : TWITCH_DEFAULT_COMMANDS) + { + addCommand(command, commands); + } + + this->items_ = std::move(commands); +} + +const std::vector &CommandSource::output() const +{ + return this->output_; +} + +} // namespace chatterino::completion diff --git a/src/controllers/completion/sources/CommandSource.hpp b/src/controllers/completion/sources/CommandSource.hpp new file mode 100644 index 000000000..7c9e1017b --- /dev/null +++ b/src/controllers/completion/sources/CommandSource.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include "controllers/completion/sources/Source.hpp" +#include "controllers/completion/strategies/Strategy.hpp" + +#include + +#include +#include +#include + +namespace chatterino::completion { + +struct CommandItem { + QString name{}; + QString prefix{}; +}; + +class CommandSource : public Source +{ +public: + using ActionCallback = std::function; + using CommandStrategy = Strategy; + + /// @brief Initializes a source for CommandItems. + /// @param strategy Strategy to apply + /// @param callback ActionCallback to invoke upon InputCompletionItem selection. + /// See InputCompletionItem::action(). Can be nullptr. + CommandSource(std::unique_ptr strategy, + ActionCallback callback = nullptr); + + void update(const QString &query) override; + void addToListModel(GenericListModel &model, + size_t maxCount = 0) const override; + void addToStringList(QStringList &list, size_t maxCount = 0, + bool isFirstWord = false) const override; + + const std::vector &output() const; + +private: + void initializeItems(); + + std::unique_ptr strategy_; + ActionCallback callback_; + + std::vector items_{}; + std::vector output_{}; +}; + +} // namespace chatterino::completion diff --git a/src/controllers/completion/sources/EmoteSource.cpp b/src/controllers/completion/sources/EmoteSource.cpp new file mode 100644 index 000000000..f5d04c9e0 --- /dev/null +++ b/src/controllers/completion/sources/EmoteSource.cpp @@ -0,0 +1,157 @@ +#include "controllers/completion/sources/EmoteSource.hpp" + +#include "Application.hpp" +#include "controllers/accounts/AccountController.hpp" +#include "controllers/completion/sources/Helpers.hpp" +#include "providers/bttv/BttvEmotes.hpp" +#include "providers/emoji/Emojis.hpp" +#include "providers/ffz/FfzEmotes.hpp" +#include "providers/seventv/SeventvEmotes.hpp" +#include "providers/twitch/TwitchAccount.hpp" +#include "providers/twitch/TwitchChannel.hpp" +#include "providers/twitch/TwitchIrcServer.hpp" +#include "singletons/Emotes.hpp" + +namespace chatterino::completion { + +namespace { + + void addEmotes(std::vector &out, const EmoteMap &map, + const QString &providerName) + { + for (auto &&emote : map) + { + out.push_back({.emote = emote.second, + .searchName = emote.first.string, + .tabCompletionName = emote.first.string, + .displayName = emote.second->name.string, + .providerName = providerName, + .isEmoji = false}); + } + } + + void addEmojis(std::vector &out, + const std::vector &map) + { + for (const auto &emoji : map) + { + for (auto &&shortCode : emoji->shortCodes) + { + out.push_back( + {.emote = emoji->emote, + .searchName = shortCode, + .tabCompletionName = QStringLiteral(":%1:").arg(shortCode), + .displayName = shortCode, + .providerName = "Emoji", + .isEmoji = true}); + } + }; + } + +} // namespace + +EmoteSource::EmoteSource(const Channel *channel, + std::unique_ptr strategy, + ActionCallback callback) + : strategy_(std::move(strategy)) + , callback_(std::move(callback)) +{ + this->initializeFromChannel(channel); +} + +void EmoteSource::update(const QString &query) +{ + this->output_.clear(); + if (this->strategy_) + { + this->strategy_->apply(this->items_, this->output_, query); + } +} + +void EmoteSource::addToListModel(GenericListModel &model, size_t maxCount) const +{ + addVecToListModel(this->output_, model, maxCount, + [this](const EmoteItem &e) { + return std::make_unique( + e.emote, e.displayName + " - " + e.providerName, + this->callback_); + }); +} + +void EmoteSource::addToStringList(QStringList &list, size_t maxCount, + bool /* isFirstWord */) const +{ + addVecToStringList(this->output_, list, maxCount, [](const EmoteItem &e) { + return e.tabCompletionName + " "; + }); +} + +void EmoteSource::initializeFromChannel(const Channel *channel) +{ + auto *app = getApp(); + + std::vector emotes; + const auto *tc = dynamic_cast(channel); + // returns true also for special Twitch channels (/live, /mentions, /whispers, etc.) + if (channel->isTwitchChannel()) + { + if (auto user = app->getAccounts()->twitch.getCurrent()) + { + // Twitch Emotes available globally + auto emoteData = user->accessEmotes(); + addEmotes(emotes, emoteData->emotes, "Twitch Emote"); + + // Twitch Emotes available locally + auto localEmoteData = user->accessLocalEmotes(); + if ((tc != nullptr) && + localEmoteData->find(tc->roomId()) != localEmoteData->end()) + { + if (const auto *localEmotes = &localEmoteData->at(tc->roomId())) + { + addEmotes(emotes, *localEmotes, "Local Twitch Emotes"); + } + } + } + + if (tc) + { + // TODO extract "Channel {BetterTTV,7TV,FrankerFaceZ}" text into a #define. + if (auto bttv = tc->bttvEmotes()) + { + addEmotes(emotes, *bttv, "Channel BetterTTV"); + } + if (auto ffz = tc->ffzEmotes()) + { + addEmotes(emotes, *ffz, "Channel FrankerFaceZ"); + } + if (auto seventv = tc->seventvEmotes()) + { + addEmotes(emotes, *seventv, "Channel 7TV"); + } + } + + if (auto bttvG = app->getBttvEmotes()->emotes()) + { + addEmotes(emotes, *bttvG, "Global BetterTTV"); + } + if (auto ffzG = app->getFfzEmotes()->emotes()) + { + addEmotes(emotes, *ffzG, "Global FrankerFaceZ"); + } + if (auto seventvG = app->getSeventvEmotes()->globalEmotes()) + { + addEmotes(emotes, *seventvG, "Global 7TV"); + } + } + + addEmojis(emotes, app->getEmotes()->getEmojis()->getEmojis()); + + this->items_ = std::move(emotes); +} + +const std::vector &EmoteSource::output() const +{ + return this->output_; +} + +} // namespace chatterino::completion diff --git a/src/controllers/completion/sources/EmoteSource.hpp b/src/controllers/completion/sources/EmoteSource.hpp new file mode 100644 index 000000000..4f61fbc2b --- /dev/null +++ b/src/controllers/completion/sources/EmoteSource.hpp @@ -0,0 +1,63 @@ +#pragma once + +#include "common/Channel.hpp" +#include "controllers/completion/sources/Source.hpp" +#include "controllers/completion/strategies/Strategy.hpp" +#include "messages/Emote.hpp" + +#include + +#include +#include +#include + +namespace chatterino::completion { + +struct EmoteItem { + /// Emote image to show in input popup + EmotePtr emote{}; + /// Name to check completion queries against + QString searchName{}; + /// Name to insert into split input upon tab completing + QString tabCompletionName{}; + /// Display name within input popup + QString displayName{}; + /// Emote provider name for input popup + QString providerName{}; + /// Whether emote is emoji + bool isEmoji{}; +}; + +class EmoteSource : public Source +{ +public: + using ActionCallback = std::function; + using EmoteStrategy = Strategy; + + /// @brief Initializes a source for EmoteItems from the given channel + /// @param channel Channel to initialize emotes from + /// @param strategy Strategy to apply + /// @param callback ActionCallback to invoke upon InputCompletionItem selection. + /// See InputCompletionItem::action(). Can be nullptr. + EmoteSource(const Channel *channel, std::unique_ptr strategy, + ActionCallback callback = nullptr); + + void update(const QString &query) override; + void addToListModel(GenericListModel &model, + size_t maxCount = 0) const override; + void addToStringList(QStringList &list, size_t maxCount = 0, + bool isFirstWord = false) const override; + + const std::vector &output() const; + +private: + void initializeFromChannel(const Channel *channel); + + std::unique_ptr strategy_; + ActionCallback callback_; + + std::vector items_{}; + std::vector output_{}; +}; + +} // namespace chatterino::completion diff --git a/src/controllers/completion/sources/Helpers.hpp b/src/controllers/completion/sources/Helpers.hpp new file mode 100644 index 000000000..74198f14a --- /dev/null +++ b/src/controllers/completion/sources/Helpers.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include "widgets/listview/GenericListModel.hpp" + +#include + +#include + +namespace chatterino::completion { + +namespace { + + size_t sizeWithinLimit(size_t size, size_t limit) + { + if (limit == 0) + { + return size; + } + return std::min(size, limit); + } + +} // namespace + +template +void addVecToListModel(const std::vector &input, GenericListModel &model, + size_t maxCount, Mapper mapper) +{ + const size_t count = sizeWithinLimit(input.size(), maxCount); + model.reserve(model.rowCount() + count); + + for (size_t i = 0; i < count; ++i) + { + model.addItem(mapper(input[i])); + } +} + +template +void addVecToStringList(const std::vector &input, QStringList &list, + size_t maxCount, Mapper mapper) +{ + const size_t count = sizeWithinLimit(input.size(), maxCount); + list.reserve(list.count() + count); + + for (size_t i = 0; i < count; ++i) + { + list.push_back(mapper(input[i])); + } +} + +} // namespace chatterino::completion diff --git a/src/controllers/completion/sources/Source.hpp b/src/controllers/completion/sources/Source.hpp new file mode 100644 index 000000000..b78ce06ed --- /dev/null +++ b/src/controllers/completion/sources/Source.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include "widgets/listview/GenericListModel.hpp" +#include "widgets/splits/InputCompletionItem.hpp" + +#include + +#include +#include +#include + +namespace chatterino::completion { + +/// @brief A Source represents a source for generating completion suggestions. +/// +/// The source can be queried to update its suggestions and then write the completion +/// suggestions to a GenericListModel or QStringList depending on the consumer's +/// requirements. +/// +/// For example, consider providing emotes for completion. The Source instance +/// initialized with every available emote in the channel (including global +/// emotes). As the user updates their query by typing, the suggestions are +/// refined and the output model is updated. +class Source +{ +public: + virtual ~Source() = default; + + /// @brief Updates the internal completion suggestions for the given query + /// @param query Query to complete against + virtual void update(const QString &query) = 0; + + /// @brief Appends the internal completion suggestions to a GenericListModel + /// @param model GenericListModel to add suggestions to + /// @param maxCount Maximum number of suggestions. Zero indicates unlimited. + virtual void addToListModel(GenericListModel &model, + size_t maxCount = 0) const = 0; + + /// @brief Appends the internal completion suggestions to a QStringList + /// @param list QStringList to add suggestions to + /// @param maxCount Maximum number of suggestions. Zero indicates unlimited. + /// @param isFirstWord Whether the completion is the first word in the input + virtual void addToStringList(QStringList &list, size_t maxCount = 0, + bool isFirstWord = false) const = 0; +}; + +}; // namespace chatterino::completion diff --git a/src/controllers/completion/sources/UnifiedSource.cpp b/src/controllers/completion/sources/UnifiedSource.cpp new file mode 100644 index 000000000..a0f462ace --- /dev/null +++ b/src/controllers/completion/sources/UnifiedSource.cpp @@ -0,0 +1,77 @@ +#include "controllers/completion/sources/UnifiedSource.hpp" + +namespace chatterino::completion { + +UnifiedSource::UnifiedSource(std::vector> sources) + : sources_(std::move(sources)) +{ +} + +void UnifiedSource::update(const QString &query) +{ + // Update all sources + for (const auto &source : this->sources_) + { + source->update(query); + } +} + +void UnifiedSource::addToListModel(GenericListModel &model, + size_t maxCount) const +{ + if (maxCount == 0) + { + for (const auto &source : this->sources_) + { + source->addToListModel(model, 0); + } + return; + } + + // Make sure to only add maxCount elements in total. + int startingSize = model.rowCount(); + int used = 0; + + for (const auto &source : this->sources_) + { + source->addToListModel(model, maxCount - used); + // Calculate how many items have been added so far + used = model.rowCount() - startingSize; + if (used >= maxCount) + { + // Used up all of limit + break; + } + } +} + +void UnifiedSource::addToStringList(QStringList &list, size_t maxCount, + bool isFirstWord) const +{ + if (maxCount == 0) + { + for (const auto &source : this->sources_) + { + source->addToStringList(list, 0, isFirstWord); + } + return; + } + + // Make sure to only add maxCount elements in total. + int startingSize = list.size(); + int used = 0; + + for (const auto &source : this->sources_) + { + source->addToStringList(list, maxCount - used, isFirstWord); + // Calculate how many items have been added so far + used = list.size() - startingSize; + if (used >= maxCount) + { + // Used up all of limit + break; + } + } +} + +} // namespace chatterino::completion diff --git a/src/controllers/completion/sources/UnifiedSource.hpp b/src/controllers/completion/sources/UnifiedSource.hpp new file mode 100644 index 000000000..416a74364 --- /dev/null +++ b/src/controllers/completion/sources/UnifiedSource.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include "common/Channel.hpp" +#include "controllers/completion/sources/CommandSource.hpp" +#include "controllers/completion/sources/EmoteSource.hpp" +#include "controllers/completion/sources/Source.hpp" +#include "controllers/completion/sources/UserSource.hpp" + +#include +#include + +namespace chatterino::completion { + +class UnifiedSource : public Source +{ +public: + /// @brief Initializes a unified completion source. + /// @param sources Vector of sources to unify + UnifiedSource(std::vector> sources); + + void update(const QString &query) override; + void addToListModel(GenericListModel &model, + size_t maxCount = 0) const override; + void addToStringList(QStringList &list, size_t maxCount = 0, + bool isFirstWord = false) const override; + +private: + std::vector> sources_; +}; + +} // namespace chatterino::completion diff --git a/src/controllers/completion/sources/UserSource.cpp b/src/controllers/completion/sources/UserSource.cpp new file mode 100644 index 000000000..d700ef25a --- /dev/null +++ b/src/controllers/completion/sources/UserSource.cpp @@ -0,0 +1,82 @@ +#include "controllers/completion/sources/UserSource.hpp" + +#include "controllers/completion/sources/Helpers.hpp" +#include "providers/twitch/TwitchChannel.hpp" +#include "singletons/Settings.hpp" +#include "util/Helpers.hpp" + +namespace chatterino::completion { + +UserSource::UserSource(const Channel *channel, + std::unique_ptr strategy, + ActionCallback callback, bool prependAt) + : strategy_(std::move(strategy)) + , callback_(std::move(callback)) + , prependAt_(prependAt) +{ + this->initializeFromChannel(channel); +} + +void UserSource::update(const QString &query) +{ + this->output_.clear(); + if (this->strategy_) + { + this->strategy_->apply(this->items_, this->output_, query); + } +} + +void UserSource::addToListModel(GenericListModel &model, size_t maxCount) const +{ + addVecToListModel(this->output_, model, maxCount, + [this](const UserItem &user) { + return std::make_unique( + nullptr, user.second, this->callback_); + }); +} + +void UserSource::addToStringList(QStringList &list, size_t maxCount, + bool isFirstWord) const +{ + bool mentionComma = getSettings()->mentionUsersWithComma; + addVecToStringList(this->output_, list, maxCount, + [this, isFirstWord, mentionComma](const UserItem &user) { + const auto userMention = formatUserMention( + user.second, isFirstWord, mentionComma); + QString strTemplate = this->prependAt_ + ? QStringLiteral("@%1 ") + : QStringLiteral("%1 "); + return strTemplate.arg(userMention); + }); +} + +void UserSource::initializeFromChannel(const Channel *channel) +{ + const auto *tc = dynamic_cast(channel); + if (!tc) + { + return; + } + + this->items_ = tc->accessChatters()->all(); + + if (getSettings()->alwaysIncludeBroadcasterInUserCompletions) + { + auto it = std::find_if(this->items_.begin(), this->items_.end(), + [tc](const UserItem &user) { + return user.first == tc->getName(); + }); + + if (it == this->items_.end()) + { + this->items_.emplace_back(tc->getName(), tc->getDisplayName()); + } + } +} + +const std::vector &UserSource::output() const +{ + return this->output_; +} + +} // namespace chatterino::completion diff --git a/src/controllers/completion/sources/UserSource.hpp b/src/controllers/completion/sources/UserSource.hpp new file mode 100644 index 000000000..316b33c7b --- /dev/null +++ b/src/controllers/completion/sources/UserSource.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include "common/Channel.hpp" +#include "controllers/completion/sources/Source.hpp" +#include "controllers/completion/strategies/Strategy.hpp" + +#include + +#include +#include +#include +#include + +namespace chatterino::completion { + +using UserItem = std::pair; + +class UserSource : public Source +{ +public: + using ActionCallback = std::function; + using UserStrategy = Strategy; + + /// @brief Initializes a source for UserItems from the given channel. + /// @param channel Channel to initialize users from. Must be a TwitchChannel + /// or completion is a no-op. + /// @param strategy Strategy to apply + /// @param callback ActionCallback to invoke upon InputCompletionItem selection. + /// See InputCompletionItem::action(). Can be nullptr. + /// @param prependAt Whether to prepend @ to string completion suggestions. + UserSource(const Channel *channel, std::unique_ptr strategy, + ActionCallback callback = nullptr, bool prependAt = true); + + void update(const QString &query) override; + void addToListModel(GenericListModel &model, + size_t maxCount = 0) const override; + void addToStringList(QStringList &list, size_t maxCount = 0, + bool isFirstWord = false) const override; + + const std::vector &output() const; + +private: + void initializeFromChannel(const Channel *channel); + + std::unique_ptr strategy_; + ActionCallback callback_; + bool prependAt_; + + std::vector items_{}; + std::vector output_{}; +}; + +} // namespace chatterino::completion diff --git a/src/controllers/completion/strategies/ClassicEmoteStrategy.cpp b/src/controllers/completion/strategies/ClassicEmoteStrategy.cpp new file mode 100644 index 000000000..1a427483a --- /dev/null +++ b/src/controllers/completion/strategies/ClassicEmoteStrategy.cpp @@ -0,0 +1,85 @@ +#include "controllers/completion/strategies/ClassicEmoteStrategy.hpp" + +#include "singletons/Settings.hpp" +#include "util/Helpers.hpp" + +namespace chatterino::completion { + +void ClassicEmoteStrategy::apply(const std::vector &items, + std::vector &output, + const QString &query) const +{ + QString normalizedQuery = query; + if (normalizedQuery.startsWith(':')) + { + normalizedQuery = normalizedQuery.mid(1); + } + + // First pass: filter by contains match + for (const auto &item : items) + { + if (item.searchName.contains(normalizedQuery, Qt::CaseInsensitive)) + { + output.push_back(item); + } + } + + // Second pass: if there is an exact match, put that emote first + for (size_t i = 1; i < output.size(); i++) + { + auto emoteText = output.at(i).searchName; + + // test for match or match with colon at start for emotes like ":)" + if (emoteText.compare(normalizedQuery, Qt::CaseInsensitive) == 0 || + emoteText.compare(":" + normalizedQuery, Qt::CaseInsensitive) == 0) + { + auto emote = output[i]; + output.erase(output.begin() + int(i)); + output.insert(output.begin(), emote); + break; + } + } +} + +struct CompletionEmoteOrder { + bool operator()(const EmoteItem &a, const EmoteItem &b) const + { + return compareEmoteStrings(a.searchName, b.searchName); + } +}; + +void ClassicTabEmoteStrategy::apply(const std::vector &items, + std::vector &output, + const QString &query) const +{ + bool emojiOnly = false; + QString normalizedQuery = query; + if (normalizedQuery.startsWith(':')) + { + normalizedQuery = normalizedQuery.mid(1); + // tab completion with : prefix should do emojis only + emojiOnly = true; + } + + std::set emotes; + + for (const auto &item : items) + { + if (emojiOnly ^ item.isEmoji) + { + continue; + } + + if (startsWithOrContains(item.searchName, normalizedQuery, + Qt::CaseInsensitive, + getSettings()->prefixOnlyEmoteCompletion)) + { + emotes.insert(item); + } + } + + output.reserve(emotes.size()); + output.assign(emotes.begin(), emotes.end()); +} + +} // namespace chatterino::completion diff --git a/src/controllers/completion/strategies/ClassicEmoteStrategy.hpp b/src/controllers/completion/strategies/ClassicEmoteStrategy.hpp new file mode 100644 index 000000000..d231c8ac1 --- /dev/null +++ b/src/controllers/completion/strategies/ClassicEmoteStrategy.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include "controllers/completion/sources/EmoteSource.hpp" +#include "controllers/completion/strategies/Strategy.hpp" + +namespace chatterino::completion { + +class ClassicEmoteStrategy : public Strategy +{ + void apply(const std::vector &items, + std::vector &output, + const QString &query) const override; +}; + +class ClassicTabEmoteStrategy : public Strategy +{ + void apply(const std::vector &items, + std::vector &output, + const QString &query) const override; +}; + +} // namespace chatterino::completion diff --git a/src/controllers/completion/strategies/ClassicUserStrategy.cpp b/src/controllers/completion/strategies/ClassicUserStrategy.cpp new file mode 100644 index 000000000..1f002ed59 --- /dev/null +++ b/src/controllers/completion/strategies/ClassicUserStrategy.cpp @@ -0,0 +1,23 @@ +#include "controllers/completion/strategies/ClassicUserStrategy.hpp" + +namespace chatterino::completion { + +void ClassicUserStrategy::apply(const std::vector &items, + std::vector &output, + const QString &query) const +{ + QString lowerQuery = query.toLower(); + if (lowerQuery.startsWith('@')) + { + lowerQuery = lowerQuery.mid(1); + } + + for (const auto &item : items) + { + if (item.first.startsWith(lowerQuery)) + { + output.push_back(item); + } + } +} +} // namespace chatterino::completion diff --git a/src/controllers/completion/strategies/ClassicUserStrategy.hpp b/src/controllers/completion/strategies/ClassicUserStrategy.hpp new file mode 100644 index 000000000..7575d1271 --- /dev/null +++ b/src/controllers/completion/strategies/ClassicUserStrategy.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include "controllers/completion/sources/UserSource.hpp" +#include "controllers/completion/strategies/Strategy.hpp" + +namespace chatterino::completion { + +class ClassicUserStrategy : public Strategy +{ + void apply(const std::vector &items, + std::vector &output, + const QString &query) const override; +}; + +} // namespace chatterino::completion diff --git a/src/controllers/completion/strategies/CommandStrategy.cpp b/src/controllers/completion/strategies/CommandStrategy.cpp new file mode 100644 index 000000000..9edf4e405 --- /dev/null +++ b/src/controllers/completion/strategies/CommandStrategy.cpp @@ -0,0 +1,45 @@ +#include "controllers/completion/strategies/CommandStrategy.hpp" + +namespace chatterino::completion { + +QString normalizeQuery(const QString &query) +{ + if (query.startsWith('/') || query.startsWith('.')) + { + return query.mid(1); + } + + return query; +} + +CommandStrategy::CommandStrategy(bool startsWithOnly) + : startsWithOnly_(startsWithOnly) +{ +} + +void CommandStrategy::apply(const std::vector &items, + std::vector &output, + const QString &query) const +{ + QString normalizedQuery = normalizeQuery(query); + + if (startsWithOnly_) + { + std::copy_if(items.begin(), items.end(), + std::back_insert_iterator(output), + [&normalizedQuery](const CommandItem &item) { + return item.name.startsWith(normalizedQuery, + Qt::CaseInsensitive); + }); + } + else + { + std::copy_if( + items.begin(), items.end(), std::back_insert_iterator(output), + [&normalizedQuery](const CommandItem &item) { + return item.name.contains(normalizedQuery, Qt::CaseInsensitive); + }); + } +}; + +} // namespace chatterino::completion diff --git a/src/controllers/completion/strategies/CommandStrategy.hpp b/src/controllers/completion/strategies/CommandStrategy.hpp new file mode 100644 index 000000000..4f9ca1266 --- /dev/null +++ b/src/controllers/completion/strategies/CommandStrategy.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include "controllers/completion/sources/CommandSource.hpp" +#include "controllers/completion/strategies/Strategy.hpp" + +namespace chatterino::completion { + +class CommandStrategy : public Strategy +{ +public: + CommandStrategy(bool startsWithOnly); + + void apply(const std::vector &items, + std::vector &output, + const QString &query) const override; + +private: + bool startsWithOnly_; +}; + +} // namespace chatterino::completion diff --git a/src/controllers/completion/strategies/SmartEmoteStrategy.cpp b/src/controllers/completion/strategies/SmartEmoteStrategy.cpp new file mode 100644 index 000000000..aa6e43127 --- /dev/null +++ b/src/controllers/completion/strategies/SmartEmoteStrategy.cpp @@ -0,0 +1,204 @@ +#include "controllers/completion/strategies/SmartEmoteStrategy.hpp" + +#include "common/QLogging.hpp" +#include "controllers/completion/sources/EmoteSource.hpp" +#include "singletons/Settings.hpp" +#include "util/Helpers.hpp" + +#include + +#include + +namespace chatterino::completion { +namespace { + /** + * @brief This function calculates the "cost" of the changes that need to + * be done to the query to make it the value. + * + * By default an emote with more differences in character casing from the + * query will get a higher cost, each additional letter also increases cost. + * + * @param prioritizeUpper If set, then differences in casing don't matter, but + * instead the more lowercase letters an emote contains, the higher cost it + * will get. Additional letters also increase the cost in this mode. + * + * @return How different the emote is from query. Values in the range [-10, + * \infty]. + */ + int costOfEmote(const QString &query, const QString &emote, + bool prioritizeUpper) + { + int score = 0; + + if (prioritizeUpper) + { + // We are in case 3, push 'more uppercase' emotes to the top + for (const auto i : emote) + { + score += int(!i.isUpper()); + } + } + else + { + // Push more matching emotes to the top + int len = std::min(emote.size(), query.size()); + for (int i = 0; i < len; i++) + { + // Different casing gets a higher cost score + score += query.at(i).isUpper() ^ emote.at(i).isUpper(); + } + } + // No case differences, put this at the top + if (score == 0) + { + score = -10; + } + + auto diff = emote.size() - query.size(); + if (diff > 0) + { + // Case changes are way less changes to the user compared to adding characters + score += diff * 100; + } + return score; + }; + + // This contains the brains of emote tab completion. Updates output to sorted completions. + // Ensure that the query string is already normalized, that is doesn't have a leading ':' + // matchingFunction is used for testing if the emote should be included in the search. + void completeEmotes( + const std::vector &items, std::vector &output, + const QString &query, bool ignoreColonForCost, + const std::function + &matchingFunction) + { + // Given these emotes: pajaW, PAJAW + // There are a few cases of input: + // 1. "pajaw" expect {pajaW, PAJAW} - no uppercase characters, do regular case insensitive search + // 2. "PA" expect {PAJAW} - uppercase characters, case sensitive search gives results + // 3. "Pajaw" expect {PAJAW, pajaW} - case sensitive search doesn't give results, need to use sorting + // 4. "NOTHING" expect {} - no results + // 5. "nothing" expect {} - same as 4 but first search is case insensitive + + // Check if the query contains any uppercase characters + // This tells us if we're in case 1 or 5 vs all others + bool haveUpper = + std::any_of(query.begin(), query.end(), [](const QChar &c) { + return c.isUpper(); + }); + + // First search, for case 1 it will be case insensitive, + // for cases 2, 3 and 4 it will be case sensitive + for (const auto &item : items) + { + if (matchingFunction( + item, query, + haveUpper ? Qt::CaseSensitive : Qt::CaseInsensitive)) + { + output.push_back(item); + } + } + + // if case 3: then true; false otherwise + bool prioritizeUpper = false; + + // No results from search + if (output.empty()) + { + if (!haveUpper) + { + // Optimisation: First search was case insensitive, but we found nothing + // There is nothing to be found: case 5. + return; + } + // Case sensitive search from case 2 found nothing, therefore we can + // only be in case 3 or 4. + + prioritizeUpper = true; + // Run the search again but this time without case sensitivity + for (const auto &item : items) + { + if (matchingFunction(item, query, Qt::CaseInsensitive)) + { + output.push_back(item); + } + } + if (output.empty()) + { + // The second search found nothing, so don't even try to sort: case 4 + return; + } + } + + std::sort(output.begin(), output.end(), + [query, prioritizeUpper, ignoreColonForCost]( + const EmoteItem &a, const EmoteItem &b) -> bool { + auto tempA = a.searchName; + auto tempB = b.searchName; + if (ignoreColonForCost && tempA.startsWith(":")) + { + tempA = tempA.mid(1); + } + if (ignoreColonForCost && tempB.startsWith(":")) + { + tempB = tempB.mid(1); + } + + auto costA = costOfEmote(query, tempA, prioritizeUpper); + auto costB = costOfEmote(query, tempB, prioritizeUpper); + if (costA == costB) + { + // Case difference and length came up tied for (a, b), break the tie + return QString::compare(tempA, tempB, + Qt::CaseInsensitive) < 0; + } + + return costA < costB; + }); + } +} // namespace + +void SmartEmoteStrategy::apply(const std::vector &items, + std::vector &output, + const QString &query) const +{ + QString normalizedQuery = query; + bool ignoreColonForCost = false; + if (normalizedQuery.startsWith(':')) + { + normalizedQuery = normalizedQuery.mid(1); + ignoreColonForCost = true; + } + completeEmotes(items, output, normalizedQuery, ignoreColonForCost, + [](const EmoteItem &left, const QString &right, + Qt::CaseSensitivity caseHandling) { + return left.searchName.contains(right, caseHandling); + }); +} + +void SmartTabEmoteStrategy::apply(const std::vector &items, + std::vector &output, + const QString &query) const +{ + bool emojiOnly = false; + QString normalizedQuery = query; + if (normalizedQuery.startsWith(':')) + { + normalizedQuery = normalizedQuery.mid(1); + // tab completion with : prefix should do emojis only + emojiOnly = true; + } + completeEmotes(items, output, normalizedQuery, false, + [emojiOnly](const EmoteItem &left, const QString &right, + Qt::CaseSensitivity caseHandling) -> bool { + if (emojiOnly ^ left.isEmoji) + { + return false; + } + return startsWithOrContains( + left.searchName, right, caseHandling, + getSettings()->prefixOnlyEmoteCompletion); + }); +} + +} // namespace chatterino::completion diff --git a/src/controllers/completion/strategies/SmartEmoteStrategy.hpp b/src/controllers/completion/strategies/SmartEmoteStrategy.hpp new file mode 100644 index 000000000..365e106b0 --- /dev/null +++ b/src/controllers/completion/strategies/SmartEmoteStrategy.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include "controllers/completion/sources/EmoteSource.hpp" +#include "controllers/completion/strategies/Strategy.hpp" + +namespace chatterino::completion { + +class SmartEmoteStrategy : public Strategy +{ + void apply(const std::vector &items, + std::vector &output, + const QString &query) const override; +}; + +class SmartTabEmoteStrategy : public Strategy +{ + void apply(const std::vector &items, + std::vector &output, + const QString &query) const override; +}; + +} // namespace chatterino::completion diff --git a/src/controllers/completion/strategies/Strategy.hpp b/src/controllers/completion/strategies/Strategy.hpp new file mode 100644 index 000000000..d10569911 --- /dev/null +++ b/src/controllers/completion/strategies/Strategy.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include + +#include + +namespace chatterino::completion { + +/// @brief An Strategy implements ordering and filtering of completion items in +/// response to a query. +/// @tparam T Type of items to consider +template +class Strategy +{ +public: + virtual ~Strategy() = default; + + /// @brief Applies the strategy, taking the input items and storing the + /// appropriate output items in the desired order. + /// @param items Input items to consider + /// @param output Output vector for items + /// @param query Completion query + virtual void apply(const std::vector &items, std::vector &output, + const QString &query) const = 0; +}; + +} // namespace chatterino::completion diff --git a/src/controllers/filters/FilterModel.cpp b/src/controllers/filters/FilterModel.cpp index 68d91ec4f..5ce793eb6 100644 --- a/src/controllers/filters/FilterModel.cpp +++ b/src/controllers/filters/FilterModel.cpp @@ -1,6 +1,7 @@ -#include "FilterModel.hpp" +#include "controllers/filters/FilterModel.hpp" #include "Application.hpp" +#include "controllers/filters/FilterRecord.hpp" #include "singletons/Settings.hpp" #include "util/StandardItemHelper.hpp" diff --git a/src/controllers/filters/FilterModel.hpp b/src/controllers/filters/FilterModel.hpp index b900ffcc5..3b44c7acb 100644 --- a/src/controllers/filters/FilterModel.hpp +++ b/src/controllers/filters/FilterModel.hpp @@ -1,12 +1,14 @@ #pragma once +#include "common/SignalVectorModel.hpp" + #include -#include "common/SignalVectorModel.hpp" -#include "controllers/filters/FilterRecord.hpp" - namespace chatterino { +class FilterRecord; +using FilterRecordPtr = std::shared_ptr; + class FilterModel : public SignalVectorModel { public: @@ -14,13 +16,12 @@ public: protected: // turn a vector item into a model row - virtual FilterRecordPtr getItemFromRow( - std::vector &row, - const FilterRecordPtr &original) override; + FilterRecordPtr getItemFromRow(std::vector &row, + const FilterRecordPtr &original) override; // turns a row in the model into a vector item - virtual void getRowFromItem(const FilterRecordPtr &item, - std::vector &row) override; + void getRowFromItem(const FilterRecordPtr &item, + std::vector &row) override; }; } // namespace chatterino diff --git a/src/controllers/filters/FilterRecord.cpp b/src/controllers/filters/FilterRecord.cpp new file mode 100644 index 000000000..409aa64fa --- /dev/null +++ b/src/controllers/filters/FilterRecord.cpp @@ -0,0 +1,73 @@ +#include "controllers/filters/FilterRecord.hpp" + +#include "controllers/filters/lang/Filter.hpp" + +namespace chatterino { + +static std::unique_ptr buildFilter(const QString &filterText) +{ + using namespace filters; + auto result = Filter::fromString(filterText); + if (std::holds_alternative(result)) + { + auto filter = + std::make_unique(std::move(std::get(result))); + + if (filter->returnType() != Type::Bool) + { + // Only accept Bool results + return nullptr; + } + + return filter; + } + + return nullptr; +} + +FilterRecord::FilterRecord(QString name, QString filter) + : FilterRecord(std::move(name), std::move(filter), QUuid::createUuid()) +{ +} + +FilterRecord::FilterRecord(QString name, QString filter, const QUuid &id) + : name_(std::move(name)) + , filterText_(std::move(filter)) + , id_(id) + , filter_(buildFilter(this->filterText_)) +{ +} + +const QString &FilterRecord::getName() const +{ + return this->name_; +} + +const QString &FilterRecord::getFilter() const +{ + return this->filterText_; +} + +const QUuid &FilterRecord::getId() const +{ + return this->id_; +} + +bool FilterRecord::valid() const +{ + return this->filter_ != nullptr; +} + +bool FilterRecord::filter(const filters::ContextMap &context) const +{ + assert(this->valid()); + return this->filter_->execute(context).toBool(); +} + +bool FilterRecord::operator==(const FilterRecord &other) const +{ + return std::tie(this->name_, this->filter_, this->id_) == + std::tie(other.name_, other.filter_, other.id_); +} + +} // namespace chatterino diff --git a/src/controllers/filters/FilterRecord.hpp b/src/controllers/filters/FilterRecord.hpp index 5f0eb750c..c5f120040 100644 --- a/src/controllers/filters/FilterRecord.hpp +++ b/src/controllers/filters/FilterRecord.hpp @@ -1,15 +1,13 @@ #pragma once -#include "util/RapidJsonSerializeQString.hpp" +#include "controllers/filters/lang/Filter.hpp" #include "util/RapidjsonHelpers.hpp" +#include "util/RapidJsonSerializeQString.hpp" -#include "controllers/filters/parser/FilterParser.hpp" -#include "controllers/filters/parser/Types.hpp" - +#include #include #include #include -#include #include @@ -18,59 +16,28 @@ namespace chatterino { class FilterRecord { public: - bool operator==(const FilterRecord &other) const - { - return std::tie(this->name_, this->filter_, this->id_) == - std::tie(other.name_, other.filter_, other.id_); - } + FilterRecord(QString name, QString filter); - FilterRecord(const QString &name, const QString &filter) - : name_(name) - , filter_(filter) - , id_(QUuid::createUuid()) - , parser_(std::make_unique(filter)) - { - } + FilterRecord(QString name, QString filter, const QUuid &id); - FilterRecord(const QString &name, const QString &filter, const QUuid &id) - : name_(name) - , filter_(filter) - , id_(id) - , parser_(std::make_unique(filter)) - { - } + const QString &getName() const; - const QString &getName() const - { - return this->name_; - } + const QString &getFilter() const; - const QString &getFilter() const - { - return this->filter_; - } + const QUuid &getId() const; - const QUuid &getId() const - { - return this->id_; - } + bool valid() const; - bool valid() const - { - return this->parser_->valid(); - } + bool filter(const filters::ContextMap &context) const; - bool filter(const filterparser::ContextMap &context) const - { - return this->parser_->execute(context); - } + bool operator==(const FilterRecord &other) const; private: - QString name_; - QString filter_; - QUuid id_; + const QString name_; + const QString filterText_; + const QUuid id_; - std::unique_ptr parser_; + const std::unique_ptr filter_; }; using FilterRecordPtr = std::shared_ptr; diff --git a/src/controllers/filters/FilterSet.cpp b/src/controllers/filters/FilterSet.cpp new file mode 100644 index 000000000..a64acdf76 --- /dev/null +++ b/src/controllers/filters/FilterSet.cpp @@ -0,0 +1,83 @@ +#include "controllers/filters/FilterSet.hpp" + +#include "controllers/filters/FilterRecord.hpp" +#include "singletons/Settings.hpp" + +namespace chatterino { + +FilterSet::FilterSet() +{ + this->listener_ = + getSettings()->filterRecords.delayedItemsChanged.connect([this] { + this->reloadFilters(); + }); +} + +FilterSet::FilterSet(const QList &filterIds) +{ + auto filters = getSettings()->filterRecords.readOnly(); + for (const auto &f : *filters) + { + if (filterIds.contains(f->getId())) + { + this->filters_.insert(f->getId(), f); + } + } + + this->listener_ = + getSettings()->filterRecords.delayedItemsChanged.connect([this] { + this->reloadFilters(); + }); +} + +FilterSet::~FilterSet() +{ + this->listener_.disconnect(); +} + +bool FilterSet::filter(const MessagePtr &m, ChannelPtr channel) const +{ + if (this->filters_.size() == 0) + { + return true; + } + + filters::ContextMap context = filters::buildContextMap(m, channel.get()); + for (const auto &f : this->filters_.values()) + { + if (!f->valid() || !f->filter(context)) + { + return false; + } + } + + return true; +} + +const QList FilterSet::filterIds() const +{ + return this->filters_.keys(); +} + +void FilterSet::reloadFilters() +{ + auto filters = getSettings()->filterRecords.readOnly(); + for (const auto &key : this->filters_.keys()) + { + bool found = false; + for (const auto &f : *filters) + { + if (f->getId() == key) + { + found = true; + this->filters_.insert(key, f); + } + } + if (!found) + { + this->filters_.remove(key); + } + } +} + +} // namespace chatterino diff --git a/src/controllers/filters/FilterSet.hpp b/src/controllers/filters/FilterSet.hpp index 687f79964..dd1501664 100644 --- a/src/controllers/filters/FilterSet.hpp +++ b/src/controllers/filters/FilterSet.hpp @@ -1,86 +1,37 @@ #pragma once -#include "controllers/filters/FilterRecord.hpp" -#include "singletons/Settings.hpp" +#include +#include +#include +#include + +#include namespace chatterino { +class FilterRecord; +using FilterRecordPtr = std::shared_ptr; +struct Message; +class Channel; +using MessagePtr = std::shared_ptr; +using ChannelPtr = std::shared_ptr; + class FilterSet { public: - FilterSet() - { - this->listener_ = - getCSettings().filterRecords.delayedItemsChanged.connect([this] { - this->reloadFilters(); - }); - } + FilterSet(); + FilterSet(const QList &filterIds); - FilterSet(const QList &filterIds) - { - auto filters = getCSettings().filterRecords.readOnly(); - for (const auto &f : *filters) - { - if (filterIds.contains(f->getId())) - this->filters_.insert(f->getId(), f); - } + ~FilterSet(); - this->listener_ = - getCSettings().filterRecords.delayedItemsChanged.connect([this] { - this->reloadFilters(); - }); - } - - ~FilterSet() - { - this->listener_.disconnect(); - } - - bool filter(const MessagePtr &m, ChannelPtr channel) const - { - if (this->filters_.size() == 0) - return true; - - filterparser::ContextMap context = - filterparser::buildContextMap(m, channel.get()); - for (const auto &f : this->filters_.values()) - { - if (!f->valid() || !f->filter(context)) - return false; - } - - return true; - } - - const QList filterIds() const - { - return this->filters_.keys(); - } + bool filter(const MessagePtr &m, ChannelPtr channel) const; + const QList filterIds() const; private: QMap filters_; pajlada::Signals::Connection listener_; - void reloadFilters() - { - auto filters = getCSettings().filterRecords.readOnly(); - for (const auto &key : this->filters_.keys()) - { - bool found = false; - for (const auto &f : *filters) - { - if (f->getId() == key) - { - found = true; - this->filters_.insert(key, f); - } - } - if (!found) - { - this->filters_.remove(key); - } - } - } + void reloadFilters(); }; using FilterSetPtr = std::shared_ptr; diff --git a/src/controllers/filters/lang/Filter.cpp b/src/controllers/filters/lang/Filter.cpp new file mode 100644 index 000000000..2cda8a13b --- /dev/null +++ b/src/controllers/filters/lang/Filter.cpp @@ -0,0 +1,182 @@ +#include "controllers/filters/lang/Filter.hpp" + +#include "Application.hpp" +#include "common/Channel.hpp" +#include "controllers/filters/lang/FilterParser.hpp" +#include "messages/Message.hpp" +#include "providers/twitch/TwitchBadge.hpp" +#include "providers/twitch/TwitchChannel.hpp" +#include "providers/twitch/TwitchIrcServer.hpp" + +namespace chatterino::filters { + +ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel) +{ + auto watchingChannel = getApp()->getTwitch()->getWatchingChannel().get(); + + /* + * Looking to add a new identifier to filters? Here's what to do: + * 1. Update validIdentifiersMap in Tokenizer.hpp + * 2. Add the identifier to the list below + * 3. Add the type of the identifier to MESSAGE_TYPING_CONTEXT in Filter.hpp + * 4. Add the value for the identifier to the ContextMap returned by this function + * + * List of identifiers: + * + * author.badges + * author.color + * author.name + * author.no_color + * author.subbed + * author.sub_length + * + * channel.name + * channel.watching + * + * flags.highlighted + * flags.points_redeemed + * flags.sub_message + * flags.system_message + * flags.reward_message + * flags.first_message + * flags.elevated_message + * flags.cheer_message + * flags.whisper + * flags.reply + * flags.automod + * flags.restricted + * flags.monitored + * + * message.content + * message.length + * + * reward.title + * reward.cost + * reward.id + */ + + using MessageFlag = chatterino::MessageFlag; + + QStringList badges; + badges.reserve(m->badges.size()); + for (const auto &e : m->badges) + { + badges << e.key_; + } + + bool watching = !watchingChannel->getName().isEmpty() && + watchingChannel->getName().compare( + m->channelName, Qt::CaseInsensitive) == 0; + + bool subscribed = false; + int subLength = 0; + for (const auto &subBadge : {"subscriber", "founder"}) + { + if (!badges.contains(subBadge)) + { + continue; + } + subscribed = true; + if (m->badgeInfos.find(subBadge) != m->badgeInfos.end()) + { + subLength = m->badgeInfos.at(subBadge).toInt(); + } + } + ContextMap vars = { + {"author.badges", std::move(badges)}, + {"author.color", m->usernameColor}, + {"author.name", m->displayName}, + {"author.no_color", !m->usernameColor.isValid()}, + {"author.subbed", subscribed}, + {"author.sub_length", subLength}, + + {"channel.name", m->channelName}, + {"channel.watching", watching}, + + {"flags.action", m->flags.has(MessageFlag::Action)}, + {"flags.highlighted", m->flags.has(MessageFlag::Highlighted)}, + {"flags.points_redeemed", m->flags.has(MessageFlag::RedeemedHighlight)}, + {"flags.sub_message", m->flags.has(MessageFlag::Subscription)}, + {"flags.system_message", m->flags.has(MessageFlag::System)}, + {"flags.reward_message", + m->flags.has(MessageFlag::RedeemedChannelPointReward)}, + {"flags.first_message", m->flags.has(MessageFlag::FirstMessage)}, + {"flags.elevated_message", m->flags.has(MessageFlag::ElevatedMessage)}, + {"flags.hype_chat", m->flags.has(MessageFlag::ElevatedMessage)}, + {"flags.cheer_message", m->flags.has(MessageFlag::CheerMessage)}, + {"flags.whisper", m->flags.has(MessageFlag::Whisper)}, + {"flags.reply", m->flags.has(MessageFlag::ReplyMessage)}, + {"flags.automod", m->flags.has(MessageFlag::AutoMod)}, + {"flags.restricted", m->flags.has(MessageFlag::RestrictedMessage)}, + {"flags.monitored", m->flags.has(MessageFlag::MonitoredMessage)}, + + {"message.content", m->messageText}, + {"message.length", m->messageText.length()}, + }; + { + auto *tc = dynamic_cast(channel); + if (channel && !channel->isEmpty() && tc) + { + vars["channel.live"] = tc->isLive(); + } + else + { + vars["channel.live"] = false; + } + } + if (m->reward != nullptr) + { + vars["reward.title"] = m->reward->title; + vars["reward.cost"] = m->reward->cost; + vars["reward.id"] = m->reward->id; + } + else + { + vars["reward.title"] = ""; + vars["reward.cost"] = -1; + vars["reward.id"] = ""; + } + return vars; +} + +FilterResult Filter::fromString(const QString &str) +{ + FilterParser parser(str); + + if (parser.valid()) + { + auto exp = parser.release(); + auto typ = parser.returnType(); + return Filter(std::move(exp), typ); + } + + return FilterError{parser.errors().join("\n")}; +} + +Filter::Filter(ExpressionPtr expression, Type returnType) + : expression_(std::move(expression)) + , returnType_(returnType) +{ +} + +Type Filter::returnType() const +{ + return this->returnType_; +} + +QVariant Filter::execute(const ContextMap &context) const +{ + return this->expression_->execute(context); +} + +QString Filter::filterString() const +{ + return this->expression_->filterString(); +} + +QString Filter::debugString(const TypingContext &context) const +{ + return this->expression_->debug(context); +} + +} // namespace chatterino::filters diff --git a/src/controllers/filters/lang/Filter.hpp b/src/controllers/filters/lang/Filter.hpp new file mode 100644 index 000000000..7a0f44805 --- /dev/null +++ b/src/controllers/filters/lang/Filter.hpp @@ -0,0 +1,84 @@ +#pragma once + +#include "controllers/filters/lang/expressions/Expression.hpp" +#include "controllers/filters/lang/Types.hpp" + +#include + +#include +#include + +namespace chatterino { + +class Channel; +struct Message; +using MessagePtr = std::shared_ptr; + +} // namespace chatterino + +namespace chatterino::filters { + +// MESSAGE_TYPING_CONTEXT maps filter variables to their expected type at evaluation. +// For example, flags.highlighted is a boolean variable, so it is marked as Type::Bool +// below. These variable types will be used to check whether a filter "makes sense", +// i.e. if all the variables and operators being used have compatible types. +static const QMap MESSAGE_TYPING_CONTEXT = { + {"author.badges", Type::StringList}, + {"author.color", Type::Color}, + {"author.name", Type::String}, + {"author.no_color", Type::Bool}, + {"author.subbed", Type::Bool}, + {"author.sub_length", Type::Int}, + {"channel.name", Type::String}, + {"channel.watching", Type::Bool}, + {"channel.live", Type::Bool}, + {"flags.action", Type::Bool}, + {"flags.highlighted", Type::Bool}, + {"flags.points_redeemed", Type::Bool}, + {"flags.sub_message", Type::Bool}, + {"flags.system_message", Type::Bool}, + {"flags.reward_message", Type::Bool}, + {"flags.first_message", Type::Bool}, + {"flags.elevated_message", Type::Bool}, + {"flags.hype_chat", Type::Bool}, + {"flags.cheer_message", Type::Bool}, + {"flags.whisper", Type::Bool}, + {"flags.reply", Type::Bool}, + {"flags.automod", Type::Bool}, + {"flags.restricted", Type::Bool}, + {"flags.monitored", Type::Bool}, + {"message.content", Type::String}, + {"message.length", Type::Int}, + {"reward.title", Type::String}, + {"reward.cost", Type::Int}, + {"reward.id", Type::String}, +}; + +ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel); + +class Filter; +struct FilterError { + QString message; +}; + +using FilterResult = std::variant; + +class Filter +{ +public: + static FilterResult fromString(const QString &str); + + Type returnType() const; + QVariant execute(const ContextMap &context) const; + + QString filterString() const; + QString debugString(const TypingContext &context) const; + +private: + Filter(ExpressionPtr expression, Type returnType); + + ExpressionPtr expression_; + Type returnType_; +}; + +} // namespace chatterino::filters diff --git a/src/controllers/filters/parser/FilterParser.cpp b/src/controllers/filters/lang/FilterParser.cpp similarity index 70% rename from src/controllers/filters/parser/FilterParser.cpp rename to src/controllers/filters/lang/FilterParser.cpp index f4c80efa6..dddbd164a 100644 --- a/src/controllers/filters/parser/FilterParser.cpp +++ b/src/controllers/filters/lang/FilterParser.cpp @@ -1,117 +1,50 @@ -#include "FilterParser.hpp" +#include "controllers/filters/lang/FilterParser.hpp" -#include "Application.hpp" -#include "common/Channel.hpp" -#include "controllers/filters/parser/Types.hpp" -#include "providers/twitch/TwitchIrcServer.hpp" +#include "controllers/filters/lang/expressions/BinaryOperation.hpp" +#include "controllers/filters/lang/expressions/Expression.hpp" +#include "controllers/filters/lang/expressions/ListExpression.hpp" +#include "controllers/filters/lang/expressions/RegexExpression.hpp" +#include "controllers/filters/lang/expressions/UnaryOperation.hpp" +#include "controllers/filters/lang/expressions/ValueExpression.hpp" +#include "controllers/filters/lang/Filter.hpp" +#include "controllers/filters/lang/Types.hpp" -namespace filterparser { +namespace { -ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel) +using namespace chatterino::filters; + +QString explainIllType(const IllTyped &ill) { - auto watchingChannel = chatterino::getApp()->twitch->watchingChannel.get(); - - /* Known Identifiers - * - * author.badges - * author.color - * author.name - * author.no_color - * author.subbed - * author.sub_length - * - * channel.name - * channel.watching - * - * flags.highlighted - * flags.points_redeemed - * flags.sub_message - * flags.system_message - * flags.reward_message - * flags.first_message - * flags.whisper - * - * message.content - * message.length - * - */ - - using MessageFlag = chatterino::MessageFlag; - - QStringList badges; - badges.reserve(m->badges.size()); - for (const auto &e : m->badges) - { - badges << e.key_; - } - - bool watching = !watchingChannel->getName().isEmpty() && - watchingChannel->getName().compare( - m->channelName, Qt::CaseInsensitive) == 0; - - bool subscribed = false; - int subLength = 0; - for (const QString &subBadge : {"subscriber", "founder"}) - { - if (!badges.contains(subBadge)) - { - continue; - } - subscribed = true; - if (m->badgeInfos.find(subBadge) != m->badgeInfos.end()) - { - subLength = m->badgeInfos.at(subBadge).toInt(); - } - } - ContextMap vars = { - {"author.badges", std::move(badges)}, - {"author.color", m->usernameColor}, - {"author.name", m->displayName}, - {"author.no_color", !m->usernameColor.isValid()}, - {"author.subbed", subscribed}, - {"author.sub_length", subLength}, - - {"channel.name", m->channelName}, - {"channel.watching", watching}, - - {"flags.highlighted", m->flags.has(MessageFlag::Highlighted)}, - {"flags.points_redeemed", m->flags.has(MessageFlag::RedeemedHighlight)}, - {"flags.sub_message", m->flags.has(MessageFlag::Subscription)}, - {"flags.system_message", m->flags.has(MessageFlag::System)}, - {"flags.reward_message", - m->flags.has(MessageFlag::RedeemedChannelPointReward)}, - {"flags.first_message", m->flags.has(MessageFlag::FirstMessage)}, - {"flags.whisper", m->flags.has(MessageFlag::Whisper)}, - {"flags.reply", m->flags.has(MessageFlag::ReplyMessage)}, - - {"message.content", m->messageText}, - {"message.length", m->messageText.length()}, - }; - { - using namespace chatterino; - auto *tc = dynamic_cast(channel); - if (channel && !channel->isEmpty() && tc) - { - vars["channel.live"] = tc->isLive(); - } - else - { - vars["channel.live"] = false; - } - } - return vars; + return QString("%1\n\nProblem occurred here:\n%2") + .arg(ill.message) + .arg(ill.expr->filterString()); } +} // namespace + +namespace chatterino::filters { + FilterParser::FilterParser(const QString &text) : text_(text) , tokenizer_(Tokenizer(text)) , builtExpression_(this->parseExpression(true)) { -} + if (!this->valid_) + { + return; + } -bool FilterParser::execute(const ContextMap &context) const -{ - return this->builtExpression_->execute(context).toBool(); + // safety: returnType must not live longer than the parsed expression. See + // comment on IllTyped::expr. + auto returnType = + this->builtExpression_->synthesizeType(MESSAGE_TYPING_CONTEXT); + if (isIllTyped(returnType)) + { + this->errorLog(explainIllType(std::get(returnType))); + return; + } + + this->returnType_ = std::get(returnType).type; } bool FilterParser::valid() const @@ -119,6 +52,18 @@ bool FilterParser::valid() const return this->valid_; } +Type FilterParser::returnType() const +{ + return this->returnType_; +} + +ExpressionPtr FilterParser::release() +{ + ExpressionPtr ret; + this->builtExpression_.swap(ret); + return ret; +} + ExpressionPtr FilterParser::parseExpression(bool top) { auto e = this->parseAnd(); @@ -369,12 +314,7 @@ const QStringList &FilterParser::errors() const const QString FilterParser::debugString() const { - return this->builtExpression_->debug(); + return this->builtExpression_->debug(MESSAGE_TYPING_CONTEXT); } -const QString FilterParser::filterString() const -{ - return this->builtExpression_->filterString(); -} - -} // namespace filterparser +} // namespace chatterino::filters diff --git a/src/controllers/filters/parser/FilterParser.hpp b/src/controllers/filters/lang/FilterParser.hpp similarity index 62% rename from src/controllers/filters/parser/FilterParser.hpp rename to src/controllers/filters/lang/FilterParser.hpp index 70037993e..1fa8fa596 100644 --- a/src/controllers/filters/parser/FilterParser.hpp +++ b/src/controllers/filters/lang/FilterParser.hpp @@ -1,28 +1,25 @@ #pragma once -#include "controllers/filters/parser/Tokenizer.hpp" -#include "controllers/filters/parser/Types.hpp" +#include "controllers/filters/lang/expressions/Expression.hpp" +#include "controllers/filters/lang/Tokenizer.hpp" +#include "controllers/filters/lang/Types.hpp" -namespace chatterino { - -class Channel; - -} // namespace chatterino - -namespace filterparser { - -ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel); +namespace chatterino::filters { class FilterParser { public: + /** + * Take input text & attempt to parse it into a filter + **/ FilterParser(const QString &text); - bool execute(const ContextMap &context) const; + bool valid() const; + Type returnType() const; + ExpressionPtr release(); const QStringList &errors() const; const QString debugString() const; - const QString filterString() const; private: ExpressionPtr parseExpression(bool top = false); @@ -41,5 +38,7 @@ private: QString text_; Tokenizer tokenizer_; ExpressionPtr builtExpression_; + Type returnType_ = Type::Bool; }; -} // namespace filterparser + +} // namespace chatterino::filters diff --git a/src/controllers/filters/parser/Tokenizer.cpp b/src/controllers/filters/lang/Tokenizer.cpp similarity index 67% rename from src/controllers/filters/parser/Tokenizer.cpp rename to src/controllers/filters/lang/Tokenizer.cpp index e7da03247..f25f1976b 100644 --- a/src/controllers/filters/parser/Tokenizer.cpp +++ b/src/controllers/filters/lang/Tokenizer.cpp @@ -1,7 +1,79 @@ -#include "controllers/filters/parser/Tokenizer.hpp" +#include "controllers/filters/lang/Tokenizer.hpp" + #include "common/QLogging.hpp" -namespace filterparser { +namespace chatterino::filters { + +QString tokenTypeToInfoString(TokenType type) +{ + switch (type) + { + case AND: + return "And"; + case OR: + return "Or"; + case LP: + return ""; + case RP: + return ""; + case LIST_START: + return ""; + case LIST_END: + return ""; + case COMMA: + return ""; + case PLUS: + return "Plus"; + case MINUS: + return "Minus"; + case MULTIPLY: + return "Multiply"; + case DIVIDE: + return "Divide"; + case MOD: + return "Mod"; + case EQ: + return "Eq"; + case NEQ: + return "NotEq"; + case LT: + return "LessThan"; + case GT: + return "GreaterThan"; + case LTE: + return "LessThanEq"; + case GTE: + return "GreaterThanEq"; + case CONTAINS: + return "Contains"; + case STARTS_WITH: + return "StartsWith"; + case ENDS_WITH: + return "EndsWith"; + case MATCH: + return "Match"; + case NOT: + return "Not"; + case STRING: + return ""; + case INT: + return ""; + case IDENTIFIER: + return ""; + case CONTROL_START: + case CONTROL_END: + case BINARY_START: + case BINARY_END: + case UNARY_START: + case UNARY_END: + case MATH_START: + case MATH_END: + case OTHER_START: + case NONE: + default: + return ""; + } +} Tokenizer::Tokenizer(const QString &text) { @@ -33,7 +105,9 @@ QString Tokenizer::current() const QString Tokenizer::preview() const { if (this->hasNext()) + { return this->tokens_.at(this->i_); + } return ""; } @@ -100,51 +174,97 @@ const QStringList Tokenizer::allTokens() TokenType Tokenizer::tokenize(const QString &text) { if (text == "&&") + { return TokenType::AND; + } else if (text == "||") + { return TokenType::OR; + } else if (text == "(") + { return TokenType::LP; + } else if (text == ")") + { return TokenType::RP; + } else if (text == "{") + { return TokenType::LIST_START; + } else if (text == "}") + { return TokenType::LIST_END; + } else if (text == ",") + { return TokenType::COMMA; + } else if (text == "+") + { return TokenType::PLUS; + } else if (text == "-") + { return TokenType::MINUS; + } else if (text == "*") + { return TokenType::MULTIPLY; + } else if (text == "/") + { return TokenType::DIVIDE; + } else if (text == "==") + { return TokenType::EQ; + } else if (text == "!=") + { return TokenType::NEQ; + } else if (text == "%") + { return TokenType::MOD; + } else if (text == "<") + { return TokenType::LT; + } else if (text == ">") + { return TokenType::GT; + } else if (text == "<=") + { return TokenType::LTE; + } else if (text == ">=") + { return TokenType::GTE; + } else if (text == "contains") + { return TokenType::CONTAINS; + } else if (text == "startswith") + { return TokenType::STARTS_WITH; + } else if (text == "endswith") + { return TokenType::ENDS_WITH; + } else if (text == "match") + { return TokenType::MATCH; + } else if (text == "!") + { return TokenType::NOT; + } else { if ((text.startsWith("r\"") || text.startsWith("ri\"")) && @@ -154,14 +274,20 @@ TokenType Tokenizer::tokenize(const QString &text) } if (text.front() == '"' && text.back() == '"') + { return TokenType::STRING; + } if (validIdentifiersMap.keys().contains(text)) + { return TokenType::IDENTIFIER; + } bool flag; if (text.toInt(&flag); flag) + { return TokenType::INT; + } } return TokenType::NONE; @@ -189,4 +315,4 @@ bool Tokenizer::typeIsMathOp(TokenType token) return token > TokenType::MATH_START && token < TokenType::MATH_END; } -} // namespace filterparser +} // namespace chatterino::filters diff --git a/src/controllers/filters/parser/Tokenizer.hpp b/src/controllers/filters/lang/Tokenizer.hpp similarity index 56% rename from src/controllers/filters/parser/Tokenizer.hpp rename to src/controllers/filters/lang/Tokenizer.hpp index 752616078..ced78c5d2 100644 --- a/src/controllers/filters/parser/Tokenizer.hpp +++ b/src/controllers/filters/lang/Tokenizer.hpp @@ -1,12 +1,12 @@ #pragma once -#include "controllers/filters/parser/Types.hpp" +#include "controllers/filters/lang/Types.hpp" #include #include #include -namespace filterparser { +namespace chatterino::filters { static const QMap validIdentifiersMap = { {"author.badges", "author badges"}, @@ -17,17 +17,30 @@ static const QMap validIdentifiersMap = { {"author.sub_length", "author sub length"}, {"channel.name", "channel name"}, {"channel.watching", "/watching channel?"}, - {"channel.live", "Channel live?"}, + {"channel.live", "channel live?"}, + {"flags.action", "action/me message?"}, {"flags.highlighted", "highlighted?"}, {"flags.points_redeemed", "redeemed points?"}, {"flags.sub_message", "sub/resub message?"}, {"flags.system_message", "system message?"}, {"flags.reward_message", "channel point reward message?"}, {"flags.first_message", "first message?"}, + {"flags.elevated_message", "hype chat message?"}, + // Ideally these values are unique, because ChannelFilterEditorDialog::ValueSpecifier::expressionText depends on + // std::map layout in Qt 6 and internal implementation in Qt 5. + {"flags.hype_chat", "hype chat message?"}, + {"flags.cheer_message", "cheer message?"}, {"flags.whisper", "whisper message?"}, {"flags.reply", "reply message?"}, + {"flags.automod", "automod message?"}, + {"flags.restricted", "restricted message?"}, + {"flags.monitored", "monitored message?"}, {"message.content", "message text"}, - {"message.length", "message length"}}; + {"message.length", "message length"}, + {"reward.title", "point reward title"}, + {"reward.cost", "point reward cost"}, + {"reward.id", "point reward id"}, +}; // clang-format off static const QRegularExpression tokenRegex( @@ -39,6 +52,58 @@ static const QRegularExpression tokenRegex( ); // clang-format on +enum TokenType { + // control + CONTROL_START = 0, + AND = 1, + OR = 2, + LP = 3, + RP = 4, + LIST_START = 5, + LIST_END = 6, + COMMA = 7, + CONTROL_END = 19, + + // binary operator + BINARY_START = 20, + EQ = 21, + NEQ = 22, + LT = 23, + GT = 24, + LTE = 25, + GTE = 26, + CONTAINS = 27, + STARTS_WITH = 28, + ENDS_WITH = 29, + MATCH = 30, + BINARY_END = 49, + + // unary operator + UNARY_START = 50, + NOT = 51, + UNARY_END = 99, + + // math operators + MATH_START = 100, + PLUS = 101, + MINUS = 102, + MULTIPLY = 103, + DIVIDE = 104, + MOD = 105, + MATH_END = 149, + + // other types + OTHER_START = 150, + STRING = 151, + INT = 152, + IDENTIFIER = 153, + REGULAR_EXPRESSION = 154, + + NONE = 200 +}; + +QString tokenTypeToInfoString(TokenType type); + class Tokenizer { public: @@ -71,4 +136,4 @@ private: TokenType tokenize(const QString &text); }; -} // namespace filterparser +} // namespace chatterino::filters diff --git a/src/controllers/filters/lang/Types.cpp b/src/controllers/filters/lang/Types.cpp new file mode 100644 index 000000000..66a715960 --- /dev/null +++ b/src/controllers/filters/lang/Types.cpp @@ -0,0 +1,101 @@ +#include "controllers/filters/lang/Types.hpp" + +#include "controllers/filters/lang/expressions/Expression.hpp" +#include "controllers/filters/lang/Tokenizer.hpp" + +namespace chatterino::filters { + +bool isList(const PossibleType &possibleType) +{ + using T = Type; + if (isIllTyped(possibleType)) + { + return false; + } + + auto typ = std::get(possibleType); + return typ == T::List || typ == T::StringList || + typ == T::MatchingSpecifier; +} + +QString typeToString(Type type) +{ + using T = Type; + switch (type) + { + case T::String: + return "String"; + case T::Int: + return "Int"; + case T::Bool: + return "Bool"; + case T::Color: + return "Color"; + case T::RegularExpression: + return "RegularExpression"; + case T::List: + return "List"; + case T::StringList: + return "StringList"; + case T::MatchingSpecifier: + return "MatchingSpecifier"; + case T::Map: + return "Map"; + default: + return "Unknown"; + } +} + +QString TypeClass::string() const +{ + return typeToString(this->type); +} + +bool TypeClass::operator==(Type t) const +{ + return this->type == t; +} + +bool TypeClass::operator==(const TypeClass &t) const +{ + return this->type == t.type; +} + +bool TypeClass::operator==(const IllTyped &t) const +{ + return false; +} + +bool TypeClass::operator!=(Type t) const +{ + return !this->operator==(t); +} + +bool TypeClass::operator!=(const TypeClass &t) const +{ + return !this->operator==(t); +} + +bool TypeClass::operator!=(const IllTyped &t) const +{ + return true; +} + +QString IllTyped::string() const +{ + return "IllTyped"; +} + +QString possibleTypeToString(const PossibleType &possible) +{ + if (isWellTyped(possible)) + { + return std::get(possible).string(); + } + else + { + return std::get(possible).string(); + } +} + +} // namespace chatterino::filters diff --git a/src/controllers/filters/lang/Types.hpp b/src/controllers/filters/lang/Types.hpp new file mode 100644 index 000000000..737841090 --- /dev/null +++ b/src/controllers/filters/lang/Types.hpp @@ -0,0 +1,108 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include + +namespace chatterino::filters { + +class Expression; + +enum class Type { + String, + Int, + Bool, + Color, + RegularExpression, + List, + StringList, // List of only strings + MatchingSpecifier, // 2-element list in {RegularExpression, Int} form + Map +}; + +using ContextMap = QMap; +using TypingContext = QMap; + +QString typeToString(Type type); + +struct IllTyped; + +struct TypeClass { + Type type; + + QString string() const; + + bool operator==(Type t) const; + bool operator==(const TypeClass &t) const; + bool operator==(const IllTyped &t) const; + bool operator!=(Type t) const; + bool operator!=(const TypeClass &t) const; + bool operator!=(const IllTyped &t) const; +}; + +struct IllTyped { + // Important nuance to expr: + // During type synthesis, should an error occur and an IllTyped PossibleType be + // returned, expr is a pointer to an Expression that exists in the Expression + // tree that was parsed. Therefore, you cannot hold on to this pointer longer + // than the Expression tree exists. Be careful! + const Expression *expr; + QString message; + + QString string() const; +}; + +using PossibleType = std::variant; + +inline bool isWellTyped(const PossibleType &possible) +{ + return std::holds_alternative(possible); +} + +inline bool isIllTyped(const PossibleType &possible) +{ + return std::holds_alternative(possible); +} + +QString possibleTypeToString(const PossibleType &possible); + +bool isList(const PossibleType &possibleType); + +inline bool variantIs(const QVariant &a, int type) +{ +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + return a.typeId() == type; +#else + return a.type() == type; +#endif +} + +inline bool variantIsNot(const QVariant &a, int type) +{ +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + return a.typeId() != type; +#else + return a.type() != type; +#endif +} + +inline bool convertVariantTypes(QVariant &a, QVariant &b, int type) +{ +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QMetaType ty(type); + return a.convert(ty) && b.convert(ty); +#else + return a.convert(type) && b.convert(type); +#endif +} + +inline bool variantTypesMatch(QVariant &a, QVariant &b, int type) +{ + return variantIs(a, type) && variantIs(b, type); +} + +} // namespace chatterino::filters diff --git a/src/controllers/filters/lang/expressions/BinaryOperation.cpp b/src/controllers/filters/lang/expressions/BinaryOperation.cpp new file mode 100644 index 000000000..acf21726b --- /dev/null +++ b/src/controllers/filters/lang/expressions/BinaryOperation.cpp @@ -0,0 +1,447 @@ +#include "controllers/filters/lang/expressions/BinaryOperation.hpp" + +#include + +namespace { + +/// Loosely compares `lhs` with `rhs`. +/// This attempts to convert both variants to a common type if they're not equal. +bool looselyCompareVariants(QVariant &lhs, QVariant &rhs) +{ +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + // Qt 6 and later don't convert types as much as Qt 5 did when comparing. + // + // Based on QVariant::cmp from Qt 5.15 + // https://github.com/qt/qtbase/blob/29400a683f96867133b28299c0d0bd6bcf40df35/src/corelib/kernel/qvariant.cpp#L4039-L4071 + if (lhs.metaType() != rhs.metaType()) + { + if (rhs.canConvert(lhs.metaType())) + { + if (!rhs.convert(lhs.metaType())) + { + return false; + } + } + else + { + // try the opposite conversion, it might work + qSwap(lhs, rhs); + if (!rhs.convert(lhs.metaType())) + { + return false; + } + } + } +#endif + + return lhs == rhs; +} + +} // namespace + +namespace chatterino::filters { + +BinaryOperation::BinaryOperation(TokenType op, ExpressionPtr left, + ExpressionPtr right) + : op_(op) + , left_(std::move(left)) + , right_(std::move(right)) +{ +} + +QVariant BinaryOperation::execute(const ContextMap &context) const +{ + auto left = this->left_->execute(context); + auto right = this->right_->execute(context); + switch (this->op_) + { + case PLUS: + if (variantIs(left, QMetaType::QString) && + right.canConvert()) + { + return left.toString().append(right.toString()); + } + if (convertVariantTypes(left, right, QMetaType::Int)) + { + return left.toInt() + right.toInt(); + } + return 0; + case MINUS: + if (convertVariantTypes(left, right, QMetaType::Int)) + { + return left.toInt() - right.toInt(); + } + return 0; + case MULTIPLY: + if (convertVariantTypes(left, right, QMetaType::Int)) + { + return left.toInt() * right.toInt(); + } + return 0; + case DIVIDE: + if (convertVariantTypes(left, right, QMetaType::Int)) + { + return left.toInt() / right.toInt(); + } + return 0; + case MOD: + if (convertVariantTypes(left, right, QMetaType::Int)) + { + return left.toInt() % right.toInt(); + } + return 0; + case OR: + if (convertVariantTypes(left, right, QMetaType::Bool)) + { + return left.toBool() || right.toBool(); + } + return false; + case AND: + if (convertVariantTypes(left, right, QMetaType::Bool)) + { + return left.toBool() && right.toBool(); + } + return false; + case EQ: + if (variantTypesMatch(left, right, QMetaType::QString)) + { + return left.toString().compare(right.toString(), + Qt::CaseInsensitive) == 0; + } + return looselyCompareVariants(left, right); + case NEQ: + if (variantTypesMatch(left, right, QMetaType::QString)) + { + return left.toString().compare(right.toString(), + Qt::CaseInsensitive) != 0; + } + return !looselyCompareVariants(left, right); + case LT: + if (convertVariantTypes(left, right, QMetaType::Int)) + { + return left.toInt() < right.toInt(); + } + return false; + case GT: + if (convertVariantTypes(left, right, QMetaType::Int)) + { + return left.toInt() > right.toInt(); + } + return false; + case LTE: + if (convertVariantTypes(left, right, QMetaType::Int)) + { + return left.toInt() <= right.toInt(); + } + return false; + case GTE: + if (convertVariantTypes(left, right, QMetaType::Int)) + { + return left.toInt() >= right.toInt(); + } + return false; + case CONTAINS: + if (variantIs(left, QMetaType::QStringList) && + right.canConvert()) + { + return left.toStringList().contains(right.toString(), + Qt::CaseInsensitive); + } + + if (variantIs(left, QMetaType::QVariantMap) && + right.canConvert()) + { + return left.toMap().contains(right.toString()); + } + + if (variantIs(left, QMetaType::QVariantList)) + { + return left.toList().contains(right); + } + + if (left.canConvert() && right.canConvert()) + { + return left.toString().contains(right.toString(), + Qt::CaseInsensitive); + } + + return false; + case STARTS_WITH: + if (variantIs(left, QMetaType::QStringList) && + right.canConvert()) + { + auto list = left.toStringList(); + return !list.isEmpty() && + list.first().compare(right.toString(), + Qt::CaseInsensitive) == 0; + } + + if (variantIs(left, QMetaType::QVariantList)) + { + return left.toList().startsWith(right); + } + + if (left.canConvert() && right.canConvert()) + { + return left.toString().startsWith(right.toString(), + Qt::CaseInsensitive); + } + + return false; + + case ENDS_WITH: + if (variantIs(left, QMetaType::QStringList) && + right.canConvert()) + { + auto list = left.toStringList(); + return !list.isEmpty() && + list.last().compare(right.toString(), + Qt::CaseInsensitive) == 0; + } + + if (variantIs(left, QMetaType::QVariantList)) + { + return left.toList().endsWith(right); + } + + if (left.canConvert() && right.canConvert()) + { + return left.toString().endsWith(right.toString(), + Qt::CaseInsensitive); + } + + return false; + case MATCH: { + if (!left.canConvert()) + { + return false; + } + + auto matching = left.toString(); + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + switch (static_cast(right.typeId())) +#else + switch (static_cast(right.type())) +#endif + { + case QMetaType::QRegularExpression: { + return right.toRegularExpression() + .match(matching) + .hasMatch(); + } + case QMetaType::QVariantList: { + auto list = right.toList(); + + // list must be two items + if (list.size() != 2) + { + return false; + } + + // list must be a regular expression and an int + if (variantIsNot(list.at(0), + QMetaType::QRegularExpression) || + variantIsNot(list.at(1), QMetaType::Int)) + { + return false; + } + + auto match = + list.at(0).toRegularExpression().match(matching); + + // if matched, return nth capture group. Otherwise, return "" + if (match.hasMatch()) + { + return match.captured(list.at(1).toInt()); + } + else + { + return ""; + } + } + default: + return false; + } + } + default: + return false; + } +} + +PossibleType BinaryOperation::synthesizeType(const TypingContext &context) const +{ + auto leftSyn = this->left_->synthesizeType(context); + auto rightSyn = this->right_->synthesizeType(context); + + // Return if either operand is ill-typed + if (isIllTyped(leftSyn)) + { + return leftSyn; + } + else if (isIllTyped(rightSyn)) + { + return rightSyn; + } + + auto left = std::get(leftSyn); + auto right = std::get(rightSyn); + + switch (this->op_) + { + case PLUS: + if (left == Type::String) + { + return TypeClass{Type::String}; // String concatenation + } + else if (left == Type::Int && right == Type::Int) + { + return TypeClass{Type::Int}; + } + + return IllTyped{this, "Can only add Ints or concatenate a String"}; + case MINUS: + case MULTIPLY: + case DIVIDE: + case MOD: + if (left == Type::Int && right == Type::Int) + { + return TypeClass{Type::Int}; + } + + return IllTyped{this, "Can only perform operation with Ints"}; + case OR: + case AND: + if (left == Type::Bool && right == Type::Bool) + { + return TypeClass{Type::Bool}; + } + + return IllTyped{this, + "Can only perform logical operations with Bools"}; + case EQ: + case NEQ: + // equals/not equals always produces a valid output + return TypeClass{Type::Bool}; + case LT: + case GT: + case LTE: + case GTE: + if (left == Type::Int && right == Type::Int) + { + return TypeClass{Type::Bool}; + } + + return IllTyped{this, "Can only perform comparisons with Ints"}; + case STARTS_WITH: + case ENDS_WITH: + if (isList(left)) + { + return TypeClass{Type::Bool}; + } + if (left == Type::String && right == Type::String) + { + return TypeClass{Type::Bool}; + } + + return IllTyped{ + this, + "Can only perform starts/ends with a List or two Strings"}; + case CONTAINS: + if (isList(left) || left == Type::Map) + { + return TypeClass{Type::Bool}; + } + if (left == Type::String && right == Type::String) + { + return TypeClass{Type::Bool}; + } + + return IllTyped{ + this, + "Can only perform contains with a List, a Map, or two Strings"}; + case MATCH: { + if (left != Type::String) + { + return IllTyped{this, + "Left argument of match must be a String"}; + } + + if (right == Type::RegularExpression) + { + return TypeClass{Type::Bool}; + } + if (right == Type::MatchingSpecifier) + { // group capturing + return TypeClass{Type::String}; + } + + return IllTyped{this, "Can only match on a RegularExpression or a " + "MatchingSpecifier"}; + } + default: + return IllTyped{this, "Not implemented"}; + } +} + +QString BinaryOperation::debug(const TypingContext &context) const +{ + return QString("BinaryOp[%1](%2 : %3, %4 : %5)") + .arg(tokenTypeToInfoString(this->op_)) + .arg(this->left_->debug(context)) + .arg(possibleTypeToString(this->left_->synthesizeType(context))) + .arg(this->right_->debug(context)) + .arg(possibleTypeToString(this->right_->synthesizeType(context))); +} + +QString BinaryOperation::filterString() const +{ + const auto opText = [&]() -> QString { + switch (this->op_) + { + case AND: + return "&&"; + case OR: + return "||"; + case PLUS: + return "+"; + case MINUS: + return "-"; + case MULTIPLY: + return "*"; + case DIVIDE: + return "/"; + case MOD: + return "%"; + case EQ: + return "=="; + case NEQ: + return "!="; + case LT: + return "<"; + case GT: + return ">"; + case LTE: + return "<="; + case GTE: + return ">="; + case CONTAINS: + return "contains"; + case STARTS_WITH: + return "startswith"; + case ENDS_WITH: + return "endswith"; + case MATCH: + return "match"; + default: + return ""; + } + }(); + + return QString("(%1 %2 %3)") + .arg(this->left_->filterString()) + .arg(opText) + .arg(this->right_->filterString()); +} + +} // namespace chatterino::filters diff --git a/src/controllers/filters/lang/expressions/BinaryOperation.hpp b/src/controllers/filters/lang/expressions/BinaryOperation.hpp new file mode 100644 index 000000000..b42f81bf5 --- /dev/null +++ b/src/controllers/filters/lang/expressions/BinaryOperation.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "controllers/filters/lang/expressions/Expression.hpp" +#include "controllers/filters/lang/Types.hpp" + +namespace chatterino::filters { + +class BinaryOperation : public Expression +{ +public: + BinaryOperation(TokenType op, ExpressionPtr left, ExpressionPtr right); + + QVariant execute(const ContextMap &context) const override; + PossibleType synthesizeType(const TypingContext &context) const override; + QString debug(const TypingContext &context) const override; + QString filterString() const override; + +private: + TokenType op_; + ExpressionPtr left_; + ExpressionPtr right_; +}; + +} // namespace chatterino::filters diff --git a/src/controllers/filters/lang/expressions/Expression.cpp b/src/controllers/filters/lang/expressions/Expression.cpp new file mode 100644 index 000000000..0c353d8c4 --- /dev/null +++ b/src/controllers/filters/lang/expressions/Expression.cpp @@ -0,0 +1,5 @@ +#include "controllers/filters/lang/expressions/Expression.hpp" + +namespace chatterino::filters { + +} // namespace chatterino::filters diff --git a/src/controllers/filters/lang/expressions/Expression.hpp b/src/controllers/filters/lang/expressions/Expression.hpp new file mode 100644 index 000000000..fe4fddac3 --- /dev/null +++ b/src/controllers/filters/lang/expressions/Expression.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "controllers/filters/lang/Tokenizer.hpp" +#include "controllers/filters/lang/Types.hpp" + +#include +#include + +#include +#include + +namespace chatterino::filters { + +class Expression +{ +public: + virtual ~Expression() = default; + + virtual QVariant execute(const ContextMap &context) const = 0; + virtual PossibleType synthesizeType(const TypingContext &context) const = 0; + virtual QString debug(const TypingContext &context) const = 0; + virtual QString filterString() const = 0; +}; + +using ExpressionPtr = std::unique_ptr; +using ExpressionList = std::vector>; + +} // namespace chatterino::filters diff --git a/src/controllers/filters/lang/expressions/ListExpression.cpp b/src/controllers/filters/lang/expressions/ListExpression.cpp new file mode 100644 index 000000000..c0bf5ef8b --- /dev/null +++ b/src/controllers/filters/lang/expressions/ListExpression.cpp @@ -0,0 +1,94 @@ +#include "controllers/filters/lang/expressions/ListExpression.hpp" + +namespace chatterino::filters { + +ListExpression::ListExpression(ExpressionList &&list) + : list_(std::move(list)){}; + +QVariant ListExpression::execute(const ContextMap &context) const +{ + QList results; + bool allStrings = true; + for (const auto &exp : this->list_) + { + auto res = exp->execute(context); + if (allStrings && variantIsNot(res, QMetaType::QString)) + { + allStrings = false; + } + results.append(res); + } + + // if everything is a string return a QStringList for case-insensitive comparison + if (allStrings) + { + QStringList strings; + strings.reserve(results.size()); + for (const auto &val : results) + { + strings << val.toString(); + } + return strings; + } + + return results; +} + +PossibleType ListExpression::synthesizeType(const TypingContext &context) const +{ + std::vector types; + types.reserve(this->list_.size()); + bool allStrings = true; + for (const auto &exp : this->list_) + { + auto typSyn = exp->synthesizeType(context); + if (isIllTyped(typSyn)) + { + return typSyn; // Ill-typed + } + + auto typ = std::get(typSyn); + + if (typ != Type::String) + { + allStrings = false; + } + + types.push_back(typ); + } + + if (types.size() == 2 && types[0] == Type::RegularExpression && + types[1] == Type::Int) + { + // Specific {RegularExpression, Int} form + return TypeClass{Type::MatchingSpecifier}; + } + + return allStrings ? TypeClass{Type::StringList} : TypeClass{Type::List}; +} + +QString ListExpression::debug(const TypingContext &context) const +{ + QStringList debugs; + for (const auto &exp : this->list_) + { + debugs.append( + QString("%1 : %2") + .arg(exp->debug(context)) + .arg(possibleTypeToString(exp->synthesizeType(context)))); + } + + return QString("List(%1)").arg(debugs.join(", ")); +} + +QString ListExpression::filterString() const +{ + QStringList strings; + for (const auto &exp : this->list_) + { + strings.append(exp->filterString()); + } + return QString("{%1}").arg(strings.join(", ")); +} + +} // namespace chatterino::filters diff --git a/src/controllers/filters/lang/expressions/ListExpression.hpp b/src/controllers/filters/lang/expressions/ListExpression.hpp new file mode 100644 index 000000000..6de6a46ee --- /dev/null +++ b/src/controllers/filters/lang/expressions/ListExpression.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include "controllers/filters/lang/expressions/Expression.hpp" +#include "controllers/filters/lang/Types.hpp" + +namespace chatterino::filters { + +class ListExpression : public Expression +{ +public: + ListExpression(ExpressionList &&list); + + QVariant execute(const ContextMap &context) const override; + PossibleType synthesizeType(const TypingContext &context) const override; + QString debug(const TypingContext &context) const override; + QString filterString() const override; + +private: + ExpressionList list_; +}; + +} // namespace chatterino::filters diff --git a/src/controllers/filters/lang/expressions/RegexExpression.cpp b/src/controllers/filters/lang/expressions/RegexExpression.cpp new file mode 100644 index 000000000..848175734 --- /dev/null +++ b/src/controllers/filters/lang/expressions/RegexExpression.cpp @@ -0,0 +1,36 @@ +#include "controllers/filters/lang/expressions/RegexExpression.hpp" + +namespace chatterino::filters { + +RegexExpression::RegexExpression(const QString ®ex, bool caseInsensitive) + : regexString_(regex) + , caseInsensitive_(caseInsensitive) + , regex_(QRegularExpression( + regex, caseInsensitive ? QRegularExpression::CaseInsensitiveOption + : QRegularExpression::NoPatternOption)){}; + +QVariant RegexExpression::execute(const ContextMap & /*context*/) const +{ + return this->regex_; +} + +PossibleType RegexExpression::synthesizeType( + const TypingContext & /*context*/) const +{ + return TypeClass{Type::RegularExpression}; +} + +QString RegexExpression::debug(const TypingContext & /*context*/) const +{ + return QString("RegEx(%1)").arg(this->regexString_); +} + +QString RegexExpression::filterString() const +{ + auto s = this->regexString_; + return QString("%1\"%2\"") + .arg(this->caseInsensitive_ ? "ri" : "r") + .arg(s.replace("\"", "\\\"")); +} + +} // namespace chatterino::filters diff --git a/src/controllers/filters/lang/expressions/RegexExpression.hpp b/src/controllers/filters/lang/expressions/RegexExpression.hpp new file mode 100644 index 000000000..75fa5a088 --- /dev/null +++ b/src/controllers/filters/lang/expressions/RegexExpression.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include "controllers/filters/lang/expressions/Expression.hpp" +#include "controllers/filters/lang/Types.hpp" + +#include + +namespace chatterino::filters { + +class RegexExpression : public Expression +{ +public: + RegexExpression(const QString ®ex, bool caseInsensitive); + + QVariant execute(const ContextMap &context) const override; + PossibleType synthesizeType(const TypingContext &context) const override; + QString debug(const TypingContext &context) const override; + QString filterString() const override; + +private: + QString regexString_; + bool caseInsensitive_; + QRegularExpression regex_; +}; + +} // namespace chatterino::filters diff --git a/src/controllers/filters/lang/expressions/UnaryOperation.cpp b/src/controllers/filters/lang/expressions/UnaryOperation.cpp new file mode 100644 index 000000000..11f487aa1 --- /dev/null +++ b/src/controllers/filters/lang/expressions/UnaryOperation.cpp @@ -0,0 +1,69 @@ +#include "controllers/filters/lang/expressions/UnaryOperation.hpp" + +namespace chatterino::filters { + +UnaryOperation::UnaryOperation(TokenType op, ExpressionPtr right) + : op_(op) + , right_(std::move(right)) +{ +} + +QVariant UnaryOperation::execute(const ContextMap &context) const +{ + auto right = this->right_->execute(context); + switch (this->op_) + { + case NOT: + return right.canConvert() && !right.toBool(); + default: + return false; + } +} + +PossibleType UnaryOperation::synthesizeType(const TypingContext &context) const +{ + auto rightSyn = this->right_->synthesizeType(context); + if (isIllTyped(rightSyn)) + { + return rightSyn; + } + + auto right = std::get(rightSyn); + + switch (this->op_) + { + case NOT: + if (right == Type::Bool) + { + return TypeClass{Type::Bool}; + } + return IllTyped{this, "Can only negate boolean values"}; + default: + return IllTyped{this, "Not implemented"}; + } +} + +QString UnaryOperation::debug(const TypingContext &context) const +{ + return QString("UnaryOp[%1](%2 : %3)") + .arg(tokenTypeToInfoString(this->op_)) + .arg(this->right_->debug(context)) + .arg(possibleTypeToString(this->right_->synthesizeType(context))); +} + +QString UnaryOperation::filterString() const +{ + const auto opText = [&]() -> QString { + switch (this->op_) + { + case NOT: + return "!"; + default: + return ""; + } + }(); + + return QString("(%1%2)").arg(opText).arg(this->right_->filterString()); +} + +} // namespace chatterino::filters diff --git a/src/controllers/filters/lang/expressions/UnaryOperation.hpp b/src/controllers/filters/lang/expressions/UnaryOperation.hpp new file mode 100644 index 000000000..155a78b71 --- /dev/null +++ b/src/controllers/filters/lang/expressions/UnaryOperation.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include "controllers/filters/lang/expressions/Expression.hpp" +#include "controllers/filters/lang/Types.hpp" + +namespace chatterino::filters { + +class UnaryOperation : public Expression +{ +public: + UnaryOperation(TokenType op, ExpressionPtr right); + + QVariant execute(const ContextMap &context) const override; + PossibleType synthesizeType(const TypingContext &context) const override; + QString debug(const TypingContext &context) const override; + QString filterString() const override; + +private: + TokenType op_; + ExpressionPtr right_; +}; + +} // namespace chatterino::filters diff --git a/src/controllers/filters/lang/expressions/ValueExpression.cpp b/src/controllers/filters/lang/expressions/ValueExpression.cpp new file mode 100644 index 000000000..23244613d --- /dev/null +++ b/src/controllers/filters/lang/expressions/ValueExpression.cpp @@ -0,0 +1,70 @@ +#include "controllers/filters/lang/expressions/ValueExpression.hpp" + +#include "controllers/filters/lang/Tokenizer.hpp" + +namespace chatterino::filters { + +ValueExpression::ValueExpression(QVariant value, TokenType type) + : value_(std::move(value)) + , type_(type) +{ +} + +QVariant ValueExpression::execute(const ContextMap &context) const +{ + if (this->type_ == TokenType::IDENTIFIER) + { + return context.value(this->value_.toString()); + } + return this->value_; +} + +PossibleType ValueExpression::synthesizeType(const TypingContext &context) const +{ + switch (this->type_) + { + case TokenType::IDENTIFIER: { + auto it = context.find(this->value_.toString()); + if (it != context.end()) + { + return TypeClass{it.value()}; + } + + return IllTyped{this, "Unbound identifier"}; + } + case TokenType::INT: + return TypeClass{Type::Int}; + case TokenType::STRING: + return TypeClass{Type::String}; + default: + return IllTyped{this, "Invalid value type"}; + } +} + +TokenType ValueExpression::type() +{ + return this->type_; +} + +QString ValueExpression::debug(const TypingContext & /*context*/) const +{ + return QString("Val(%1)").arg(this->value_.toString()); +} + +QString ValueExpression::filterString() const +{ + switch (this->type_) + { + case INT: + return QString::number(this->value_.toInt()); + case STRING: + return QString("\"%1\"").arg( + this->value_.toString().replace("\"", "\\\"")); + case IDENTIFIER: + return this->value_.toString(); + default: + return ""; + } +} + +} // namespace chatterino::filters diff --git a/src/controllers/filters/lang/expressions/ValueExpression.hpp b/src/controllers/filters/lang/expressions/ValueExpression.hpp new file mode 100644 index 000000000..56cbf80c4 --- /dev/null +++ b/src/controllers/filters/lang/expressions/ValueExpression.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "controllers/filters/lang/expressions/Expression.hpp" +#include "controllers/filters/lang/Types.hpp" + +namespace chatterino::filters { + +class ValueExpression : public Expression +{ +public: + ValueExpression(QVariant value, TokenType type); + TokenType type(); + + QVariant execute(const ContextMap &context) const override; + PossibleType synthesizeType(const TypingContext &context) const override; + QString debug(const TypingContext &context) const override; + QString filterString() const override; + +private: + QVariant value_; + TokenType type_; +}; + +} // namespace chatterino::filters diff --git a/src/controllers/filters/parser/Types.cpp b/src/controllers/filters/parser/Types.cpp deleted file mode 100644 index 159e89ce9..000000000 --- a/src/controllers/filters/parser/Types.cpp +++ /dev/null @@ -1,512 +0,0 @@ -#include "controllers/filters/parser/Types.hpp" - -namespace filterparser { - -bool convertVariantTypes(QVariant &a, QVariant &b, int type) -{ - return a.convert(type) && b.convert(type); -} - -bool variantTypesMatch(QVariant &a, QVariant &b, QVariant::Type type) -{ - return a.type() == type && b.type() == type; -} - -QString tokenTypeToInfoString(TokenType type) -{ - switch (type) - { - case CONTROL_START: - case CONTROL_END: - case BINARY_START: - case BINARY_END: - case UNARY_START: - case UNARY_END: - case MATH_START: - case MATH_END: - case OTHER_START: - case NONE: - return ""; - case AND: - return ""; - case OR: - return ""; - case LP: - return ""; - case RP: - return ""; - case LIST_START: - return ""; - case LIST_END: - return ""; - case COMMA: - return ""; - case PLUS: - return ""; - case MINUS: - return ""; - case MULTIPLY: - return ""; - case DIVIDE: - return ""; - case MOD: - return ""; - case EQ: - return ""; - case NEQ: - return ""; - case LT: - return ""; - case GT: - return ""; - case LTE: - return ""; - case GTE: - return ""; - case CONTAINS: - return ""; - case STARTS_WITH: - return ""; - case ENDS_WITH: - return ""; - case MATCH: - return ""; - case NOT: - return ""; - case STRING: - return ""; - case INT: - return ""; - case IDENTIFIER: - return ""; - default: - return ""; - } -} - -// ValueExpression - -ValueExpression::ValueExpression(QVariant value, TokenType type) - : value_(value) - , type_(type){}; - -QVariant ValueExpression::execute(const ContextMap &context) const -{ - if (this->type_ == TokenType::IDENTIFIER) - { - return context.value(this->value_.toString()); - } - return this->value_; -} - -TokenType ValueExpression::type() -{ - return this->type_; -} - -QString ValueExpression::debug() const -{ - return this->value_.toString(); -} - -QString ValueExpression::filterString() const -{ - switch (this->type_) - { - case INT: - return QString::number(this->value_.toInt()); - case STRING: - return QString("\"%1\"").arg( - this->value_.toString().replace("\"", "\\\"")); - case IDENTIFIER: - return this->value_.toString(); - default: - return ""; - } -} - -// RegexExpression - -RegexExpression::RegexExpression(QString regex, bool caseInsensitive) - : regexString_(regex) - , caseInsensitive_(caseInsensitive) - , regex_(QRegularExpression( - regex, caseInsensitive ? QRegularExpression::CaseInsensitiveOption - : QRegularExpression::NoPatternOption)){}; - -QVariant RegexExpression::execute(const ContextMap &) const -{ - return this->regex_; -} - -QString RegexExpression::debug() const -{ - return this->regexString_; -} - -QString RegexExpression::filterString() const -{ - auto s = this->regexString_; - return QString("%1\"%2\"") - .arg(this->caseInsensitive_ ? "ri" : "r") - .arg(s.replace("\"", "\\\"")); -} - -// ListExpression - -ListExpression::ListExpression(ExpressionList list) - : list_(std::move(list)){}; - -QVariant ListExpression::execute(const ContextMap &context) const -{ - QList results; - bool allStrings = true; - for (const auto &exp : this->list_) - { - auto res = exp->execute(context); - if (allStrings && res.type() != QVariant::Type::String) - { - allStrings = false; - } - results.append(res); - } - - // if everything is a string return a QStringList for case-insensitive comparison - if (allStrings) - { - QStringList strings; - strings.reserve(results.size()); - for (const auto &val : results) - { - strings << val.toString(); - } - return strings; - } - else - { - return results; - } -} - -QString ListExpression::debug() const -{ - QStringList debugs; - for (const auto &exp : this->list_) - { - debugs.append(exp->debug()); - } - return QString("{%1}").arg(debugs.join(", ")); -} - -QString ListExpression::filterString() const -{ - QStringList strings; - for (const auto &exp : this->list_) - { - strings.append(QString("(%1)").arg(exp->filterString())); - } - return QString("{%1}").arg(strings.join(", ")); -} - -// BinaryOperation - -BinaryOperation::BinaryOperation(TokenType op, ExpressionPtr left, - ExpressionPtr right) - : op_(op) - , left_(std::move(left)) - , right_(std::move(right)) -{ -} - -QVariant BinaryOperation::execute(const ContextMap &context) const -{ - auto left = this->left_->execute(context); - auto right = this->right_->execute(context); - switch (this->op_) - { - case PLUS: - if (left.type() == QVariant::Type::String && - right.canConvert(QMetaType::QString)) - { - return left.toString().append(right.toString()); - } - if (convertVariantTypes(left, right, QMetaType::Int)) - { - return left.toInt() + right.toInt(); - } - return 0; - case MINUS: - if (convertVariantTypes(left, right, QMetaType::Int)) - return left.toInt() - right.toInt(); - return 0; - case MULTIPLY: - if (convertVariantTypes(left, right, QMetaType::Int)) - return left.toInt() * right.toInt(); - return 0; - case DIVIDE: - if (convertVariantTypes(left, right, QMetaType::Int)) - return left.toInt() / right.toInt(); - return 0; - case MOD: - if (convertVariantTypes(left, right, QMetaType::Int)) - return left.toInt() % right.toInt(); - return 0; - case OR: - if (convertVariantTypes(left, right, QMetaType::Bool)) - return left.toBool() || right.toBool(); - return false; - case AND: - if (convertVariantTypes(left, right, QMetaType::Bool)) - return left.toBool() && right.toBool(); - return false; - case EQ: - if (variantTypesMatch(left, right, QVariant::Type::String)) - { - return left.toString().compare(right.toString(), - Qt::CaseInsensitive) == 0; - } - return left == right; - case NEQ: - if (variantTypesMatch(left, right, QVariant::Type::String)) - { - return left.toString().compare(right.toString(), - Qt::CaseInsensitive) != 0; - } - return left != right; - case LT: - if (convertVariantTypes(left, right, QMetaType::Int)) - return left.toInt() < right.toInt(); - return false; - case GT: - if (convertVariantTypes(left, right, QMetaType::Int)) - return left.toInt() > right.toInt(); - return false; - case LTE: - if (convertVariantTypes(left, right, QMetaType::Int)) - return left.toInt() <= right.toInt(); - return false; - case GTE: - if (convertVariantTypes(left, right, QMetaType::Int)) - return left.toInt() >= right.toInt(); - return false; - case CONTAINS: - if (left.type() == QVariant::Type::StringList && - right.canConvert(QMetaType::QString)) - { - return left.toStringList().contains(right.toString(), - Qt::CaseInsensitive); - } - - if (left.type() == QVariant::Type::Map && - right.canConvert(QMetaType::QString)) - { - return left.toMap().contains(right.toString()); - } - - if (left.type() == QVariant::Type::List) - { - return left.toList().contains(right); - } - - if (left.canConvert(QMetaType::QString) && - right.canConvert(QMetaType::QString)) - { - return left.toString().contains(right.toString(), - Qt::CaseInsensitive); - } - - return false; - case STARTS_WITH: - if (left.type() == QVariant::Type::StringList && - right.canConvert(QMetaType::QString)) - { - auto list = left.toStringList(); - return !list.isEmpty() && - list.first().compare(right.toString(), - Qt::CaseInsensitive); - } - - if (left.type() == QVariant::Type::List) - { - return left.toList().startsWith(right); - } - - if (left.canConvert(QMetaType::QString) && - right.canConvert(QMetaType::QString)) - { - return left.toString().startsWith(right.toString(), - Qt::CaseInsensitive); - } - - return false; - - case ENDS_WITH: - if (left.type() == QVariant::Type::StringList && - right.canConvert(QMetaType::QString)) - { - auto list = left.toStringList(); - return !list.isEmpty() && - list.last().compare(right.toString(), - Qt::CaseInsensitive); - } - - if (left.type() == QVariant::Type::List) - { - return left.toList().endsWith(right); - } - - if (left.canConvert(QMetaType::QString) && - right.canConvert(QMetaType::QString)) - { - return left.toString().endsWith(right.toString(), - Qt::CaseInsensitive); - } - - return false; - case MATCH: { - if (!left.canConvert(QMetaType::QString)) - { - return false; - } - - auto matching = left.toString(); - - switch (right.type()) - { - case QVariant::Type::RegularExpression: { - return right.toRegularExpression() - .match(matching) - .hasMatch(); - } - case QVariant::Type::List: { - auto list = right.toList(); - - // list must be two items - if (list.size() != 2) - return false; - - // list must be a regular expression and an int - if (list.at(0).type() != - QVariant::Type::RegularExpression || - list.at(1).type() != QVariant::Type::Int) - return false; - - auto match = - list.at(0).toRegularExpression().match(matching); - - // if matched, return nth capture group. Otherwise, return false - if (match.hasMatch()) - return match.captured(list.at(1).toInt()); - else - return false; - } - default: - return false; - } - } - default: - return false; - } -} - -QString BinaryOperation::debug() const -{ - return QString("(%1 %2 %3)") - .arg(this->left_->debug(), tokenTypeToInfoString(this->op_), - this->right_->debug()); -} - -QString BinaryOperation::filterString() const -{ - const auto opText = [&]() -> QString { - switch (this->op_) - { - case AND: - return "&&"; - case OR: - return "||"; - case PLUS: - return "+"; - case MINUS: - return "-"; - case MULTIPLY: - return "*"; - case DIVIDE: - return "/"; - case MOD: - return "%"; - case EQ: - return "=="; - case NEQ: - return "!="; - case LT: - return "<"; - case GT: - return ">"; - case LTE: - return "<="; - case GTE: - return ">="; - case CONTAINS: - return "contains"; - case STARTS_WITH: - return "startswith"; - case ENDS_WITH: - return "endswith"; - case MATCH: - return "match"; - default: - return QString(); - } - }(); - - return QString("(%1) %2 (%3)") - .arg(this->left_->filterString()) - .arg(opText) - .arg(this->right_->filterString()); -} - -// UnaryOperation - -UnaryOperation::UnaryOperation(TokenType op, ExpressionPtr right) - : op_(op) - , right_(std::move(right)) -{ -} - -QVariant UnaryOperation::execute(const ContextMap &context) const -{ - auto right = this->right_->execute(context); - switch (this->op_) - { - case NOT: - if (right.canConvert()) - return !right.toBool(); - return false; - default: - return false; - } -} - -QString UnaryOperation::debug() const -{ - return QString("(%1 %2)").arg(tokenTypeToInfoString(this->op_), - this->right_->debug()); -} - -QString UnaryOperation::filterString() const -{ - const auto opText = [&]() -> QString { - switch (this->op_) - { - case NOT: - return "!"; - default: - return QString(); - } - }(); - - return QString("%1(%2)").arg(opText).arg(this->right_->filterString()); -} - -} // namespace filterparser diff --git a/src/controllers/filters/parser/Types.hpp b/src/controllers/filters/parser/Types.hpp deleted file mode 100644 index 0d8c7a5b2..000000000 --- a/src/controllers/filters/parser/Types.hpp +++ /dev/null @@ -1,162 +0,0 @@ -#pragma once - -#include "messages/Message.hpp" - -#include - -namespace filterparser { - -using MessagePtr = std::shared_ptr; -using ContextMap = QMap; - -enum TokenType { - // control - CONTROL_START = 0, - AND = 1, - OR = 2, - LP = 3, - RP = 4, - LIST_START = 5, - LIST_END = 6, - COMMA = 7, - CONTROL_END = 19, - - // binary operator - BINARY_START = 20, - EQ = 21, - NEQ = 22, - LT = 23, - GT = 24, - LTE = 25, - GTE = 26, - CONTAINS = 27, - STARTS_WITH = 28, - ENDS_WITH = 29, - MATCH = 30, - BINARY_END = 49, - - // unary operator - UNARY_START = 50, - NOT = 51, - UNARY_END = 99, - - // math operators - MATH_START = 100, - PLUS = 101, - MINUS = 102, - MULTIPLY = 103, - DIVIDE = 104, - MOD = 105, - MATH_END = 149, - - // other types - OTHER_START = 150, - STRING = 151, - INT = 152, - IDENTIFIER = 153, - REGULAR_EXPRESSION = 154, - - NONE = 200 -}; - -bool convertVariantTypes(QVariant &a, QVariant &b, int type); -QString tokenTypeToInfoString(TokenType type); - -class Expression -{ -public: - virtual ~Expression() = default; - - virtual QVariant execute(const ContextMap &) const - { - return false; - } - - virtual QString debug() const - { - return "(false)"; - } - - virtual QString filterString() const - { - return ""; - } -}; - -using ExpressionPtr = std::unique_ptr; - -class ValueExpression : public Expression -{ -public: - ValueExpression(QVariant value, TokenType type); - TokenType type(); - - QVariant execute(const ContextMap &context) const override; - QString debug() const override; - QString filterString() const override; - -private: - QVariant value_; - TokenType type_; -}; - -class RegexExpression : public Expression -{ -public: - RegexExpression(QString regex, bool caseInsensitive); - - QVariant execute(const ContextMap &context) const override; - QString debug() const override; - QString filterString() const override; - -private: - QString regexString_; - bool caseInsensitive_; - QRegularExpression regex_; -}; - -using ExpressionList = std::vector>; - -class ListExpression : public Expression -{ -public: - ListExpression(ExpressionList list); - - QVariant execute(const ContextMap &context) const override; - QString debug() const override; - QString filterString() const override; - -private: - ExpressionList list_; -}; - -class BinaryOperation : public Expression -{ -public: - BinaryOperation(TokenType op, ExpressionPtr left, ExpressionPtr right); - - QVariant execute(const ContextMap &context) const override; - QString debug() const override; - QString filterString() const override; - -private: - TokenType op_; - ExpressionPtr left_; - ExpressionPtr right_; -}; - -class UnaryOperation : public Expression -{ -public: - UnaryOperation(TokenType op, ExpressionPtr right); - - QVariant execute(const ContextMap &context) const override; - QString debug() const override; - QString filterString() const override; - -private: - TokenType op_; - ExpressionPtr right_; -}; - -} // namespace filterparser diff --git a/src/controllers/highlights/BadgeHighlightModel.cpp b/src/controllers/highlights/BadgeHighlightModel.cpp index 9ea8a2f93..3b61a01bf 100644 --- a/src/controllers/highlights/BadgeHighlightModel.cpp +++ b/src/controllers/highlights/BadgeHighlightModel.cpp @@ -1,7 +1,11 @@ -#include "BadgeHighlightModel.hpp" +#include "controllers/highlights/BadgeHighlightModel.hpp" #include "Application.hpp" +#include "common/SignalVectorModel.hpp" +#include "controllers/highlights/HighlightBadge.hpp" +#include "controllers/highlights/HighlightPhrase.hpp" #include "messages/Emote.hpp" +#include "providers/twitch/TwitchBadges.hpp" #include "singletons/Settings.hpp" #include "util/StandardItemHelper.hpp" @@ -9,7 +13,7 @@ namespace chatterino { // commandmodel BadgeHighlightModel::BadgeHighlightModel(QObject *parent) - : SignalVectorModel(5, parent) + : SignalVectorModel(6, parent) { } @@ -28,6 +32,7 @@ HighlightBadge BadgeHighlightModel::getItemFromRow( return HighlightBadge{ original.badgeName(), row[Column::Badge]->data(Qt::DisplayRole).toString(), + row[Column::ShowInMentions]->data(Qt::CheckStateRole).toBool(), row[Column::FlashTaskbar]->data(Qt::CheckStateRole).toBool(), row[Column::PlaySound]->data(Qt::CheckStateRole).toBool(), row[Column::SoundPath]->data(Qt::UserRole).toString(), @@ -42,12 +47,13 @@ void BadgeHighlightModel::getRowFromItem(const HighlightBadge &item, using Column = BadgeHighlightModel::Column; setStringItem(row[Column::Badge], item.displayName(), false, true); + setBoolItem(row[Column::ShowInMentions], item.showInMentions()); setBoolItem(row[Column::FlashTaskbar], item.hasAlert()); setBoolItem(row[Column::PlaySound], item.hasSound()); setFilePathItem(row[Column::SoundPath], item.getSoundUrl()); setColorItem(row[Column::Color], *item.getColor()); - TwitchBadges::instance()->getBadgeIcon( + getApp()->getTwitchBadges()->getBadgeIcon( item.badgeName(), [item, row](QString /*name*/, const QIconPtr pixmap) { row[Column::Badge]->setData(QVariant(*pixmap), Qt::DecorationRole); }); diff --git a/src/controllers/highlights/BadgeHighlightModel.hpp b/src/controllers/highlights/BadgeHighlightModel.hpp index ffe77d310..20600bb67 100644 --- a/src/controllers/highlights/BadgeHighlightModel.hpp +++ b/src/controllers/highlights/BadgeHighlightModel.hpp @@ -1,14 +1,13 @@ #pragma once -#include - #include "common/SignalVectorModel.hpp" -#include "controllers/highlights/HighlightBadge.hpp" -#include "providers/twitch/TwitchBadges.hpp" + +#include namespace chatterino { class HighlightController; +class HighlightBadge; class BadgeHighlightModel : public SignalVectorModel { @@ -17,20 +16,20 @@ public: enum Column { Badge = 0, - FlashTaskbar = 1, - PlaySound = 2, - SoundPath = 3, - Color = 4 + ShowInMentions = 1, + FlashTaskbar = 2, + PlaySound = 3, + SoundPath = 4, + Color = 5 }; protected: // vector into model row - virtual HighlightBadge getItemFromRow( - std::vector &row, - const HighlightBadge &original) override; + HighlightBadge getItemFromRow(std::vector &row, + const HighlightBadge &original) override; - virtual void getRowFromItem(const HighlightBadge &item, - std::vector &row) override; + void getRowFromItem(const HighlightBadge &item, + std::vector &row) override; }; } // namespace chatterino diff --git a/src/controllers/highlights/HighlightBadge.cpp b/src/controllers/highlights/HighlightBadge.cpp index 8195cbd36..4fe7f44d5 100644 --- a/src/controllers/highlights/HighlightBadge.cpp +++ b/src/controllers/highlights/HighlightBadge.cpp @@ -1,7 +1,7 @@ -#include "HighlightBadge.hpp" +#include "controllers/highlights/HighlightBadge.hpp" -#include "messages/SharedMessageBuilder.hpp" -#include "singletons/Resources.hpp" +#include "providers/twitch/TwitchBadge.hpp" +#include "util/IrcHelpers.hpp" namespace chatterino { @@ -9,27 +9,31 @@ QColor HighlightBadge::FALLBACK_HIGHLIGHT_COLOR = QColor(127, 63, 73, 127); bool HighlightBadge::operator==(const HighlightBadge &other) const { - return std::tie(this->badgeName_, this->displayName_, this->hasSound_, - this->hasAlert_, this->soundUrl_, this->color_) == - std::tie(other.badgeName_, other.displayName_, other.hasSound_, - other.hasAlert_, other.soundUrl_, other.color_); + return std::tie(this->badgeName_, this->displayName_, this->showInMentions_, + this->hasSound_, this->hasAlert_, this->soundUrl_, + this->color_) == + std::tie(other.badgeName_, other.displayName_, other.showInMentions_, + other.hasSound_, other.hasAlert_, other.soundUrl_, + other.color_); } HighlightBadge::HighlightBadge(const QString &badgeName, - const QString &displayName, bool hasAlert, - bool hasSound, const QString &soundUrl, - QColor color) - : HighlightBadge(badgeName, displayName, hasAlert, hasSound, soundUrl, - std::make_shared(color)) + const QString &displayName, bool showInMentions, + bool hasAlert, bool hasSound, + const QString &soundUrl, QColor color) + : HighlightBadge(badgeName, displayName, showInMentions, hasAlert, hasSound, + soundUrl, std::make_shared(color)) { } HighlightBadge::HighlightBadge(const QString &badgeName, - const QString &displayName, bool hasAlert, - bool hasSound, const QString &soundUrl, + const QString &displayName, bool showInMentions, + bool hasAlert, bool hasSound, + const QString &soundUrl, std::shared_ptr color) : badgeName_(badgeName) , displayName_(displayName) + , showInMentions_(showInMentions) , hasAlert_(hasAlert) , hasSound_(hasSound) , soundUrl_(soundUrl) @@ -54,6 +58,11 @@ const QString &HighlightBadge::displayName() const return this->displayName_; } +bool HighlightBadge::showInMentions() const +{ + return this->showInMentions_; +} + bool HighlightBadge::hasAlert() const { return this->hasAlert_; @@ -87,7 +96,7 @@ bool HighlightBadge::compare(const QString &id, const Badge &badge) const { if (this->hasVersions_) { - auto parts = SharedMessageBuilder::slashKeyValue(id); + auto parts = slashKeyValue(id); return parts.first.compare(badge.key_, Qt::CaseInsensitive) == 0 && parts.second.compare(badge.value_, Qt::CaseInsensitive) == 0; } diff --git a/src/controllers/highlights/HighlightBadge.hpp b/src/controllers/highlights/HighlightBadge.hpp index c3daf3045..0be6895de 100644 --- a/src/controllers/highlights/HighlightBadge.hpp +++ b/src/controllers/highlights/HighlightBadge.hpp @@ -1,29 +1,35 @@ #pragma once -#include "providers/twitch/TwitchBadge.hpp" -#include "util/RapidJsonSerializeQString.hpp" #include "util/RapidjsonHelpers.hpp" +#include "util/RapidJsonSerializeQString.hpp" +#include +#include #include #include -#include + +#include namespace chatterino { + +class Badge; + class HighlightBadge { public: bool operator==(const HighlightBadge &other) const; HighlightBadge(const QString &badgeName, const QString &displayName, - bool hasAlert, bool hasSound, const QString &soundUrl, - QColor color); + bool showInMentions, bool hasAlert, bool hasSound, + const QString &soundUrl, QColor color); HighlightBadge(const QString &badgeName, const QString &displayName, - bool hasAlert, bool hasSound, const QString &soundUrl, - std::shared_ptr color); + bool showInMentions, bool hasAlert, bool hasSound, + const QString &soundUrl, std::shared_ptr color); const QString &badgeName() const; const QString &displayName() const; + bool showInMentions() const; bool hasAlert() const; bool hasSound() const; bool isMatch(const Badge &badge) const; @@ -53,6 +59,7 @@ private: QString badgeName_; QString displayName_; + bool showInMentions_; bool hasAlert_; bool hasSound_; QUrl soundUrl_; @@ -75,6 +82,7 @@ struct Serialize { chatterino::rj::set(ret, "name", value.badgeName(), a); chatterino::rj::set(ret, "displayName", value.displayName(), a); + chatterino::rj::set(ret, "showInMentions", value.showInMentions(), a); chatterino::rj::set(ret, "alert", value.hasAlert(), a); chatterino::rj::set(ret, "sound", value.hasSound(), a); chatterino::rj::set(ret, "soundUrl", value.getSoundUrl().toString(), a); @@ -94,11 +102,12 @@ struct Deserialize { { PAJLADA_REPORT_ERROR(error); return chatterino::HighlightBadge(QString(), QString(), false, - false, "", QColor()); + false, false, "", QColor()); } QString _name; QString _displayName; + bool _showInMentions = false; bool _hasAlert = true; bool _hasSound = false; QString _soundUrl; @@ -106,6 +115,7 @@ struct Deserialize { chatterino::rj::getSafe(value, "name", _name); chatterino::rj::getSafe(value, "displayName", _displayName); + chatterino::rj::getSafe(value, "showInMentions", _showInMentions); chatterino::rj::getSafe(value, "alert", _hasAlert); chatterino::rj::getSafe(value, "sound", _hasSound); chatterino::rj::getSafe(value, "soundUrl", _soundUrl); @@ -113,10 +123,13 @@ struct Deserialize { auto _color = QColor(encodedColor); if (!_color.isValid()) + { _color = chatterino::HighlightBadge::FALLBACK_HIGHLIGHT_COLOR; + } - return chatterino::HighlightBadge(_name, _displayName, _hasAlert, - _hasSound, _soundUrl, _color); + return chatterino::HighlightBadge(_name, _displayName, _showInMentions, + _hasAlert, _hasSound, _soundUrl, + _color); } }; diff --git a/src/controllers/highlights/HighlightBlacklistModel.cpp b/src/controllers/highlights/HighlightBlacklistModel.cpp index 0ff68bc0b..8bfc09867 100644 --- a/src/controllers/highlights/HighlightBlacklistModel.cpp +++ b/src/controllers/highlights/HighlightBlacklistModel.cpp @@ -1,6 +1,7 @@ #include "controllers/highlights/HighlightBlacklistModel.hpp" #include "Application.hpp" +#include "controllers/highlights/HighlightBlacklistUser.hpp" #include "singletons/Settings.hpp" #include "util/StandardItemHelper.hpp" diff --git a/src/controllers/highlights/HighlightBlacklistModel.hpp b/src/controllers/highlights/HighlightBlacklistModel.hpp index d4acc474d..8de3b07ea 100644 --- a/src/controllers/highlights/HighlightBlacklistModel.hpp +++ b/src/controllers/highlights/HighlightBlacklistModel.hpp @@ -1,12 +1,12 @@ #pragma once +#include "common/SignalVectorModel.hpp" + #include -#include "common/SignalVectorModel.hpp" -#include "controllers/highlights/HighlightBlacklistUser.hpp" - namespace chatterino { +class HighlightBlacklistUser; class HighlightController; class HighlightBlacklistModel : public SignalVectorModel @@ -21,13 +21,13 @@ public: protected: // turn a vector item into a model row - virtual HighlightBlacklistUser getItemFromRow( + HighlightBlacklistUser getItemFromRow( std::vector &row, const HighlightBlacklistUser &original) override; // turns a row in the model into a vector item - virtual void getRowFromItem(const HighlightBlacklistUser &item, - std::vector &row) override; + void getRowFromItem(const HighlightBlacklistUser &item, + std::vector &row) override; }; } // namespace chatterino diff --git a/src/controllers/highlights/HighlightBlacklistUser.hpp b/src/controllers/highlights/HighlightBlacklistUser.hpp index 08a56ba4d..0875a1610 100644 --- a/src/controllers/highlights/HighlightBlacklistUser.hpp +++ b/src/controllers/highlights/HighlightBlacklistUser.hpp @@ -1,11 +1,11 @@ #pragma once -#include "util/RapidJsonSerializeQString.hpp" #include "util/RapidjsonHelpers.hpp" +#include "util/RapidJsonSerializeQString.hpp" +#include #include #include -#include #include diff --git a/src/controllers/highlights/HighlightController.cpp b/src/controllers/highlights/HighlightController.cpp index 632f76cc4..78d072a32 100644 --- a/src/controllers/highlights/HighlightController.cpp +++ b/src/controllers/highlights/HighlightController.cpp @@ -1,6 +1,17 @@ #include "controllers/highlights/HighlightController.hpp" +#include "Application.hpp" #include "common/QLogging.hpp" +#include "controllers/accounts/AccountController.hpp" +#include "controllers/highlights/HighlightBadge.hpp" +#include "controllers/highlights/HighlightPhrase.hpp" +#include "messages/Message.hpp" +#include "messages/MessageBuilder.hpp" +#include "providers/colors/ColorProvider.hpp" +#include "providers/twitch/TwitchAccount.hpp" +#include "providers/twitch/TwitchBadge.hpp" +#include "singletons/Paths.hpp" +#include "singletons/Settings.hpp" namespace { @@ -11,23 +22,25 @@ auto highlightPhraseCheck(const HighlightPhrase &highlight) -> HighlightCheck return HighlightCheck{ [highlight](const auto &args, const auto &badges, const auto &senderName, const auto &originalMessage, - const auto self) -> boost::optional { + const auto &flags, + const auto self) -> std::optional { (void)args; // unused (void)badges; // unused (void)senderName; // unused + (void)flags; // unused if (self) { // Phrase checks should ignore highlights from the user - return boost::none; + return std::nullopt; } if (!highlight.isMatch(originalMessage)) { - return boost::none; + return std::nullopt; } - boost::optional highlightSoundUrl; + std::optional highlightSoundUrl; if (highlight.hasCustomSound()) { highlightSoundUrl = highlight.getSoundUrl(); @@ -48,9 +61,8 @@ void rebuildSubscriptionHighlights(Settings &settings, { auto highlightSound = settings.enableSubHighlightSound.getValue(); auto highlightAlert = settings.enableSubHighlightTaskbar.getValue(); - auto highlightSoundUrlValue = - settings.whisperHighlightSoundUrl.getValue(); - boost::optional highlightSoundUrl; + auto highlightSoundUrlValue = settings.subHighlightSoundUrl.getValue(); + std::optional highlightSoundUrl; if (!highlightSoundUrlValue.isEmpty()) { highlightSoundUrl = highlightSoundUrlValue; @@ -60,16 +72,17 @@ void rebuildSubscriptionHighlights(Settings &settings, checks.emplace_back(HighlightCheck{ [=](const auto &args, const auto &badges, const auto &senderName, - const auto &originalMessage, - const auto self) -> boost::optional { + const auto &originalMessage, const auto &flags, + const auto self) -> std::optional { (void)badges; // unused (void)senderName; // unused (void)originalMessage; // unused + (void)flags; // unused (void)self; // unused if (!args.isSubscriptionMessage) { - return boost::none; + return std::nullopt; } auto highlightColor = @@ -95,7 +108,7 @@ void rebuildWhisperHighlights(Settings &settings, auto highlightAlert = settings.enableWhisperHighlightTaskbar.getValue(); auto highlightSoundUrlValue = settings.whisperHighlightSoundUrl.getValue(); - boost::optional highlightSoundUrl; + std::optional highlightSoundUrl; if (!highlightSoundUrlValue.isEmpty()) { highlightSoundUrl = highlightSoundUrlValue; @@ -105,16 +118,17 @@ void rebuildWhisperHighlights(Settings &settings, checks.emplace_back(HighlightCheck{ [=](const auto &args, const auto &badges, const auto &senderName, - const auto &originalMessage, - const auto self) -> boost::optional { + const auto &originalMessage, const auto &flags, + const auto self) -> std::optional { (void)badges; // unused (void)senderName; // unused (void)originalMessage; // unused + (void)flags; // unused (void)self; // unused if (!args.isReceivedWhisper) { - return boost::none; + return std::nullopt; } return HighlightResult{ @@ -128,13 +142,52 @@ void rebuildWhisperHighlights(Settings &settings, } } +void rebuildReplyThreadHighlight(Settings &settings, + std::vector &checks) +{ + if (settings.enableThreadHighlight) + { + auto highlightSound = settings.enableThreadHighlightSound.getValue(); + auto highlightAlert = settings.enableThreadHighlightTaskbar.getValue(); + auto highlightSoundUrlValue = + settings.threadHighlightSoundUrl.getValue(); + std::optional highlightSoundUrl; + if (!highlightSoundUrlValue.isEmpty()) + { + highlightSoundUrl = highlightSoundUrlValue; + } + auto highlightInMentions = + settings.showThreadHighlightInMentions.getValue(); + checks.emplace_back(HighlightCheck{ + [=](const auto & /*args*/, const auto & /*badges*/, + const auto & /*senderName*/, const auto & /*originalMessage*/, + const auto &flags, + const auto self) -> std::optional { + if (flags.has(MessageFlag::SubscribedThread) && !self) + { + return HighlightResult{ + highlightAlert, + highlightSound, + highlightSoundUrl, + ColorProvider::instance().color( + ColorType::ThreadMessageHighlight), + highlightInMentions, + }; + } + + return std::nullopt; + }}); + } +} + void rebuildMessageHighlights(Settings &settings, std::vector &checks) { - auto currentUser = getIApp()->getAccounts()->twitch.getCurrent(); + auto currentUser = getApp()->getAccounts()->twitch.getCurrent(); QString currentUsername = currentUser->getUserName(); - if (settings.enableSelfHighlight && !currentUsername.isEmpty()) + if (settings.enableSelfHighlight && !currentUsername.isEmpty() && + !currentUser->isAnon()) { HighlightPhrase highlight( currentUsername, settings.showSelfHighlightInMentions, @@ -151,6 +204,43 @@ void rebuildMessageHighlights(Settings &settings, { checks.emplace_back(highlightPhraseCheck(highlight)); } + + if (settings.enableAutomodHighlight) + { + const auto highlightSound = + settings.enableAutomodHighlightSound.getValue(); + const auto highlightAlert = + settings.enableAutomodHighlightTaskbar.getValue(); + const auto highlightSoundUrlValue = + settings.automodHighlightSoundUrl.getValue(); + auto highlightColor = + ColorProvider::instance().color(ColorType::AutomodHighlight); + + checks.emplace_back(HighlightCheck{ + [=](const auto & /*args*/, const auto & /*badges*/, + const auto & /*senderName*/, const auto & /*originalMessage*/, + const auto &flags, + const auto /*self*/) -> std::optional { + if (!flags.has(MessageFlag::AutoModOffendingMessage)) + { + return std::nullopt; + } + + std::optional highlightSoundUrl; + if (!highlightSoundUrlValue.isEmpty()) + { + highlightSoundUrl = highlightSoundUrlValue; + } + + return HighlightResult{ + highlightAlert, // alert + highlightSound, // playSound + highlightSoundUrl, // customSoundUrl + highlightColor, // color + false, // showInMentions + }; + }}); + } } void rebuildUserHighlights(Settings &settings, @@ -158,32 +248,65 @@ void rebuildUserHighlights(Settings &settings, { auto userHighlights = settings.highlightedUsers.readOnly(); + if (settings.enableSelfMessageHighlight) + { + bool showInMentions = settings.showSelfMessageHighlightInMentions; + + checks.emplace_back(HighlightCheck{ + [showInMentions]( + const auto &args, const auto &badges, const auto &senderName, + const auto &originalMessage, const auto &flags, + const auto self) -> std::optional { + (void)args; //unused + (void)badges; //unused + (void)senderName; //unused + (void)flags; //unused + (void)originalMessage; //unused + + if (!self) + { + return std::nullopt; + } + + // Highlight color is provided by the ColorProvider and will be updated accordingly + auto highlightColor = ColorProvider::instance().color( + ColorType::SelfMessageHighlight); + + return HighlightResult{false, false, (QUrl) nullptr, + highlightColor, showInMentions}; + }}); + } + for (const auto &highlight : *userHighlights) { checks.emplace_back(HighlightCheck{ [highlight](const auto &args, const auto &badges, const auto &senderName, const auto &originalMessage, - const auto self) -> boost::optional { + const auto &flags, + const auto self) -> std::optional { (void)args; // unused (void)badges; // unused (void)originalMessage; // unused + (void)flags; // unused (void)self; // unused if (!highlight.isMatch(senderName)) { - return boost::none; + return std::nullopt; } - boost::optional highlightSoundUrl; + std::optional highlightSoundUrl; if (highlight.hasCustomSound()) { highlightSoundUrl = highlight.getSoundUrl(); } return HighlightResult{ - highlight.hasAlert(), highlight.hasSound(), - highlightSoundUrl, highlight.getColor(), - highlight.showInMentions(), + highlight.hasAlert(), // + highlight.hasSound(), // + highlightSoundUrl, // + highlight.getColor(), // + highlight.showInMentions(), // }; }}); } @@ -199,33 +322,35 @@ void rebuildBadgeHighlights(Settings &settings, checks.emplace_back(HighlightCheck{ [highlight](const auto &args, const auto &badges, const auto &senderName, const auto &originalMessage, - const auto self) -> boost::optional { + const auto &flags, + const auto self) -> std::optional { (void)args; // unused (void)senderName; // unused (void)originalMessage; // unused + (void)flags; // unused (void)self; // unused for (const Badge &badge : badges) { if (highlight.isMatch(badge)) { - boost::optional highlightSoundUrl; + std::optional highlightSoundUrl; if (highlight.hasCustomSound()) { highlightSoundUrl = highlight.getSoundUrl(); } return HighlightResult{ - highlight.hasAlert(), - highlight.hasSound(), - highlightSoundUrl, - highlight.getColor(), - false, // showInMentions + highlight.hasAlert(), // + highlight.hasSound(), // + highlightSoundUrl, // + highlight.getColor(), // + highlight.showInMentions(), // }; } } - return boost::none; + return std::nullopt; }}); } } @@ -234,17 +359,126 @@ void rebuildBadgeHighlights(Settings &settings, namespace chatterino { -void HighlightController::initialize(Settings &settings, Paths & /*paths*/) +HighlightResult::HighlightResult(bool _alert, bool _playSound, + std::optional _customSoundUrl, + std::shared_ptr _color, + bool _showInMentions) + : alert(_alert) + , playSound(_playSound) + , customSoundUrl(std::move(_customSoundUrl)) + , color(std::move(_color)) + , showInMentions(_showInMentions) { +} + +HighlightResult HighlightResult::emptyResult() +{ + return { + false, false, std::nullopt, nullptr, false, + }; +} + +bool HighlightResult::operator==(const HighlightResult &other) const +{ + if (this->alert != other.alert) + { + return false; + } + if (this->playSound != other.playSound) + { + return false; + } + if (this->customSoundUrl != other.customSoundUrl) + { + return false; + } + + if (this->color && other.color) + { + if (*this->color != *other.color) + { + return false; + } + } + + if (this->showInMentions != other.showInMentions) + { + return false; + } + + return true; +} + +bool HighlightResult::operator!=(const HighlightResult &other) const +{ + return !(*this == other); +} + +bool HighlightResult::empty() const +{ + return !this->alert && !this->playSound && + !this->customSoundUrl.has_value() && !this->color && + !this->showInMentions; +} + +bool HighlightResult::full() const +{ + return this->alert && this->playSound && this->customSoundUrl.has_value() && + this->color && this->showInMentions; +} + +std::ostream &operator<<(std::ostream &os, const HighlightResult &result) +{ + os << "Alert: " << (result.alert ? "Yes" : "No") << ", " + << "Play sound: " << (result.playSound ? "Yes" : "No") << " (" + << (result.customSoundUrl + ? result.customSoundUrl->toString().toStdString() + : "") + << ")" + << ", " + << "Color: " << (result.color ? result.color->name().toStdString() : "") + << ", " + << "Show in mentions: " << (result.showInMentions ? "Yes" : "No"); + return os; +} + +HighlightController::HighlightController(Settings &settings, + AccountController *accounts) +{ + assert(accounts != nullptr); + + this->rebuildListener_.addSetting(settings.enableSelfHighlight); + this->rebuildListener_.addSetting(settings.enableSelfHighlightSound); + this->rebuildListener_.addSetting(settings.enableSelfHighlightTaskbar); + this->rebuildListener_.addSetting(settings.selfHighlightSoundUrl); + this->rebuildListener_.addSetting(settings.showSelfHighlightInMentions); + this->rebuildListener_.addSetting(settings.enableWhisperHighlight); this->rebuildListener_.addSetting(settings.enableWhisperHighlightSound); this->rebuildListener_.addSetting(settings.enableWhisperHighlightTaskbar); this->rebuildListener_.addSetting(settings.whisperHighlightSoundUrl); - this->rebuildListener_.addSetting(settings.whisperHighlightColor); - this->rebuildListener_.addSetting(settings.enableSelfHighlight); + this->rebuildListener_.addSetting(settings.enableSubHighlight); this->rebuildListener_.addSetting(settings.enableSubHighlightSound); this->rebuildListener_.addSetting(settings.enableSubHighlightTaskbar); + this->rebuildListener_.addSetting(settings.enableSelfMessageHighlight); + this->rebuildListener_.addSetting( + settings.showSelfMessageHighlightInMentions); + // We do not need to rebuild the listener for the selfMessagesHighlightColor + // The color is dynamically fetched any time the self message highlight is triggered + this->rebuildListener_.addSetting(settings.subHighlightSoundUrl); + + this->rebuildListener_.addSetting(settings.enableThreadHighlight); + this->rebuildListener_.addSetting(settings.enableThreadHighlightSound); + this->rebuildListener_.addSetting(settings.enableThreadHighlightTaskbar); + this->rebuildListener_.addSetting(settings.threadHighlightSoundUrl); + this->rebuildListener_.addSetting(settings.showThreadHighlightInMentions); + + this->rebuildListener_.addSetting(settings.enableAutomodHighlight); + this->rebuildListener_.addSetting(settings.showAutomodInMentions); + this->rebuildListener_.addSetting(settings.enableAutomodHighlightSound); + this->rebuildListener_.addSetting(settings.enableAutomodHighlightTaskbar); + this->rebuildListener_.addSetting(settings.automodHighlightSoundUrl); this->rebuildListener_.setCB([this, &settings] { qCDebug(chatterinoHighlights) @@ -253,7 +487,7 @@ void HighlightController::initialize(Settings &settings, Paths & /*paths*/) }); this->signalHolder_.managedConnect( - getCSettings().highlightedBadges.delayedItemsChanged, + getSettings()->highlightedBadges.delayedItemsChanged, [this, &settings] { qCDebug(chatterinoHighlights) << "Rebuild checks because highlight badges changed"; @@ -261,26 +495,26 @@ void HighlightController::initialize(Settings &settings, Paths & /*paths*/) }); this->signalHolder_.managedConnect( - getCSettings().highlightedUsers.delayedItemsChanged, [this, &settings] { + getSettings()->highlightedUsers.delayedItemsChanged, [this, &settings] { qCDebug(chatterinoHighlights) << "Rebuild checks because highlight users changed"; this->rebuildChecks(settings); }); this->signalHolder_.managedConnect( - getCSettings().highlightedMessages.delayedItemsChanged, + getSettings()->highlightedMessages.delayedItemsChanged, [this, &settings] { qCDebug(chatterinoHighlights) << "Rebuild checks because highlight messages changed"; this->rebuildChecks(settings); }); - getIApp()->getAccounts()->twitch.currentUserChanged.connect( - [this, &settings] { + this->bConnections.emplace_back( + accounts->twitch.currentUserChanged.connect([this, &settings] { qCDebug(chatterinoHighlights) << "Rebuild checks because user swapped accounts"; this->rebuildChecks(settings); - }); + })); this->rebuildChecks(settings); } @@ -292,22 +526,25 @@ void HighlightController::rebuildChecks(Settings &settings) checks->clear(); // CURRENT ORDER: - // Subscription -> Whisper -> User -> Message -> Badge + // Subscription -> Whisper -> Message -> User -> Reply Threads -> Badge rebuildSubscriptionHighlights(settings, *checks); rebuildWhisperHighlights(settings, *checks); + rebuildMessageHighlights(settings, *checks); + rebuildUserHighlights(settings, *checks); - rebuildMessageHighlights(settings, *checks); + rebuildReplyThreadHighlight(settings, *checks); rebuildBadgeHighlights(settings, *checks); } std::pair HighlightController::check( const MessageParseArgs &args, const std::vector &badges, - const QString &senderName, const QString &originalMessage) const + const QString &senderName, const QString &originalMessage, + const MessageFlags &messageFlags) const { bool highlighted = false; auto result = HighlightResult::emptyResult(); @@ -315,13 +552,13 @@ std::pair HighlightController::check( // Access for checking const auto checks = this->checks_.accessConst(); - auto currentUser = getIApp()->getAccounts()->twitch.getCurrent(); + auto currentUser = getApp()->getAccounts()->twitch.getCurrent(); auto self = (senderName == currentUser->getUserName()); for (const auto &check : *checks) { - if (auto checkResult = - check.cb(args, badges, senderName, originalMessage, self); + if (auto checkResult = check.cb(args, badges, senderName, + originalMessage, messageFlags, self); checkResult) { highlighted = true; diff --git a/src/controllers/highlights/HighlightController.hpp b/src/controllers/highlights/HighlightController.hpp index 0a1ecb2a9..488f628fb 100644 --- a/src/controllers/highlights/HighlightController.hpp +++ b/src/controllers/highlights/HighlightController.hpp @@ -1,43 +1,35 @@ #pragma once -#include "common/Singleton.hpp" #include "common/UniqueAccess.hpp" -#include "messages/MessageBuilder.hpp" -#include "providers/twitch/TwitchBadge.hpp" -#include "singletons/Paths.hpp" +#include "messages/MessageFlag.hpp" #include "singletons/Settings.hpp" +#include +#include +#include #include #include -#include #include #include +#include #include namespace chatterino { +class Badge; +struct MessageParseArgs; +class AccountController; + struct HighlightResult { HighlightResult(bool _alert, bool _playSound, - boost::optional _customSoundUrl, - std::shared_ptr _color, bool _showInMentions) - : alert(_alert) - , playSound(_playSound) - , customSoundUrl(std::move(_customSoundUrl)) - , color(std::move(_color)) - , showInMentions(_showInMentions) - { - } + std::optional _customSoundUrl, + std::shared_ptr _color, bool _showInMentions); /** * @brief Construct an empty HighlightResult with all side-effects disabled **/ - static HighlightResult emptyResult() - { - return { - false, false, boost::none, nullptr, false, - }; - } + static HighlightResult emptyResult(); /** * @brief true if highlight should trigger the taskbar to flash @@ -54,7 +46,7 @@ struct HighlightResult { * * May only be set if playSound is true **/ - boost::optional customSoundUrl{}; + std::optional customSoundUrl{}; /** * @brief set if highlight should set a background color @@ -66,97 +58,43 @@ struct HighlightResult { **/ bool showInMentions{false}; - bool operator==(const HighlightResult &other) const - { - if (this->alert != other.alert) - { - return false; - } - if (this->playSound != other.playSound) - { - return false; - } - if (this->customSoundUrl != other.customSoundUrl) - { - return false; - } - - if (this->color && other.color) - { - if (*this->color != *other.color) - { - return false; - } - } - - if (this->showInMentions != other.showInMentions) - { - return false; - } - - return true; - } - - bool operator!=(const HighlightResult &other) const - { - return !(*this == other); - } + bool operator==(const HighlightResult &other) const; + bool operator!=(const HighlightResult &other) const; /** * @brief Returns true if no side-effect has been enabled **/ - [[nodiscard]] bool empty() const - { - return !this->alert && !this->playSound && - !this->customSoundUrl.has_value() && !this->color && - !this->showInMentions; - } + [[nodiscard]] bool empty() const; /** * @brief Returns true if all side-effects have been enabled **/ - [[nodiscard]] bool full() const - { - return this->alert && this->playSound && - this->customSoundUrl.has_value() && this->color && - this->showInMentions; - } + [[nodiscard]] bool full() const; friend std::ostream &operator<<(std::ostream &os, - const HighlightResult &result) - { - os << "Alert: " << (result.alert ? "Yes" : "No") << ", " - << "Play sound: " << (result.playSound ? "Yes" : "No") << " (" - << (result.customSoundUrl - ? result.customSoundUrl.get().toString().toStdString() - : "") - << ")" - << ", " - << "Color: " - << (result.color ? result.color->name().toStdString() : "") << ", " - << "Show in mentions: " << (result.showInMentions ? "Yes" : "No"); - return os; - } + const HighlightResult &result); }; struct HighlightCheck { - using Checker = std::function( + using Checker = std::function( const MessageParseArgs &args, const std::vector &badges, - const QString &senderName, const QString &originalMessage, bool self)>; + const QString &senderName, const QString &originalMessage, + const MessageFlags &messageFlags, bool self)>; Checker cb; }; -class HighlightController final : public Singleton +class HighlightController final { public: - void initialize(Settings &settings, Paths &paths) override; + HighlightController(Settings &settings, AccountController *accounts); /** * @brief Checks the given message parameters if it matches our internal checks, and returns a result **/ [[nodiscard]] std::pair check( const MessageParseArgs &args, const std::vector &badges, - const QString &senderName, const QString &originalMessage) const; + const QString &senderName, const QString &originalMessage, + const MessageFlags &messageFlags) const; private: /** @@ -170,6 +108,7 @@ private: pajlada::SettingListener rebuildListener_; pajlada::Signals::SignalHolder signalHolder_; + std::vector bConnections; }; } // namespace chatterino diff --git a/src/controllers/highlights/HighlightModel.cpp b/src/controllers/highlights/HighlightModel.cpp index f1473a262..52f679375 100644 --- a/src/controllers/highlights/HighlightModel.cpp +++ b/src/controllers/highlights/HighlightModel.cpp @@ -1,6 +1,9 @@ -#include "HighlightModel.hpp" +#include "controllers/highlights/HighlightModel.hpp" #include "Application.hpp" +#include "common/SignalVectorModel.hpp" +#include "controllers/highlights/HighlightPhrase.hpp" +#include "providers/colors/ColorProvider.hpp" #include "singletons/Settings.hpp" #include "singletons/WindowManager.hpp" #include "util/StandardItemHelper.hpp" @@ -95,9 +98,8 @@ void HighlightModel::afterInit() QUrl(getSettings()->whisperHighlightSoundUrl.getValue()); setFilePathItem(whisperRow[Column::SoundPath], whisperSound, false); - // auto whisperColor = ColorProvider::instance().color(ColorType::Whisper); - // setColorItem(whisperRow[Column::Color], *whisperColor, false); - whisperRow[Column::Color]->setFlags(Qt::ItemFlag::NoItemFlags); + auto whisperColor = ColorProvider::instance().color(ColorType::Whisper); + setColorItem(whisperRow[Column::Color], *whisperColor, false); this->insertCustomRow(whisperRow, HighlightRowIndexes::WhisperRow); @@ -140,10 +142,7 @@ void HighlightModel::afterInit() redeemedRow[Column::PlaySound]->setFlags({}); redeemedRow[Column::UseRegex]->setFlags({}); redeemedRow[Column::CaseSensitive]->setFlags({}); - - QUrl RedeemedSound = - QUrl(getSettings()->redeemedHighlightSoundUrl.getValue()); - setFilePathItem(redeemedRow[Column::SoundPath], RedeemedSound, false); + redeemedRow[Column::SoundPath]->setFlags(Qt::NoItemFlags); auto RedeemedColor = ColorProvider::instance().color(ColorType::RedeemedHighlight); @@ -169,11 +168,7 @@ void HighlightModel::afterInit() firstMessageRow[Column::PlaySound]->setFlags({}); firstMessageRow[Column::UseRegex]->setFlags({}); firstMessageRow[Column::CaseSensitive]->setFlags({}); - - QUrl FirstMessageSound = - QUrl(getSettings()->firstMessageHighlightSoundUrl.getValue()); - setFilePathItem(firstMessageRow[Column::SoundPath], FirstMessageSound, - false); + firstMessageRow[Column::SoundPath]->setFlags(Qt::NoItemFlags); auto FirstMessageColor = ColorProvider::instance().color(ColorType::FirstMessageHighlight); @@ -181,6 +176,89 @@ void HighlightModel::afterInit() this->insertCustomRow(firstMessageRow, HighlightRowIndexes::FirstMessageRow); + + // Highlight settings for hype chats + std::vector elevatedMessageRow = this->createRow(); + setBoolItem(elevatedMessageRow[Column::Pattern], + getSettings()->enableElevatedMessageHighlight.getValue(), true, + false); + elevatedMessageRow[Column::Pattern]->setData("Hype Chats", Qt::DisplayRole); + elevatedMessageRow[Column::ShowInMentions]->setFlags({}); + // setBoolItem(elevatedMessageRow[Column::FlashTaskbar], + // getSettings()->enableElevatedMessageHighlightTaskbar.getValue(), + // true, false); + // setBoolItem(elevatedMessageRow[Column::PlaySound], + // getSettings()->enableElevatedMessageHighlightSound.getValue(), + // true, false); + elevatedMessageRow[Column::FlashTaskbar]->setFlags({}); + elevatedMessageRow[Column::PlaySound]->setFlags({}); + elevatedMessageRow[Column::UseRegex]->setFlags({}); + elevatedMessageRow[Column::CaseSensitive]->setFlags({}); + elevatedMessageRow[Column::SoundPath]->setFlags(Qt::NoItemFlags); + + auto elevatedMessageColor = + ColorProvider::instance().color(ColorType::ElevatedMessageHighlight); + setColorItem(elevatedMessageRow[Column::Color], *elevatedMessageColor, + false); + + this->insertCustomRow(elevatedMessageRow, + HighlightRowIndexes::ElevatedMessageRow); + + // Highlight settings for reply threads + std::vector threadMessageRow = this->createRow(); + setBoolItem(threadMessageRow[Column::Pattern], + getSettings()->enableThreadHighlight.getValue(), true, false); + threadMessageRow[Column::Pattern]->setData("Subscribed Reply Threads", + Qt::DisplayRole); + setBoolItem(threadMessageRow[Column::ShowInMentions], + getSettings()->showThreadHighlightInMentions.getValue(), true, + false); + setBoolItem(threadMessageRow[Column::FlashTaskbar], + getSettings()->enableThreadHighlightTaskbar.getValue(), true, + false); + setBoolItem(threadMessageRow[Column::PlaySound], + getSettings()->enableThreadHighlightSound.getValue(), true, + false); + threadMessageRow[Column::UseRegex]->setFlags({}); + threadMessageRow[Column::CaseSensitive]->setFlags({}); + + QUrl threadMessageSound = + QUrl(getSettings()->threadHighlightSoundUrl.getValue()); + setFilePathItem(threadMessageRow[Column::SoundPath], threadMessageSound, + false); + + auto threadMessageColor = + ColorProvider::instance().color(ColorType::ThreadMessageHighlight); + setColorItem(threadMessageRow[Column::Color], *threadMessageColor, false); + + this->insertCustomRow(threadMessageRow, + HighlightRowIndexes::ThreadMessageRow); + + // Highlight settings for automod caught messages + const std::vector automodRow = this->createRow(); + setBoolItem(automodRow[Column::Pattern], + getSettings()->enableAutomodHighlight.getValue(), true, false); + setBoolItem(automodRow[Column::ShowInMentions], + getSettings()->showAutomodInMentions.getValue(), true, false); + automodRow[Column::Pattern]->setData("AutoMod Caught Messages", + Qt::DisplayRole); + setBoolItem(automodRow[Column::FlashTaskbar], + getSettings()->enableAutomodHighlightTaskbar.getValue(), true, + false); + setBoolItem(automodRow[Column::PlaySound], + getSettings()->enableAutomodHighlightSound.getValue(), true, + false); + automodRow[Column::UseRegex]->setFlags({}); + automodRow[Column::CaseSensitive]->setFlags({}); + + const auto automodSound = + QUrl(getSettings()->automodHighlightSoundUrl.getValue()); + setFilePathItem(automodRow[Column::SoundPath], automodSound, false); + auto automodColor = + ColorProvider::instance().color(ColorType::AutomodHighlight); + setColorItem(automodRow[Column::Color], *automodColor, false); + + this->insertCustomRow(automodRow, HighlightRowIndexes::AutomodRow); } void HighlightModel::customRowSetData(const std::vector &row, @@ -215,6 +293,21 @@ void HighlightModel::customRowSetData(const std::vector &row, getSettings()->enableFirstMessageHighlight.setValue( value.toBool()); } + else if (rowIndex == HighlightRowIndexes::ElevatedMessageRow) + { + getSettings()->enableElevatedMessageHighlight.setValue( + value.toBool()); + } + else if (rowIndex == HighlightRowIndexes::ThreadMessageRow) + { + getSettings()->enableThreadHighlight.setValue( + value.toBool()); + } + else if (rowIndex == HighlightRowIndexes::AutomodRow) + { + getSettings()->enableAutomodHighlight.setValue( + value.toBool()); + } } } break; @@ -226,6 +319,16 @@ void HighlightModel::customRowSetData(const std::vector &row, getSettings()->showSelfHighlightInMentions.setValue( value.toBool()); } + else if (rowIndex == HighlightRowIndexes::ThreadMessageRow) + { + getSettings()->showThreadHighlightInMentions.setValue( + value.toBool()); + } + else if (rowIndex == HighlightRowIndexes::AutomodRow) + { + getSettings()->showAutomodInMentions.setValue( + value.toBool()); + } } } break; @@ -257,6 +360,22 @@ void HighlightModel::customRowSetData(const std::vector &row, // getSettings()->enableFirstMessageHighlightTaskbar.setValue( // value.toBool()); } + else if (rowIndex == HighlightRowIndexes::ElevatedMessageRow) + { + // getSettings() + // ->enableElevatedMessageHighlightTaskbar.setvalue( + // value.toBool()); + } + else if (rowIndex == HighlightRowIndexes::ThreadMessageRow) + { + getSettings()->enableThreadHighlightTaskbar.setValue( + value.toBool()); + } + else if (rowIndex == HighlightRowIndexes::AutomodRow) + { + getSettings()->enableAutomodHighlightTaskbar.setValue( + value.toBool()); + } } } break; @@ -288,6 +407,21 @@ void HighlightModel::customRowSetData(const std::vector &row, // getSettings()->enableFirstMessageHighlightSound.setValue( // value.toBool()); } + else if (rowIndex == HighlightRowIndexes::ElevatedMessageRow) + { + // getSettings()->enableElevatedMessageHighlightSound.setValue( + // value.toBool()); + } + else if (rowIndex == HighlightRowIndexes::ThreadMessageRow) + { + getSettings()->enableThreadHighlightSound.setValue( + value.toBool()); + } + else if (rowIndex == HighlightRowIndexes::AutomodRow) + { + getSettings()->enableAutomodHighlightSound.setValue( + value.toBool()); + } } } break; @@ -318,14 +452,14 @@ void HighlightModel::customRowSetData(const std::vector &row, getSettings()->subHighlightSoundUrl.setValue( value.toString()); } - else if (rowIndex == HighlightRowIndexes::RedeemedRow) + else if (rowIndex == HighlightRowIndexes::ThreadMessageRow) { - getSettings()->redeemedHighlightSoundUrl.setValue( + getSettings()->threadHighlightSoundUrl.setValue( value.toString()); } - else if (rowIndex == HighlightRowIndexes::FirstMessageRow) + else if (rowIndex == HighlightRowIndexes::AutomodRow) { - getSettings()->firstMessageHighlightSoundUrl.setValue( + getSettings()->automodHighlightSoundUrl.setValue( value.toString()); } } @@ -335,40 +469,57 @@ void HighlightModel::customRowSetData(const std::vector &row, // Custom color if (role == Qt::DecorationRole) { - auto colorName = value.value().name(QColor::HexArgb); + const auto setColor = [&](auto &setting, ColorType ty) { + auto color = value.value(); + setting.setValue(color.name(QColor::HexArgb)); + }; + if (rowIndex == HighlightRowIndexes::SelfHighlightRow) { - getSettings()->selfHighlightColor.setValue(colorName); + setColor(getSettings()->selfHighlightColor, + ColorType::SelfHighlight); + } + else if (rowIndex == HighlightRowIndexes::WhisperRow) + { + setColor(getSettings()->whisperHighlightColor, + ColorType::Whisper); } - // else if (rowIndex == HighlightRowIndexes::WhisperRow) - // { - // getSettings()->whisperHighlightColor.setValue(colorName); - // } else if (rowIndex == HighlightRowIndexes::SubRow) { - getSettings()->subHighlightColor.setValue(colorName); + setColor(getSettings()->subHighlightColor, + ColorType::Subscription); } else if (rowIndex == HighlightRowIndexes::RedeemedRow) { - getSettings()->redeemedHighlightColor.setValue(colorName); - const_cast(ColorProvider::instance()) - .updateColor(ColorType::RedeemedHighlight, - QColor(colorName)); + setColor(getSettings()->redeemedHighlightColor, + ColorType::RedeemedHighlight); } else if (rowIndex == HighlightRowIndexes::FirstMessageRow) { - getSettings()->firstMessageHighlightColor.setValue( - colorName); - const_cast(ColorProvider::instance()) - .updateColor(ColorType::FirstMessageHighlight, - QColor(colorName)); + setColor(getSettings()->firstMessageHighlightColor, + ColorType::FirstMessageHighlight); + } + else if (rowIndex == HighlightRowIndexes::ElevatedMessageRow) + { + setColor(getSettings()->elevatedMessageHighlightColor, + ColorType::ElevatedMessageHighlight); + } + else if (rowIndex == HighlightRowIndexes::ThreadMessageRow) + { + setColor(getSettings()->threadHighlightColor, + ColorType::ThreadMessageHighlight); + } + else if (rowIndex == HighlightRowIndexes::AutomodRow) + { + setColor(getSettings()->automodHighlightColor, + ColorType::AutomodHighlight); } } } break; } - getApp()->windows->forceLayoutChannelViews(); + getApp()->getWindows()->forceLayoutChannelViews(); } } // namespace chatterino diff --git a/src/controllers/highlights/HighlightModel.hpp b/src/controllers/highlights/HighlightModel.hpp index a5bbebc98..be807d33e 100644 --- a/src/controllers/highlights/HighlightModel.hpp +++ b/src/controllers/highlights/HighlightModel.hpp @@ -1,12 +1,13 @@ #pragma once +#include "common/SignalVectorModel.hpp" + #include -#include "common/SignalVectorModel.hpp" -#include "controllers/highlights/HighlightPhrase.hpp" - namespace chatterino { +class HighlightPhrase; + class HighlightModel : public SignalVectorModel { public: @@ -17,9 +18,9 @@ public: Pattern = 0, ShowInMentions = 1, FlashTaskbar = 2, - PlaySound = 3, - UseRegex = 4, - CaseSensitive = 5, + UseRegex = 3, + CaseSensitive = 4, + PlaySound = 5, SoundPath = 6, Color = 7, COUNT // keep this as last member of enum @@ -31,23 +32,29 @@ public: SubRow = 2, RedeemedRow = 3, FirstMessageRow = 4, + ElevatedMessageRow = 5, + ThreadMessageRow = 6, + AutomodRow = 7, + }; + + enum UserHighlightRowIndexes { + SelfMessageRow = 0, }; protected: // turn a vector item into a model row - virtual HighlightPhrase getItemFromRow( - std::vector &row, - const HighlightPhrase &original) override; + HighlightPhrase getItemFromRow(std::vector &row, + const HighlightPhrase &original) override; // turns a row in the model into a vector item - virtual void getRowFromItem(const HighlightPhrase &item, - std::vector &row) override; + void getRowFromItem(const HighlightPhrase &item, + std::vector &row) override; - virtual void afterInit() override; + void afterInit() override; - virtual void customRowSetData(const std::vector &row, - int column, const QVariant &value, int role, - int rowIndex) override; + void customRowSetData(const std::vector &row, int column, + const QVariant &value, int role, + int rowIndex) override; }; } // namespace chatterino diff --git a/src/controllers/highlights/HighlightPhrase.cpp b/src/controllers/highlights/HighlightPhrase.cpp index e6e479692..f716fd7a6 100644 --- a/src/controllers/highlights/HighlightPhrase.cpp +++ b/src/controllers/highlights/HighlightPhrase.cpp @@ -9,12 +9,21 @@ namespace { } // namespace -QColor HighlightPhrase::FALLBACK_HIGHLIGHT_COLOR = QColor(238, 97, 102, 65); +// change made but removed in merge conflict +// QColor HighlightPhrase::FALLBACK_HIGHLIGHT_COLOR = QColor(238, 97, 102, 65); +QColor HighlightPhrase::FALLBACK_HIGHLIGHT_COLOR = QColor(127, 63, 73, 127); +QColor HighlightPhrase::FALLBACK_SELF_MESSAGE_HIGHLIGHT_COLOR = + QColor(0, 118, 221, 115); QColor HighlightPhrase::FALLBACK_REDEEMED_HIGHLIGHT_COLOR = QColor(28, 126, 141, 60); QColor HighlightPhrase::FALLBACK_FIRST_MESSAGE_HIGHLIGHT_COLOR = QColor(72, 127, 63, 60); +QColor HighlightPhrase::FALLBACK_ELEVATED_MESSAGE_HIGHLIGHT_COLOR = + QColor(255, 174, 66, 60); +QColor HighlightPhrase::FALLBACK_THREAD_HIGHLIGHT_COLOR = + QColor(143, 48, 24, 60); QColor HighlightPhrase::FALLBACK_SUB_COLOR = QColor(196, 102, 255, 100); +QColor HighlightPhrase::FALLBACK_AUTOMOD_HIGHLIGHT_COLOR = QColor(64, 64, 64); bool HighlightPhrase::operator==(const HighlightPhrase &other) const { diff --git a/src/controllers/highlights/HighlightPhrase.hpp b/src/controllers/highlights/HighlightPhrase.hpp index 41ab8b682..ed03fbe96 100644 --- a/src/controllers/highlights/HighlightPhrase.hpp +++ b/src/controllers/highlights/HighlightPhrase.hpp @@ -1,14 +1,13 @@ #pragma once -#include "providers/colors/ColorProvider.hpp" -#include "util/RapidJsonSerializeQString.hpp" #include "util/RapidjsonHelpers.hpp" +#include "util/RapidJsonSerializeQString.hpp" +#include #include #include #include #include -#include #include @@ -80,9 +79,14 @@ public: * Qt>=5.13. */ static QColor FALLBACK_HIGHLIGHT_COLOR; + // Used for automatic self messages highlighing + static QColor FALLBACK_SELF_MESSAGE_HIGHLIGHT_COLOR; static QColor FALLBACK_REDEEMED_HIGHLIGHT_COLOR; static QColor FALLBACK_SUB_COLOR; static QColor FALLBACK_FIRST_MESSAGE_HIGHLIGHT_COLOR; + static QColor FALLBACK_ELEVATED_MESSAGE_HIGHLIGHT_COLOR; + static QColor FALLBACK_THREAD_HIGHLIGHT_COLOR; + static QColor FALLBACK_AUTOMOD_HIGHLIGHT_COLOR; private: QString pattern_; @@ -161,7 +165,9 @@ struct Deserialize { auto _color = QColor(encodedColor); if (!_color.isValid()) + { _color = chatterino::HighlightPhrase::FALLBACK_HIGHLIGHT_COLOR; + } return chatterino::HighlightPhrase(_pattern, _showInMentions, _hasAlert, _hasSound, _isRegex, diff --git a/src/controllers/highlights/UserHighlightModel.cpp b/src/controllers/highlights/UserHighlightModel.cpp index cde9c4965..2c5bfec35 100644 --- a/src/controllers/highlights/UserHighlightModel.cpp +++ b/src/controllers/highlights/UserHighlightModel.cpp @@ -1,14 +1,14 @@ -#include "UserHighlightModel.hpp" +#include "controllers/highlights/UserHighlightModel.hpp" #include "Application.hpp" -#include "controllers/highlights/HighlightModel.hpp" +#include "controllers/highlights/HighlightPhrase.hpp" +#include "providers/colors/ColorProvider.hpp" #include "singletons/Settings.hpp" +#include "singletons/WindowManager.hpp" #include "util/StandardItemHelper.hpp" namespace chatterino { -using Column = HighlightModel::Column; - // commandmodel UserHighlightModel::UserHighlightModel(QObject *parent) : SignalVectorModel(Column::COUNT, parent) @@ -26,7 +26,7 @@ HighlightPhrase UserHighlightModel::getItemFromRow( row[Column::Color]->data(Qt::DecorationRole).value(); return HighlightPhrase{ - row[Column::Pattern]->data(Qt::DisplayRole).toString(), + row[Column::Pattern]->data(Qt::DisplayRole).toString().trimmed(), row[Column::ShowInMentions]->data(Qt::CheckStateRole).toBool(), row[Column::FlashTaskbar]->data(Qt::CheckStateRole).toBool(), row[Column::PlaySound]->data(Qt::CheckStateRole).toBool(), @@ -36,6 +36,82 @@ HighlightPhrase UserHighlightModel::getItemFromRow( highlightColor}; } +void UserHighlightModel::afterInit() +{ + // User highlight settings for your own messages + std::vector messagesRow = this->createRow(); + setBoolItem(messagesRow[Column::Pattern], + getSettings()->enableSelfMessageHighlight.getValue(), true, + false); + messagesRow[Column::Pattern]->setData("Your messages (automatic)", + Qt::DisplayRole); + setBoolItem(messagesRow[Column::ShowInMentions], + getSettings()->showSelfMessageHighlightInMentions.getValue(), + true, false); + messagesRow[Column::FlashTaskbar]->setFlags({}); + messagesRow[Column::PlaySound]->setFlags({}); + messagesRow[Column::UseRegex]->setFlags({}); + messagesRow[Column::CaseSensitive]->setFlags({}); + messagesRow[Column::SoundPath]->setFlags({}); + + auto selfColor = + ColorProvider::instance().color(ColorType::SelfMessageHighlight); + setColorItem(messagesRow[Column::Color], *selfColor, false); + + this->insertCustomRow( + messagesRow, HighlightModel::UserHighlightRowIndexes::SelfMessageRow); +} + +void UserHighlightModel::customRowSetData( + const std::vector &row, int column, const QVariant &value, + int role, int rowIndex) +{ + switch (column) + { + case Column::Pattern: { + if (role == Qt::CheckStateRole) + { + if (rowIndex == + HighlightModel::UserHighlightRowIndexes::SelfMessageRow) + { + getSettings()->enableSelfMessageHighlight.setValue( + value.toBool()); + } + } + } + break; + case Column::ShowInMentions: { + if (role == Qt::CheckStateRole) + { + if (rowIndex == + HighlightModel::UserHighlightRowIndexes::SelfMessageRow) + { + getSettings()->showSelfMessageHighlightInMentions.setValue( + value.toBool()); + } + } + } + break; + case Column::Color: { + // Custom color + if (role == Qt::DecorationRole) + { + auto colorName = value.value().name(QColor::HexArgb); + if (rowIndex == + HighlightModel::UserHighlightRowIndexes::SelfMessageRow) + { + // Update the setting with the new value + getSettings()->selfMessageHighlightColor.setValue( + colorName); + } + } + } + break; + } + + getApp()->getWindows()->forceLayoutChannelViews(); +} + // row into vector item void UserHighlightModel::getRowFromItem(const HighlightPhrase &item, std::vector &row) diff --git a/src/controllers/highlights/UserHighlightModel.hpp b/src/controllers/highlights/UserHighlightModel.hpp index de4332433..e17b84792 100644 --- a/src/controllers/highlights/UserHighlightModel.hpp +++ b/src/controllers/highlights/UserHighlightModel.hpp @@ -1,27 +1,35 @@ #pragma once -#include - #include "common/SignalVectorModel.hpp" -#include "controllers/highlights/HighlightPhrase.hpp" +#include "controllers/highlights/HighlightModel.hpp" + +#include namespace chatterino { class HighlightController; +class HighlightPhrase; class UserHighlightModel : public SignalVectorModel { public: + using Column = HighlightModel::Column; + explicit UserHighlightModel(QObject *parent); protected: // vector into model row - virtual HighlightPhrase getItemFromRow( - std::vector &row, - const HighlightPhrase &original) override; + HighlightPhrase getItemFromRow(std::vector &row, + const HighlightPhrase &original) override; - virtual void getRowFromItem(const HighlightPhrase &item, - std::vector &row) override; + void getRowFromItem(const HighlightPhrase &item, + std::vector &row) override; + + void afterInit() override; + + void customRowSetData(const std::vector &row, int column, + const QVariant &value, int role, + int rowIndex) override; }; } // namespace chatterino diff --git a/src/controllers/hotkeys/ActionNames.hpp b/src/controllers/hotkeys/ActionNames.hpp index aa335a16c..3ebd7d051 100644 --- a/src/controllers/hotkeys/ActionNames.hpp +++ b/src/controllers/hotkeys/ActionNames.hpp @@ -1,10 +1,24 @@ #pragma once -#include "HotkeyCategory.hpp" +#include "controllers/hotkeys/HotkeyCategory.hpp" #include #include +#include + +inline const std::vector>> + HOTKEY_ARG_ON_OFF_TOGGLE = { + {"Toggle", {}}, + {"Set to on", {"on"}}, + {"Set to off", {"off"}}, +}; + +inline const std::vector>> + HOTKEY_ARG_WITH_OR_WITHOUT_SELECTION = { + {"No", {"withoutSelection"}}, + {"Yes", {"withSelection"}}, +}; namespace chatterino { @@ -13,6 +27,9 @@ struct ActionDefinition { // displayName is the value that would be shown to a user when they edit or create a hotkey for an action QString displayName; + // argumentDescription is a description of the arguments in a format of + // " [optional arg: possible + // values]" QString argumentDescription = ""; // minCountArguments is the minimum amount of arguments the action accepts @@ -21,6 +38,20 @@ struct ActionDefinition { // maxCountArguments is the maximum amount of arguments the action accepts uint8_t maxCountArguments = minCountArguments; + + // possibleArguments is empty or contains all possible argument values, + // it is an ordered mapping from option name (what the user sees) to + // arguments (what the action code will see). + // As std::map does not guarantee order this is a std::vector<...> + std::vector>> possibleArguments = + {}; + + // When possibleArguments are present this should be a string like + // "Direction:" which will be shown before the values from + // possibleArguments in the UI. Otherwise, it should be empty. + QString argumentsPrompt = ""; + // A more detailed description of what argumentsPrompt means + QString argumentsPromptHover = ""; }; using ActionDefinitionMap = std::map; @@ -39,15 +70,22 @@ inline const std::map actionNames{ }}, {"scrollPage", ActionDefinition{ - "Scroll", - "", - 1, + .displayName = "Scroll", + .argumentDescription = "", + .minCountArguments = 1, + .maxCountArguments = 1, + .possibleArguments{ + {"Up", {"up"}}, + {"Down", {"down"}}, + }, + .argumentsPrompt = "Direction:", }}, {"search", ActionDefinition{"Focus search box"}}, {"execModeratorAction", ActionDefinition{ "Usercard: execute moderation action", "", 1}}, + {"pin", ActionDefinition{"Usercard, reply thread: pin window"}}, }}, {HotkeyCategory::Split, { @@ -57,24 +95,44 @@ inline const std::map actionNames{ {"delete", ActionDefinition{"Close"}}, {"focus", ActionDefinition{ - "Focus neighbouring split", - "", - 1, + .displayName = "Focus neighbouring split", + .argumentDescription = "", + .minCountArguments = 1, + .maxCountArguments = 1, + .possibleArguments{ + {"Up", {"up"}}, + {"Down", {"down"}}, + {"Left", {"left"}}, + {"Right", {"right"}}, + }, + .argumentsPrompt = "Direction:", + .argumentsPromptHover = + "Which direction to look for a split to focus?", }}, {"openInBrowser", ActionDefinition{"Open channel in browser"}}, + {"openPlayerInBrowser", + ActionDefinition{"Open stream in browser player"}}, {"openInCustomPlayer", ActionDefinition{"Open stream in custom player"}}, {"openInStreamlink", ActionDefinition{"Open stream in streamlink"}}, {"openModView", ActionDefinition{"Open mod view in browser"}}, - {"openViewerList", ActionDefinition{"Open viewer list"}}, + {"openViewerList", ActionDefinition{"Open chatter list"}}, {"pickFilters", ActionDefinition{"Pick filters"}}, {"reconnect", ActionDefinition{"Reconnect to chat"}}, {"reloadEmotes", ActionDefinition{ - "Reload emotes", - "[channel or subscriber]", - 0, - 1, + .displayName = "Reload emotes", + .argumentDescription = + "[type: channel or subscriber; default: all emotes]", + .minCountArguments = 0, + .maxCountArguments = 1, + .possibleArguments{ + {"All emotes", {}}, + {"Channel emotes only", {"channel"}}, + {"Subscriber emotes only", {"subscriber"}}, + }, + .argumentsPrompt = "Emote type:", + .argumentsPromptHover = "Which emotes should Chatterino reload", }}, {"runCommand", ActionDefinition{ @@ -84,29 +142,44 @@ inline const std::map actionNames{ }}, {"scrollPage", ActionDefinition{ - "Scroll", - "", - 1, + .displayName = "Scroll", + .argumentDescription = "", + .minCountArguments = 1, + .maxCountArguments = 1, + .possibleArguments{ + {"Up", {"up"}}, + {"Down", {"down"}}, + }, + .argumentsPrompt = "Direction:", + .argumentsPromptHover = + "Which direction do you want to see more messages", }}, {"scrollToBottom", ActionDefinition{"Scroll to the bottom"}}, {"scrollToTop", ActionDefinition{"Scroll to the top"}}, {"setChannelNotification", ActionDefinition{ - "Set channel live notification", - "[on or off. default: toggle]", - 0, - 1, + .displayName = "Set channel live notification", + .argumentDescription = "[on or off. default: toggle]", + .minCountArguments = 0, + .maxCountArguments = 1, + .possibleArguments = HOTKEY_ARG_ON_OFF_TOGGLE, + .argumentsPrompt = "New value:", + .argumentsPromptHover = "Should the channel live notification be " + "enabled, disabled or toggled", }}, {"setModerationMode", ActionDefinition{ - "Set moderation mode", - "[on or off. default: toggle]", - 0, - 1, + .displayName = "Set moderation mode", + .argumentDescription = "[on or off. default: toggle]", + .minCountArguments = 0, + .maxCountArguments = 1, + .possibleArguments = HOTKEY_ARG_ON_OFF_TOGGLE, + .argumentsPrompt = "New value:", + .argumentsPromptHover = + "Should the moderation mode be enabled, disabled or toggled", }}, {"showSearch", ActionDefinition{"Search current channel"}}, {"showGlobalSearch", ActionDefinition{"Search all channels"}}, - {"startWatching", ActionDefinition{"Start watching"}}, {"debug", ActionDefinition{"Show debug popup"}}, }}, {HotkeyCategory::SplitInput, @@ -114,21 +187,38 @@ inline const std::map actionNames{ {"clear", ActionDefinition{"Clear message"}}, {"copy", ActionDefinition{ - "Copy", - "", - 1, + .displayName = "Copy", + .argumentDescription = + "", + .minCountArguments = 1, + .possibleArguments{ + {"Automatic", {"auto"}}, + {"Split", {"split"}}, + {"Split Input", {"splitInput"}}, + }, + .argumentsPrompt = "Source of text:", }}, {"cursorToStart", ActionDefinition{ - "To start of message", - "", - 1, + .displayName = "To start of message", + .argumentDescription = + "", + .minCountArguments = 1, + .maxCountArguments = 1, + .possibleArguments = HOTKEY_ARG_WITH_OR_WITHOUT_SELECTION, + .argumentsPrompt = "Select text from cursor to start:", + // XXX: write a hover for this that doesn't suck }}, {"cursorToEnd", ActionDefinition{ - "To end of message", - "", - 1, + .displayName = "To end of message", + .argumentDescription = + "", + .minCountArguments = 1, + .maxCountArguments = 1, + .possibleArguments = HOTKEY_ARG_WITH_OR_WITHOUT_SELECTION, + .argumentsPrompt = "Select text from cursor to end:", + // XXX: write a hover for this that doesn't suck }}, {"nextMessage", ActionDefinition{"Choose next sent message"}}, {"openEmotesPopup", ActionDefinition{"Open emotes list"}}, @@ -140,10 +230,16 @@ inline const std::map actionNames{ {"selectWord", ActionDefinition{"Select word"}}, {"sendMessage", ActionDefinition{ - "Send message", - "[keepInput to not clear the text after sending]", - 0, - 1, + .displayName = "Send message", + .argumentDescription = + "[keepInput to not clear the text after sending]", + .minCountArguments = 0, + .maxCountArguments = 1, + .possibleArguments{ + {"Default behavior", {}}, + {"Keep message in input after sending it", {"keepInput"}}, + }, + .argumentsPrompt = "Behavior:", }}, {"undo", ActionDefinition{"Undo"}}, @@ -158,11 +254,12 @@ inline const std::map actionNames{ {"addMiscMessage", ActionDefinition{"Debug: Add misc test message"}}, {"addRewardMessage", ActionDefinition{"Debug: Add reward test message"}}, + {"addSubMessage", ActionDefinition{"Debug: Add sub test message"}}, #endif {"moveTab", ActionDefinition{ "Move tab", - "", + "", 1, }}, {"newSplit", ActionDefinition{"Create a new split"}}, @@ -171,40 +268,79 @@ inline const std::map actionNames{ {"openTab", ActionDefinition{ "Select tab", - "", + "", 1, }}, {"openQuickSwitcher", ActionDefinition{"Open the quick switcher"}}, {"popup", ActionDefinition{ - "New popup", - "", - 1, + .displayName = "New popup", + .argumentDescription = "", + .minCountArguments = 1, + .maxCountArguments = 1, + .possibleArguments{ + {"Focused Split", {"split"}}, + {"Entire Tab", {"window"}}, + }, + .argumentsPrompt = "Include:", + .argumentsPromptHover = + "What should be included in the new popup", }}, {"quit", ActionDefinition{"Quit Chatterino"}}, {"removeTab", ActionDefinition{"Remove current tab"}}, {"reopenSplit", ActionDefinition{"Reopen closed split"}}, {"setStreamerMode", ActionDefinition{ - "Set streamer mode", - "[on, off, toggle, or auto. default: toggle]", - 0, - 1, + .displayName = "Set streamer mode", + .argumentDescription = + "[on, off, toggle, or auto. default: toggle]", + .minCountArguments = 0, + .maxCountArguments = 1, + .possibleArguments = + { + {"Toggle on/off", {}}, + {"Set to on", {"on"}}, + {"Set to off", {"off"}}, + {"Set to automatic", {"auto"}}, + }, + .argumentsPrompt = "New value:", + .argumentsPromptHover = + "Should streamer mode be enabled, disabled, toggled (on/off) " + "or set to auto", }}, {"toggleLocalR9K", ActionDefinition{"Toggle local R9K"}}, {"zoom", ActionDefinition{ - "Zoom in/out", - "", - 1, + .displayName = "Zoom in/out", + .argumentDescription = "Argument:", + .minCountArguments = 1, + .maxCountArguments = 1, + .possibleArguments = + { + {"Zoom in", {"in"}}, + {"Zoom out", {"out"}}, + {"Reset zoom", {"reset"}}, + }, + .argumentsPrompt = "Option:", }}, {"setTabVisibility", ActionDefinition{ - "Set tab visibility", - "[on, off, or toggle. default: toggle]", - 0, - 1, - }}}}, + .displayName = "Set tab visibility", + .argumentDescription = + "[on, off, toggle, or liveOnly. default: toggle]", + .minCountArguments = 0, + .maxCountArguments = 1, + .possibleArguments{ + {"Toggle", {}}, + {"Show all tabs", {"on"}}, + {"Hide all tabs", {"off"}}, + {"Only show live tabs", {"liveOnly"}}, + }, + .argumentsPrompt = "New value:", + .argumentsPromptHover = "Should the tabs be enabled, disabled, " + "toggled, or live-only.", + }}, + }}, }; } // namespace chatterino diff --git a/src/controllers/hotkeys/Hotkey.cpp b/src/controllers/hotkeys/Hotkey.cpp index 99017e08c..b6d5fc6cc 100644 --- a/src/controllers/hotkeys/Hotkey.cpp +++ b/src/controllers/hotkeys/Hotkey.cpp @@ -1,6 +1,5 @@ #include "controllers/hotkeys/Hotkey.hpp" -#include "Application.hpp" #include "common/QLogging.hpp" #include "controllers/hotkeys/ActionNames.hpp" #include "controllers/hotkeys/HotkeyController.hpp" @@ -58,7 +57,7 @@ std::vector Hotkey::arguments() const QString Hotkey::getCategory() const { - return getApp()->hotkeys->categoryDisplayName(this->category_); + return hotkeyCategoryDisplayName(this->category_); } Qt::ShortcutContext Hotkey::getContext() const diff --git a/src/controllers/hotkeys/HotkeyController.cpp b/src/controllers/hotkeys/HotkeyController.cpp index 757197f65..f435bf4af 100644 --- a/src/controllers/hotkeys/HotkeyController.cpp +++ b/src/controllers/hotkeys/HotkeyController.cpp @@ -1,13 +1,61 @@ #include "controllers/hotkeys/HotkeyController.hpp" #include "common/QLogging.hpp" +#include "controllers/hotkeys/Hotkey.hpp" +#include "controllers/hotkeys/HotkeyCategory.hpp" #include "controllers/hotkeys/HotkeyModel.hpp" #include "singletons/Settings.hpp" #include +namespace { + +using namespace chatterino; + +const std::map HOTKEY_CATEGORIES = { + {HotkeyCategory::PopupWindow, {"popupWindow", "Popup Windows"}}, + {HotkeyCategory::Split, {"split", "Split"}}, + {HotkeyCategory::SplitInput, {"splitInput", "Split input box"}}, + {HotkeyCategory::Window, {"window", "Window"}}, +}; + +} // namespace + namespace chatterino { +const std::map &hotkeyCategories() +{ + return HOTKEY_CATEGORIES; +} + +QString hotkeyCategoryName(HotkeyCategory category) +{ + if (!HOTKEY_CATEGORIES.contains(category)) + { + qCWarning(chatterinoHotkeys) << "Invalid HotkeyCategory passed to " + "categoryDisplayName function"; + return {}; + } + + const auto &categoryData = HOTKEY_CATEGORIES.at(category); + + return categoryData.name; +} + +QString hotkeyCategoryDisplayName(HotkeyCategory category) +{ + if (!HOTKEY_CATEGORIES.contains(category)) + { + qCWarning(chatterinoHotkeys) << "Invalid HotkeyCategory passed to " + "categoryDisplayName function"; + return {}; + } + + const auto &categoryData = HOTKEY_CATEGORIES.at(category); + + return categoryData.displayName; +} + static bool hotkeySortCompare_(const std::shared_ptr &a, const std::shared_ptr &b) { @@ -23,6 +71,9 @@ HotkeyController::HotkeyController() : hotkeys_(hotkeySortCompare_) { this->loadHotkeys(); + + this->clearRemovedDefaults(); + this->signalHolder_.managedConnect( this->hotkeys_.delayedItemsChanged, [this]() { qCDebug(chatterinoHotkeys) << "Reloading hotkeys!"; @@ -54,7 +105,7 @@ std::vector HotkeyController::shortcutsForCategory( { qCDebug(chatterinoHotkeys) << qPrintable(parent->objectName()) - << "Unimplemeneted hotkey action:" << hotkey->action() << "in " + << "Unimplemented hotkey action:" << hotkey->action() << "in" << hotkey->getCategory(); continue; } @@ -64,7 +115,7 @@ std::vector HotkeyController::shortcutsForCategory( continue; } auto createShortcutFromKeySeq = [&](QKeySequence qs) { - auto s = new QShortcut(qs, parent); + auto *s = new QShortcut(qs, parent); s->setContext(hotkey->getContext()); auto functionPointer = target->second; QObject::connect(s, &QShortcut::activated, parent, @@ -99,7 +150,7 @@ void HotkeyController::save() std::shared_ptr HotkeyController::getHotkeyByName(QString name) { - for (auto &hotkey : this->hotkeys_) + for (const auto &hotkey : this->hotkeys_) { if (hotkey->name() == name) { @@ -113,7 +164,7 @@ int HotkeyController::replaceHotkey(QString oldName, std::shared_ptr newHotkey) { int i = 0; - for (auto &hotkey : this->hotkeys_) + for (const auto &hotkey : this->hotkeys_) { if (hotkey->name() == oldName) { @@ -125,10 +176,10 @@ int HotkeyController::replaceHotkey(QString oldName, return this->hotkeys_.append(newHotkey); } -boost::optional HotkeyController::hotkeyCategoryFromName( +std::optional HotkeyController::hotkeyCategoryFromName( QString categoryName) { - for (const auto &[category, data] : this->categories()) + for (const auto &[category, data] : HOTKEY_CATEGORIES) { if (data.name == categoryName) { @@ -159,38 +210,9 @@ bool HotkeyController::isDuplicate(std::shared_ptr hotkey, return false; } -QString HotkeyController::categoryDisplayName(HotkeyCategory category) const +const std::set &HotkeyController::removedOrDeprecatedHotkeys() const { - if (this->hotkeyCategories_.count(category) == 0) - { - qCWarning(chatterinoHotkeys) << "Invalid HotkeyCategory passed to " - "categoryDisplayName function"; - return QString(); - } - - const auto &categoryData = this->hotkeyCategories_.at(category); - - return categoryData.displayName; -} - -QString HotkeyController::categoryName(HotkeyCategory category) const -{ - if (this->hotkeyCategories_.count(category) == 0) - { - qCWarning(chatterinoHotkeys) << "Invalid HotkeyCategory passed to " - "categoryName function"; - return QString(); - } - - const auto &categoryData = this->hotkeyCategories_.at(category); - - return categoryData.name; -} - -const std::map - &HotkeyController::categories() const -{ - return this->hotkeyCategories_; + return this->removedOrDeprecatedHotkeys_; } void HotkeyController::loadHotkeys() @@ -278,7 +300,7 @@ void HotkeyController::saveHotkeys() pajlada::Settings::Setting::set( section + "/keySequence", hotkey->keySequence().toString()); - auto categoryName = this->categoryName(hotkey->category()); + auto categoryName = hotkeyCategoryName(hotkey->category()); pajlada::Settings::Setting::set(section + "/category", categoryName); pajlada::Settings::Setting>::set( @@ -518,6 +540,17 @@ void HotkeyController::resetToDefaults() this->loadHotkeys(); } +void HotkeyController::clearRemovedDefaults() +{ + // The "toggleLiveOnly" argument was removed 2024-08-04 + this->tryRemoveDefault(HotkeyCategory::Window, QKeySequence("Ctrl+Shift+L"), + "setTabVisibility", {"toggleLiveOnly"}, + "toggle live tabs only"); + + this->warnForRemovedHotkeyActions(HotkeyCategory::Window, + "setTabVisibility", {"toggleLiveOnly"}); +} + void HotkeyController::tryAddDefault(std::set &addedHotkeys, HotkeyCategory category, QKeySequence keySequence, QString action, @@ -535,10 +568,37 @@ void HotkeyController::tryAddDefault(std::set &addedHotkeys, addedHotkeys.insert(name); } +bool HotkeyController::tryRemoveDefault(HotkeyCategory category, + QKeySequence keySequence, + QString action, + std::vector args, QString name) +{ + return this->hotkeys_.removeFirstMatching([&](const auto &hotkey) { + return hotkey->category() == category && + hotkey->keySequence() == keySequence && + hotkey->action() == action && hotkey->arguments() == args && + hotkey->name() == name; + }); +} + +void HotkeyController::warnForRemovedHotkeyActions(HotkeyCategory category, + QString action, + std::vector args) +{ + for (const auto &hotkey : this->hotkeys_) + { + if (hotkey->category() == category && hotkey->action() == action && + hotkey->arguments() == args) + { + this->removedOrDeprecatedHotkeys_.insert(hotkey->name()); + } + } +} + void HotkeyController::showHotkeyError(const std::shared_ptr &hotkey, QString warning) { - auto msgBox = new QMessageBox( + auto *msgBox = new QMessageBox( QMessageBox::Icon::Warning, "Hotkey error", QString( "There was an error while executing your hotkey named \"%1\": \n%2") @@ -547,4 +607,40 @@ void HotkeyController::showHotkeyError(const std::shared_ptr &hotkey, msgBox->exec(); } +QKeySequence HotkeyController::getDisplaySequence( + HotkeyCategory category, const QString &action, + const std::optional> &arguments) const +{ + const auto &found = this->findLike(category, action, arguments); + if (found != nullptr) + { + return found->keySequence(); + } + return {}; +} + +std::shared_ptr HotkeyController::findLike( + HotkeyCategory category, const QString &action, + const std::optional> &arguments) const +{ + for (auto other : this->hotkeys_) + { + if (other->category() == category && other->action() == action) + { + if (arguments) + { + if (other->arguments() == *arguments) + { + return other; + } + } + else + { + return other; + } + } + } + return nullptr; +} + } // namespace chatterino diff --git a/src/controllers/hotkeys/HotkeyController.hpp b/src/controllers/hotkeys/HotkeyController.hpp index 069c14bf1..7fea8be10 100644 --- a/src/controllers/hotkeys/HotkeyController.hpp +++ b/src/controllers/hotkeys/HotkeyController.hpp @@ -1,13 +1,12 @@ #pragma once #include "common/SignalVector.hpp" -#include "common/Singleton.hpp" #include "controllers/hotkeys/HotkeyCategory.hpp" -#include #include #include +#include #include class QShortcut; @@ -18,7 +17,27 @@ class Hotkey; class HotkeyModel; -class HotkeyController final : public Singleton +/** + * @returns a const map with the HotkeyCategory enum as its key, and HotkeyCategoryData as the value. + **/ +[[nodiscard]] const std::map & + hotkeyCategories(); + +/** + * @brief Returns the name of the given hotkey category + * + * @returns the name, or an empty string if an invalid hotkey category was given + **/ +[[nodiscard]] QString hotkeyCategoryName(HotkeyCategory category); + +/** + * @brief Returns the display name of the given hotkey category + * + * @returns the display name, or an empty string if an invalid hotkey category was given + **/ +[[nodiscard]] QString hotkeyCategoryDisplayName(HotkeyCategory category); + +class HotkeyController final { public: using HotkeyFunction = std::function)>; @@ -31,8 +50,19 @@ public: HotkeyMap actionMap, QWidget *parent); - void save() override; + void save(); std::shared_ptr getHotkeyByName(QString name); + /** + * @brief returns a QKeySequence that perfoms the actions requested. + * Accepted if and only if the category matches, the action matches and arguments match. + * When arguments is present, contents of arguments must match the checked hotkey, otherwise arguments are ignored. + * For example: + * - std::nullopt (or {}) will match any hotkey satisfying category, action values, + * - {{"foo", "bar"}} will only match a hotkey that has these arguments and these arguments only + */ + QKeySequence getDisplaySequence( + HotkeyCategory category, const QString &action, + const std::optional> &arguments = {}) const; /** * @brief removes the hotkey with the oldName and inserts newHotkey at the end @@ -40,8 +70,7 @@ public: * @returns the new index in the SignalVector **/ int replaceHotkey(QString oldName, std::shared_ptr newHotkey); - boost::optional hotkeyCategoryFromName( - QString categoryName); + std::optional hotkeyCategoryFromName(QString categoryName); /** * @brief checks if the hotkey is duplicate @@ -54,28 +83,21 @@ public: [[nodiscard]] bool isDuplicate(std::shared_ptr hotkey, QString ignoreNamed); - /** - * @brief Returns the display name of the given hotkey category - * - * @returns the display name, or an empty string if an invalid hotkey category was given - **/ - [[nodiscard]] QString categoryDisplayName(HotkeyCategory category) const; - - /** - * @brief Returns the name of the given hotkey category - * - * @returns the name, or an empty string if an invalid hotkey category was given - **/ - [[nodiscard]] QString categoryName(HotkeyCategory category) const; - - /** - * @returns a const map with the HotkeyCategory enum as its key, and HotkeyCategoryData as the value. - **/ - [[nodiscard]] const std::map - &categories() const; - pajlada::Signals::NoArgSignal onItemsUpdated; + /** + * @brief Removes hotkeys that were previously added as default hotkeys. + * + * This will potentially remove hotkeys that were explicitly added by the user if they added a hotkey + * with the exact same parameters as the default hotkey. + */ + void clearRemovedDefaults(); + + /// Returns the names of removed or deprecated hotkeys the user had at launch, if any + /// + /// This is used to populate the on-launch warning in the hotkey dialog + const std::set &removedOrDeprecatedHotkeys() const; + private: /** * @brief load hotkeys from under the /hotkeys settings path @@ -109,23 +131,46 @@ private: QKeySequence keySequence, QString action, std::vector args, QString name); + /** + * @brief try to remove a default hotkey if it hasn't already been modified by the user + * + * NOTE: This could also remove a user-added hotkey assuming it matches all parameters + * + * @returns true if the hotkey was removed + **/ + bool tryRemoveDefault(HotkeyCategory category, QKeySequence keySequence, + QString action, std::vector args, + QString name); + + /// Add hotkeys matching the given arguments to list of removed/deprecated hotkeys + /// that the user should remove + void warnForRemovedHotkeyActions(HotkeyCategory category, QString action, + std::vector args); + /** * @brief show an error dialog about a hotkey in a standard format **/ static void showHotkeyError(const std::shared_ptr &hotkey, QString warning); + /** + * @brief finds a Hotkey matching category, action and arguments. + * Accepted if and only if the category matches, the action matches and arguments match. + * When arguments is present, contents of arguments must match the checked hotkey, otherwise arguments are ignored. + * For example: + * - std::nullopt (or {}) will match any hotkey satisfying category, action values, + * - {{"foo", "bar"}} will only match a hotkey that has these arguments and these arguments only + */ + std::shared_ptr findLike( + HotkeyCategory category, const QString &action, + const std::optional> &arguments = {}) const; friend class KeyboardSettingsPage; + /// Stores a list of names the user had at launch that contained deprecated or removed hotkey actions + std::set removedOrDeprecatedHotkeys_; + SignalVector> hotkeys_; pajlada::Signals::SignalHolder signalHolder_; - - const std::map hotkeyCategories_ = { - {HotkeyCategory::PopupWindow, {"popupWindow", "Popup Windows"}}, - {HotkeyCategory::Split, {"split", "Split"}}, - {HotkeyCategory::SplitInput, {"splitInput", "Split input box"}}, - {HotkeyCategory::Window, {"window", "Window"}}, - }; }; } // namespace chatterino diff --git a/src/controllers/hotkeys/HotkeyHelpers.cpp b/src/controllers/hotkeys/HotkeyHelpers.cpp index d998d7665..7be663d6e 100644 --- a/src/controllers/hotkeys/HotkeyHelpers.cpp +++ b/src/controllers/hotkeys/HotkeyHelpers.cpp @@ -1,7 +1,12 @@ #include "controllers/hotkeys/HotkeyHelpers.hpp" +#include "controllers/hotkeys/ActionNames.hpp" +#include "controllers/hotkeys/HotkeyCategory.hpp" + #include +#include + namespace chatterino { std::vector parseHotkeyArguments(QString argumentString) @@ -27,4 +32,20 @@ std::vector parseHotkeyArguments(QString argumentString) return arguments; } +std::optional findHotkeyActionDefinition( + HotkeyCategory category, const QString &action) +{ + auto allActions = actionNames.find(category); + if (allActions != actionNames.end()) + { + const auto &actionsMap = allActions->second; + auto definition = actionsMap.find(action); + if (definition != actionsMap.end()) + { + return {definition->second}; + } + } + return {}; +} + } // namespace chatterino diff --git a/src/controllers/hotkeys/HotkeyHelpers.hpp b/src/controllers/hotkeys/HotkeyHelpers.hpp index 4e63569ff..a2218472d 100644 --- a/src/controllers/hotkeys/HotkeyHelpers.hpp +++ b/src/controllers/hotkeys/HotkeyHelpers.hpp @@ -1,11 +1,16 @@ #pragma once +#include "controllers/hotkeys/ActionNames.hpp" + #include +#include #include namespace chatterino { std::vector parseHotkeyArguments(QString argumentString); +std::optional findHotkeyActionDefinition( + HotkeyCategory category, const QString &action); } // namespace chatterino diff --git a/src/controllers/hotkeys/HotkeyModel.cpp b/src/controllers/hotkeys/HotkeyModel.cpp index 33fd40643..2089bab5a 100644 --- a/src/controllers/hotkeys/HotkeyModel.cpp +++ b/src/controllers/hotkeys/HotkeyModel.cpp @@ -1,6 +1,7 @@ #include "controllers/hotkeys/HotkeyModel.hpp" #include "common/QLogging.hpp" +#include "controllers/hotkeys/Hotkey.hpp" #include "util/StandardItemHelper.hpp" namespace chatterino { diff --git a/src/controllers/hotkeys/HotkeyModel.hpp b/src/controllers/hotkeys/HotkeyModel.hpp index f23b95373..8ec659593 100644 --- a/src/controllers/hotkeys/HotkeyModel.hpp +++ b/src/controllers/hotkeys/HotkeyModel.hpp @@ -1,7 +1,6 @@ #pragma once #include "common/SignalVectorModel.hpp" -#include "controllers/hotkeys/Hotkey.hpp" #include "util/QStringHash.hpp" #include @@ -9,6 +8,7 @@ namespace chatterino { class HotkeyController; +class Hotkey; class HotkeyModel : public SignalVectorModel> { @@ -17,21 +17,20 @@ public: protected: // turn a vector item into a model row - virtual std::shared_ptr getItemFromRow( + std::shared_ptr getItemFromRow( std::vector &row, const std::shared_ptr &original) override; // turns a row in the model into a vector item - virtual void getRowFromItem(const std::shared_ptr &item, - std::vector &row) override; + void getRowFromItem(const std::shared_ptr &item, + std::vector &row) override; - virtual int beforeInsert(const std::shared_ptr &item, - std::vector &row, - int proposedIndex) override; + int beforeInsert(const std::shared_ptr &item, + std::vector &row, + int proposedIndex) override; - virtual void afterRemoved(const std::shared_ptr &item, - std::vector &row, - int index) override; + void afterRemoved(const std::shared_ptr &item, + std::vector &row, int index) override; friend class HotkeyController; diff --git a/src/controllers/ignores/IgnoreController.cpp b/src/controllers/ignores/IgnoreController.cpp index e36feead0..7922b16dd 100644 --- a/src/controllers/ignores/IgnoreController.cpp +++ b/src/controllers/ignores/IgnoreController.cpp @@ -1,7 +1,10 @@ #include "controllers/ignores/IgnoreController.hpp" +#include "Application.hpp" #include "common/QLogging.hpp" +#include "controllers/accounts/AccountController.hpp" #include "controllers/ignores/IgnorePhrase.hpp" +#include "providers/twitch/TwitchAccount.hpp" #include "singletons/Settings.hpp" namespace chatterino { @@ -11,7 +14,7 @@ bool isIgnoredMessage(IgnoredMessageParameters &¶ms) if (!params.message.isEmpty()) { // TODO(pajlada): Do we need to check if the phrase is valid first? - auto phrases = getCSettings().ignoredMessages.readOnly(); + auto phrases = getSettings()->ignoredMessages.readOnly(); for (const auto &phrase : *phrases) { if (phrase.isBlock() && phrase.isMatch(params.message)) @@ -29,10 +32,12 @@ bool isIgnoredMessage(IgnoredMessageParameters &¶ms) { auto sourceUserID = params.twitchUserID; - auto blocks = - getApp()->accounts->twitch.getCurrent()->accessBlockedUserIds(); - - if (auto it = blocks->find(sourceUserID); it != blocks->end()) + bool isBlocked = getApp() + ->getAccounts() + ->twitch.getCurrent() + ->blockedUserIds() + .contains(sourceUserID); + if (isBlocked) { switch (static_cast( getSettings()->showBlockedUsersMessages.getValue())) diff --git a/src/controllers/ignores/IgnoreModel.cpp b/src/controllers/ignores/IgnoreModel.cpp index cbecbb4d9..3702a4270 100644 --- a/src/controllers/ignores/IgnoreModel.cpp +++ b/src/controllers/ignores/IgnoreModel.cpp @@ -1,6 +1,7 @@ -#include "IgnoreModel.hpp" +#include "controllers/ignores/IgnoreModel.hpp" #include "Application.hpp" +#include "controllers/ignores/IgnorePhrase.hpp" #include "singletons/Settings.hpp" #include "util/StandardItemHelper.hpp" diff --git a/src/controllers/ignores/IgnoreModel.hpp b/src/controllers/ignores/IgnoreModel.hpp index 473b61b93..c604f8cd2 100644 --- a/src/controllers/ignores/IgnoreModel.hpp +++ b/src/controllers/ignores/IgnoreModel.hpp @@ -1,12 +1,13 @@ #pragma once +#include "common/SignalVectorModel.hpp" + #include -#include "common/SignalVectorModel.hpp" -#include "controllers/ignores/IgnorePhrase.hpp" - namespace chatterino { +class IgnorePhrase; + class IgnoreModel : public SignalVectorModel { public: @@ -14,12 +15,12 @@ public: protected: // turn a vector item into a model row - virtual IgnorePhrase getItemFromRow(std::vector &row, - const IgnorePhrase &original) override; + IgnorePhrase getItemFromRow(std::vector &row, + const IgnorePhrase &original) override; // turns a row in the model into a vector item - virtual void getRowFromItem(const IgnorePhrase &item, - std::vector &row) override; + void getRowFromItem(const IgnorePhrase &item, + std::vector &row) override; }; } // namespace chatterino diff --git a/src/controllers/ignores/IgnorePhrase.cpp b/src/controllers/ignores/IgnorePhrase.cpp new file mode 100644 index 000000000..eacd7e4bd --- /dev/null +++ b/src/controllers/ignores/IgnorePhrase.cpp @@ -0,0 +1,122 @@ +#include "controllers/ignores/IgnorePhrase.hpp" + +#include "Application.hpp" +#include "controllers/accounts/AccountController.hpp" +#include "providers/twitch/TwitchAccount.hpp" +#include "singletons/Settings.hpp" + +namespace chatterino { + +IgnorePhrase::IgnorePhrase(const QString &pattern, bool isRegex, bool isBlock, + const QString &replace, bool isCaseSensitive) + : pattern_(pattern) + , isRegex_(isRegex) + , regex_(pattern) + , isBlock_(isBlock) + , replace_(replace) + , isCaseSensitive_(isCaseSensitive) +{ + if (this->isCaseSensitive_) + { + regex_.setPatternOptions( + QRegularExpression::UseUnicodePropertiesOption); + } + else + { + regex_.setPatternOptions( + QRegularExpression::CaseInsensitiveOption | + QRegularExpression::UseUnicodePropertiesOption); + } +} + +bool IgnorePhrase::operator==(const IgnorePhrase &other) const +{ + return std::tie(this->pattern_, this->isRegex_, this->isBlock_, + this->replace_, this->isCaseSensitive_) == + std::tie(other.pattern_, other.isRegex_, other.isBlock_, + other.replace_, other.isCaseSensitive_); +} + +const QString &IgnorePhrase::getPattern() const +{ + return this->pattern_; +} + +bool IgnorePhrase::isRegex() const +{ + return this->isRegex_; +} + +bool IgnorePhrase::isRegexValid() const +{ + return this->regex_.isValid(); +} + +bool IgnorePhrase::isMatch(const QString &subject) const +{ + return !this->pattern_.isEmpty() && + (this->isRegex() + ? (this->regex_.isValid() && + this->regex_.match(subject).hasMatch()) + : subject.contains(this->pattern_, this->caseSensitivity())); +} + +const QRegularExpression &IgnorePhrase::getRegex() const +{ + return this->regex_; +} + +bool IgnorePhrase::isBlock() const +{ + return this->isBlock_; +} + +const QString &IgnorePhrase::getReplace() const +{ + return this->replace_; +} + +bool IgnorePhrase::isCaseSensitive() const +{ + return this->isCaseSensitive_; +} + +Qt::CaseSensitivity IgnorePhrase::caseSensitivity() const +{ + return this->isCaseSensitive_ ? Qt::CaseSensitive : Qt::CaseInsensitive; +} + +const std::unordered_map &IgnorePhrase::getEmotes() const +{ + return this->emotes_; +} + +bool IgnorePhrase::containsEmote() const +{ + if (!this->emotesChecked_) + { + const auto &accvec = getApp()->getAccounts()->twitch.accounts; + for (const auto &acc : accvec) + { + const auto &accemotes = *acc->accessEmotes(); + for (const auto &emote : accemotes.emotes) + { + if (this->replace_.contains(emote.first.string, + Qt::CaseSensitive)) + { + this->emotes_.emplace(emote.first, emote.second); + } + } + } + this->emotesChecked_ = true; + } + return !this->emotes_.empty(); +} + +IgnorePhrase IgnorePhrase::createEmpty() +{ + return IgnorePhrase(QString(), false, false, + getSettings()->ignoredPhraseReplace.getValue(), true); +} + +} // namespace chatterino diff --git a/src/controllers/ignores/IgnorePhrase.hpp b/src/controllers/ignores/IgnorePhrase.hpp index ffd4c09ec..5ff129fd0 100644 --- a/src/controllers/ignores/IgnorePhrase.hpp +++ b/src/controllers/ignores/IgnorePhrase.hpp @@ -1,128 +1,52 @@ #pragma once -#include "Application.hpp" -#include "controllers/accounts/AccountController.hpp" -#include "singletons/Settings.hpp" - -#include "util/RapidJsonSerializeQString.hpp" +#include "common/Aliases.hpp" #include "util/RapidjsonHelpers.hpp" +#include "util/RapidJsonSerializeQString.hpp" +#include #include #include -#include #include +#include namespace chatterino { +struct Emote; +using EmotePtr = std::shared_ptr; + class IgnorePhrase { public: - bool operator==(const IgnorePhrase &other) const - { - return std::tie(this->pattern_, this->isRegex_, this->isBlock_, - this->replace_, this->isCaseSensitive_) == - std::tie(other.pattern_, other.isRegex_, other.isBlock_, - other.replace_, other.isCaseSensitive_); - } - IgnorePhrase(const QString &pattern, bool isRegex, bool isBlock, - const QString &replace, bool isCaseSensitive) - : pattern_(pattern) - , isRegex_(isRegex) - , regex_(pattern) - , isBlock_(isBlock) - , replace_(replace) - , isCaseSensitive_(isCaseSensitive) - { - if (this->isCaseSensitive_) - { - regex_.setPatternOptions( - QRegularExpression::UseUnicodePropertiesOption); - } - else - { - regex_.setPatternOptions( - QRegularExpression::CaseInsensitiveOption | - QRegularExpression::UseUnicodePropertiesOption); - } - } + const QString &replace, bool isCaseSensitive); - const QString &getPattern() const - { - return this->pattern_; - } + bool operator==(const IgnorePhrase &other) const; - bool isRegex() const - { - return this->isRegex_; - } + const QString &getPattern() const; - bool isRegexValid() const - { - return this->regex_.isValid(); - } + bool isRegex() const; - bool isMatch(const QString &subject) const - { - return !this->pattern_.isEmpty() && - (this->isRegex() ? (this->regex_.isValid() && - this->regex_.match(subject).hasMatch()) - : subject.contains(this->pattern_, - this->caseSensitivity())); - } + bool isRegexValid() const; - const QRegularExpression &getRegex() const - { - return this->regex_; - } + bool isMatch(const QString &subject) const; - bool isBlock() const - { - return this->isBlock_; - } + const QRegularExpression &getRegex() const; - const QString &getReplace() const - { - return this->replace_; - } + bool isBlock() const; - bool isCaseSensitive() const - { - return this->isCaseSensitive_; - } + const QString &getReplace() const; - Qt::CaseSensitivity caseSensitivity() const - { - return this->isCaseSensitive_ ? Qt::CaseSensitive : Qt::CaseInsensitive; - } + bool isCaseSensitive() const; - const std::unordered_map &getEmotes() const - { - return this->emotes_; - } + Qt::CaseSensitivity caseSensitivity() const; - bool containsEmote() const - { - if (!this->emotesChecked_) - { - const auto &accvec = getApp()->accounts->twitch.accounts; - for (const auto &acc : accvec) - { - const auto &accemotes = *acc->accessEmotes(); - for (const auto &emote : accemotes.emotes) - { - if (this->replace_.contains(emote.first.string, - Qt::CaseSensitive)) - { - this->emotes_.emplace(emote.first, emote.second); - } - } - } - this->emotesChecked_ = true; - } - return !this->emotes_.empty(); - } + const std::unordered_map &getEmotes() const; + + bool containsEmote() const; + + static IgnorePhrase createEmpty(); private: QString pattern_; @@ -163,10 +87,7 @@ struct Deserialize { if (!value.IsObject()) { PAJLADA_REPORT_ERROR(error) - return chatterino::IgnorePhrase( - QString(), false, false, - ::chatterino::getSettings()->ignoredPhraseReplace.getValue(), - true); + return chatterino::IgnorePhrase::createEmpty(); } QString _pattern; diff --git a/src/controllers/logging/ChannelLog.cpp b/src/controllers/logging/ChannelLog.cpp new file mode 100644 index 000000000..67925b945 --- /dev/null +++ b/src/controllers/logging/ChannelLog.cpp @@ -0,0 +1,30 @@ +#include "controllers/logging/ChannelLog.hpp" + +namespace chatterino { + +ChannelLog::ChannelLog(QString channelName) + : channelName_(std::move(channelName)) +{ +} + +bool ChannelLog::operator==(const ChannelLog &other) const +{ + return this->channelName_ == other.channelName_; +} + +QString ChannelLog::channelName() const +{ + return this->channelName_.toLower(); +} + +QString ChannelLog::toString() const +{ + return this->channelName_; +} + +ChannelLog ChannelLog::createEmpty() +{ + return {""}; +} + +} // namespace chatterino diff --git a/src/controllers/logging/ChannelLog.hpp b/src/controllers/logging/ChannelLog.hpp new file mode 100644 index 000000000..b04cc0f06 --- /dev/null +++ b/src/controllers/logging/ChannelLog.hpp @@ -0,0 +1,69 @@ +#pragma once + +#include "util/RapidjsonHelpers.hpp" + +#include +#include + +namespace chatterino { + +/** + * @brief Contains the description of a channel that will be logged + **/ +class ChannelLog +{ + QString channelName_; + +public: + ChannelLog(QString channelName); + + bool operator==(const ChannelLog &other) const; + + [[nodiscard]] QString channelName() const; + + [[nodiscard]] QString toString() const; + + [[nodiscard]] static ChannelLog createEmpty(); +}; + +} // namespace chatterino + +namespace pajlada { + +template <> +struct Serialize { + static rapidjson::Value get(const chatterino::ChannelLog &value, + rapidjson::Document::AllocatorType &a) + { + rapidjson::Value ret(rapidjson::kObjectType); + + chatterino::rj::set(ret, "channelName", value.channelName(), a); + + return ret; + } +}; + +template <> +struct Deserialize { + static chatterino::ChannelLog get(const rapidjson::Value &value, + bool *error = nullptr) + { + if (!value.IsObject()) + { + PAJLADA_REPORT_ERROR(error); + return chatterino::ChannelLog::createEmpty(); + } + + QString channelName; + + if (!chatterino::rj::getSafe(value, "channelName", channelName)) + { + PAJLADA_REPORT_ERROR(error); + return chatterino::ChannelLog::createEmpty(); + } + + return {channelName}; + } +}; + +} // namespace pajlada diff --git a/src/controllers/logging/ChannelLoggingModel.cpp b/src/controllers/logging/ChannelLoggingModel.cpp new file mode 100644 index 000000000..5c49a1f03 --- /dev/null +++ b/src/controllers/logging/ChannelLoggingModel.cpp @@ -0,0 +1,25 @@ +#include "controllers/logging/ChannelLoggingModel.hpp" + +#include "util/StandardItemHelper.hpp" + +namespace chatterino { + +ChannelLoggingModel ::ChannelLoggingModel(QObject *parent) + : SignalVectorModel(Column::COUNT, parent) +{ +} + +ChannelLog ChannelLoggingModel::getItemFromRow( + std::vector &row, const ChannelLog & /*original*/) +{ + auto channelName = row[Column::Channel]->data(Qt::DisplayRole).toString(); + return {channelName}; +} + +void ChannelLoggingModel::getRowFromItem(const ChannelLog &item, + std::vector &row) +{ + setStringItem(row[Column::Channel], item.channelName()); +} + +} // namespace chatterino diff --git a/src/controllers/logging/ChannelLoggingModel.hpp b/src/controllers/logging/ChannelLoggingModel.hpp new file mode 100644 index 000000000..dddebab4b --- /dev/null +++ b/src/controllers/logging/ChannelLoggingModel.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include "common/SignalVectorModel.hpp" +#include "controllers/logging/ChannelLog.hpp" + +#include + +namespace chatterino { + +class ChannelLoggingModel : public SignalVectorModel +{ + explicit ChannelLoggingModel(QObject *parent); + + enum Column { + Channel, + COUNT, + }; + +protected: + // turn a vector item into a model row + ChannelLog getItemFromRow(std::vector &row, + const ChannelLog &original) override; + + // turns a row in the model into a vector item + void getRowFromItem(const ChannelLog &item, + std::vector &row) override; + + friend class ModerationPage; +}; + +} // namespace chatterino diff --git a/src/controllers/moderationactions/ModerationAction.cpp b/src/controllers/moderationactions/ModerationAction.cpp index a24ce9e85..a82d1848c 100644 --- a/src/controllers/moderationactions/ModerationAction.cpp +++ b/src/controllers/moderationactions/ModerationAction.cpp @@ -1,31 +1,16 @@ -#include "ModerationAction.hpp" +#include "controllers/moderationactions/ModerationAction.hpp" -#include #include "Application.hpp" +#include "debug/AssertInGuiThread.hpp" #include "messages/Image.hpp" #include "singletons/Resources.hpp" +#include +#include + namespace chatterino { -// ModerationAction::ModerationAction(Image *_image, const QString &_action) -// : _isImage(true) -// , image(_image) -// , action(_action) -//{ -//} - -// ModerationAction::ModerationAction(const QString &_line1, const QString -// &_line2, -// const QString &_action) -// : _isImage(false) -// , image(nullptr) -// , line1(_line1) -// , line2(_line2) -// , action(_action) -//{ -//} - -ModerationAction::ModerationAction(const QString &action) +ModerationAction::ModerationAction(const QString &action, const QUrl &iconPath) : action_(action) { static QRegularExpression replaceRegex("[!/.]"); @@ -35,6 +20,8 @@ ModerationAction::ModerationAction(const QString &action) if (timeoutMatch.hasMatch()) { + this->type_ = Type::Timeout; + // if (multipleTimeouts > 1) { // QString line1; // QString line2; @@ -97,24 +84,19 @@ ModerationAction::ModerationAction(const QString &action) } this->line2_ = "w"; } - - // line1 = this->line1_; - // line2 = this->line2_; - // } else { - // this->_moderationActions.emplace_back(getResources().buttonTimeout, - // str); - // } } else if (action.startsWith("/ban ")) { - this->imageToLoad_ = 1; + this->type_ = Type::Ban; } else if (action.startsWith("/delete ")) { - this->imageToLoad_ = 2; + this->type_ = Type::Delete; } else { + this->type_ = Type::Custom; + QString xD = action; xD.replace(replaceRegex, ""); @@ -122,6 +104,11 @@ ModerationAction::ModerationAction(const QString &action) this->line1_ = xD.mid(0, 2); this->line2_ = xD.mid(2, 2); } + + if (iconPath.isValid()) + { + this->iconPath_ = iconPath; + } } bool ModerationAction::operator==(const ModerationAction &other) const @@ -134,16 +121,26 @@ bool ModerationAction::isImage() const return bool(this->image_); } -const boost::optional &ModerationAction::getImage() const +const std::optional &ModerationAction::getImage() const { assertInGuiThread(); - - if (this->imageToLoad_ != 0) + if (this->image_.has_value()) { - if (this->imageToLoad_ == 1) - this->image_ = Image::fromPixmap(getResources().buttons.ban); - else if (this->imageToLoad_ == 2) - this->image_ = Image::fromPixmap(getResources().buttons.trashCan); + return this->image_; + } + + if (this->iconPath_.isValid()) + { + this->image_ = Image::fromUrl({this->iconPath_.toString()}); + } + else if (this->type_ == Type::Ban) + { + this->image_ = Image::fromResourcePixmap(getResources().buttons.ban); + } + else if (this->type_ == Type::Delete) + { + this->image_ = + Image::fromResourcePixmap(getResources().buttons.trashCan); } return this->image_; @@ -164,4 +161,14 @@ const QString &ModerationAction::getAction() const return this->action_; } +const QUrl &ModerationAction::iconPath() const +{ + return this->iconPath_; +} + +ModerationAction::Type ModerationAction::getType() const +{ + return this->type_; +} + } // namespace chatterino diff --git a/src/controllers/moderationactions/ModerationAction.hpp b/src/controllers/moderationactions/ModerationAction.hpp index 5e08ddcf0..643eaf06d 100644 --- a/src/controllers/moderationactions/ModerationAction.hpp +++ b/src/controllers/moderationactions/ModerationAction.hpp @@ -1,12 +1,13 @@ #pragma once -#include -#include -#include - #include "util/RapidjsonHelpers.hpp" +#include +#include +#include + #include +#include namespace chatterino { @@ -16,22 +17,52 @@ using ImagePtr = std::shared_ptr; class ModerationAction { public: - ModerationAction(const QString &action); + /** + * Type of the action, parsed from the input `action` + */ + enum class Type { + /** + * /ban + */ + Ban, + + /** + * /delete + */ + Delete, + + /** + * /timeout + */ + Timeout, + + /** + * Anything not matching the action types above + */ + Custom, + }; + + ModerationAction(const QString &action, const QUrl &iconPath = {}); bool operator==(const ModerationAction &other) const; bool isImage() const; - const boost::optional &getImage() const; + const std::optional &getImage() const; const QString &getLine1() const; const QString &getLine2() const; const QString &getAction() const; + const QUrl &iconPath() const; + Type getType() const; private: - mutable boost::optional image_; + mutable std::optional image_; QString line1_; QString line2_; QString action_; - int imageToLoad_{}; + + Type type_{}; + + QUrl iconPath_; }; } // namespace chatterino @@ -46,6 +77,7 @@ struct Serialize { rapidjson::Value ret(rapidjson::kObjectType); chatterino::rj::set(ret, "pattern", value.getAction(), a); + chatterino::rj::set(ret, "icon", value.iconPath().toString(), a); return ret; } @@ -63,10 +95,12 @@ struct Deserialize { } QString pattern; - chatterino::rj::getSafe(value, "pattern", pattern); - return chatterino::ModerationAction(pattern); + QString icon; + chatterino::rj::getSafe(value, "icon", icon); + + return chatterino::ModerationAction(pattern, QUrl(icon)); } }; diff --git a/src/controllers/moderationactions/ModerationActionModel.cpp b/src/controllers/moderationactions/ModerationActionModel.cpp index 00c8f6e7a..f7160b589 100644 --- a/src/controllers/moderationactions/ModerationActionModel.cpp +++ b/src/controllers/moderationactions/ModerationActionModel.cpp @@ -1,12 +1,19 @@ -#include "ModerationActionModel.hpp" +#include "controllers/moderationactions/ModerationActionModel.hpp" +#include "controllers/moderationactions/ModerationAction.hpp" +#include "messages/Image.hpp" +#include "util/LoadPixmap.hpp" +#include "util/PostToThread.hpp" #include "util/StandardItemHelper.hpp" +#include +#include + namespace chatterino { // commandmodel ModerationActionModel ::ModerationActionModel(QObject *parent) - : SignalVectorModel(1, parent) + : SignalVectorModel(2, parent) { } @@ -14,14 +21,31 @@ ModerationActionModel ::ModerationActionModel(QObject *parent) ModerationAction ModerationActionModel::getItemFromRow( std::vector &row, const ModerationAction &original) { - return ModerationAction(row[0]->data(Qt::DisplayRole).toString()); + return ModerationAction( + row[Column::Command]->data(Qt::DisplayRole).toString(), + row[Column::Icon]->data(Qt::UserRole).toString()); } // turns a row in the model into a vector item void ModerationActionModel::getRowFromItem(const ModerationAction &item, std::vector &row) { - setStringItem(row[0], item.getAction()); + setStringItem(row[Column::Command], item.getAction()); + setFilePathItem(row[Column::Icon], item.iconPath()); + if (!item.iconPath().isEmpty()) + { + auto oImage = item.getImage(); + assert(oImage.has_value()); + if (oImage.has_value()) + { + auto url = oImage->get()->url(); + loadPixmapFromUrl(url, [row](const QPixmap &pixmap) { + postToThread([row, pixmap]() { + row[Column::Icon]->setData(pixmap, Qt::DecorationRole); + }); + }); + } + } } } // namespace chatterino diff --git a/src/controllers/moderationactions/ModerationActionModel.hpp b/src/controllers/moderationactions/ModerationActionModel.hpp index e13b3b27b..3382b4378 100644 --- a/src/controllers/moderationactions/ModerationActionModel.hpp +++ b/src/controllers/moderationactions/ModerationActionModel.hpp @@ -1,26 +1,31 @@ #pragma once +#include "common/SignalVectorModel.hpp" + #include -#include "common/SignalVectorModel.hpp" -#include "controllers/moderationactions/ModerationAction.hpp" - namespace chatterino { +class ModerationAction; + class ModerationActionModel : public SignalVectorModel { public: explicit ModerationActionModel(QObject *parent); + enum Column { + Command = 0, + Icon = 1, + }; + protected: // turn a vector item into a model row - virtual ModerationAction getItemFromRow( - std::vector &row, - const ModerationAction &original) override; + ModerationAction getItemFromRow(std::vector &row, + const ModerationAction &original) override; // turns a row in the model into a vector item - virtual void getRowFromItem(const ModerationAction &item, - std::vector &row) override; + void getRowFromItem(const ModerationAction &item, + std::vector &row) override; friend class HighlightController; }; diff --git a/src/controllers/nicknames/Nickname.hpp b/src/controllers/nicknames/Nickname.hpp index 3fb0b23b9..7343dd393 100644 --- a/src/controllers/nicknames/Nickname.hpp +++ b/src/controllers/nicknames/Nickname.hpp @@ -1,13 +1,14 @@ #pragma once -#include "controllers/accounts/AccountController.hpp" -#include "util/RapidJsonSerializeQString.hpp" #include "util/RapidjsonHelpers.hpp" +#include "util/RapidJsonSerializeQString.hpp" -#include #include +#include +#include #include +#include namespace chatterino { @@ -58,25 +59,25 @@ public: return this->isCaseSensitive_; } - [[nodiscard]] bool match(QString &usernameText) const + [[nodiscard]] std::optional match( + const QString &usernameText) const { if (this->isRegex()) { if (!this->regex_.isValid()) { - return false; + return std::nullopt; } if (this->name().isEmpty()) { - return false; + return std::nullopt; } auto workingCopy = usernameText; workingCopy.replace(this->regex_, this->replace()); if (workingCopy != usernameText) { - usernameText = workingCopy; - return true; + return workingCopy; } } else @@ -85,12 +86,11 @@ public: this->name().compare(usernameText, this->caseSensitivity()); if (res == 0) { - usernameText = this->replace(); - return true; + return this->replace(); } } - return false; + return std::nullopt; } private: diff --git a/src/controllers/nicknames/NicknamesModel.cpp b/src/controllers/nicknames/NicknamesModel.cpp index 2748f49b6..f3ab42af2 100644 --- a/src/controllers/nicknames/NicknamesModel.cpp +++ b/src/controllers/nicknames/NicknamesModel.cpp @@ -1,8 +1,6 @@ -#include "NicknamesModel.hpp" +#include "controllers/nicknames/NicknamesModel.hpp" -#include "Application.hpp" -#include "providers/twitch/api/Helix.hpp" -#include "singletons/Settings.hpp" +#include "controllers/nicknames/Nickname.hpp" #include "util/StandardItemHelper.hpp" namespace chatterino { @@ -16,7 +14,7 @@ NicknamesModel::NicknamesModel(QObject *parent) Nickname NicknamesModel::getItemFromRow(std::vector &row, const Nickname &original) { - return Nickname{row[0]->data(Qt::DisplayRole).toString(), + return Nickname{row[0]->data(Qt::DisplayRole).toString().trimmed(), row[1]->data(Qt::DisplayRole).toString(), row[2]->data(Qt::CheckStateRole).toBool(), row[3]->data(Qt::CheckStateRole).toBool()}; diff --git a/src/controllers/nicknames/NicknamesModel.hpp b/src/controllers/nicknames/NicknamesModel.hpp index f7947a7fa..2e3f32341 100644 --- a/src/controllers/nicknames/NicknamesModel.hpp +++ b/src/controllers/nicknames/NicknamesModel.hpp @@ -1,12 +1,15 @@ #pragma once +#include "common/SignalVectorModel.hpp" + #include -#include "common/SignalVectorModel.hpp" -#include "controllers/nicknames/Nickname.hpp" +#include namespace chatterino { +class Nickname; + class NicknamesModel : public SignalVectorModel { public: @@ -14,12 +17,12 @@ public: protected: // turn a vector item into a model row - virtual Nickname getItemFromRow(std::vector &row, - const Nickname &original) override; + Nickname getItemFromRow(std::vector &row, + const Nickname &original) override; // turns a row in the model into a vector item - virtual void getRowFromItem(const Nickname &item, - std::vector &row) override; + void getRowFromItem(const Nickname &item, + std::vector &row) override; }; } // namespace chatterino diff --git a/src/controllers/notifications/NotificationController.cpp b/src/controllers/notifications/NotificationController.cpp index 1eae5d9b3..7de9595ad 100644 --- a/src/controllers/notifications/NotificationController.cpp +++ b/src/controllers/notifications/NotificationController.cpp @@ -1,76 +1,70 @@ #include "controllers/notifications/NotificationController.hpp" #include "Application.hpp" -#include "common/NetworkRequest.hpp" -#include "common/Outcome.hpp" #include "common/QLogging.hpp" #include "controllers/notifications/NotificationModel.hpp" -#include "providers/twitch/TwitchIrcServer.hpp" -#include "providers/twitch/TwitchMessageBuilder.hpp" +#include "controllers/sound/ISoundController.hpp" +#include "messages/Message.hpp" +#include "messages/MessageBuilder.hpp" #include "providers/twitch/api/Helix.hpp" +#include "providers/twitch/TwitchIrcServer.hpp" +#include "singletons/Settings.hpp" +#include "singletons/StreamerMode.hpp" #include "singletons/Toasts.hpp" #include "singletons/WindowManager.hpp" #include "util/Helpers.hpp" -#include "widgets/Window.hpp" -#ifdef Q_OS_WIN -# include -#endif - -#include -#include -#include #include -#include + +namespace ranges = std::ranges; namespace chatterino { -void NotificationController::initialize(Settings &settings, Paths &paths) +NotificationController::NotificationController() { - this->initialized_ = true; for (const QString &channelName : this->twitchSetting_.getValue()) { this->channelMap[Platform::Twitch].append(channelName); } - this->channelMap[Platform::Twitch].delayedItemsChanged.connect([this] { - this->twitchSetting_.setValue(this->channelMap[Platform::Twitch].raw()); - }); + // We can safely ignore this signal connection since channelMap will always be destroyed + // before the NotificationController + std::ignore = + this->channelMap[Platform::Twitch].delayedItemsChanged.connect([this] { + this->twitchSetting_.setValue( + this->channelMap[Platform::Twitch].raw()); + }); - liveStatusTimer_ = new QTimer(); - - this->fetchFakeChannels(); - - QObject::connect(this->liveStatusTimer_, &QTimer::timeout, [=] { + QObject::connect(&this->liveStatusTimer_, &QTimer::timeout, [this] { this->fetchFakeChannels(); }); - this->liveStatusTimer_->start(60 * 1000); + this->liveStatusTimer_.start(60 * 1000); +} + +void NotificationController::initialize() +{ + this->fetchFakeChannels(); } void NotificationController::updateChannelNotification( const QString &channelName, Platform p) { - if (isChannelNotified(channelName, p)) + if (this->isChannelNotified(channelName, p)) { - removeChannelNotification(channelName, p); + this->removeChannelNotification(channelName, p); } else { - addChannelNotification(channelName, p); + this->addChannelNotification(channelName, p); } } bool NotificationController::isChannelNotified(const QString &channelName, - Platform p) + Platform p) const { - for (const auto &channel : this->channelMap[p]) - { - if (channelName.toLower() == channel.toLower()) - { - return true; - } - } - return false; + return ranges::any_of(channelMap.at(p).raw(), [&](const auto &name) { + return name.compare(channelName, Qt::CaseInsensitive) == 0; + }); } void NotificationController::addChannelNotification(const QString &channelName, @@ -85,53 +79,114 @@ void NotificationController::removeChannelNotification( for (std::vector::size_type i = 0; i != channelMap[p].raw().size(); i++) { - if (channelMap[p].raw()[i].toLower() == channelName.toLower()) + if (channelMap[p].raw()[i].compare(channelName, Qt::CaseInsensitive) == + 0) { - channelMap[p].removeAt(i); + channelMap[p].removeAt(static_cast(i)); i--; } } } -void NotificationController::playSound() -{ - static auto player = new QMediaPlayer; - static QUrl currentPlayerUrl; +void NotificationController::playSound() const +{ QUrl highlightSoundUrl = getSettings()->notificationCustomSound ? QUrl::fromLocalFile( getSettings()->notificationPathSound.getValue()) : QUrl("qrc:/sounds/ping2.wav"); - if (currentPlayerUrl != highlightSoundUrl) - { - player->setMedia(highlightSoundUrl); - - currentPlayerUrl = highlightSoundUrl; - } - player->play(); + getApp()->getSound()->play(highlightSoundUrl); } NotificationModel *NotificationController::createModel(QObject *parent, Platform p) { - NotificationModel *model = new NotificationModel(parent); + auto *model = new NotificationModel(parent); model->initialize(&this->channelMap[p]); return model; } +void NotificationController::notifyTwitchChannelLive( + const NotificationPayload &payload) const +{ + bool showNotification = + !(getSettings()->suppressInitialLiveNotification && + payload.isInitialUpdate) && + !(getApp()->getStreamerMode()->isEnabled() && + getSettings()->streamerModeSuppressLiveNotifications); + bool playedSound = false; + + if (showNotification && + this->isChannelNotified(payload.channelName, Platform::Twitch)) + { + if (Toasts::isEnabled()) + { + getApp()->getToasts()->sendChannelNotification( + payload.channelName, payload.title, Platform::Twitch); + } + if (getSettings()->notificationPlaySound) + { + this->playSound(); + playedSound = true; + } + if (getSettings()->notificationFlashTaskbar) + { + getApp()->getWindows()->sendAlert(); + } + } + + // Message in /live channel + getApp()->getTwitch()->getLiveChannel()->addMessage( + MessageBuilder::makeLiveMessage(payload.displayName, payload.channelId), + MessageContext::Original); + + // Notify on all channels with a ping sound + if (showNotification && !playedSound && + getSettings()->notificationOnAnyChannel) + { + this->playSound(); + } +} + +// NOLINTNEXTLINE(readability-convert-member-functions-to-static) +void NotificationController::notifyTwitchChannelOffline(const QString &id) const +{ + // "delete" old 'CHANNEL is live' message + LimitedQueueSnapshot snapshot = + getApp()->getTwitch()->getLiveChannel()->getMessageSnapshot(); + int snapshotLength = static_cast(snapshot.size()); + + int end = std::max(0, snapshotLength - 200); + + for (int i = snapshotLength - 1; i >= end; --i) + { + const auto &s = snapshot[i]; + + if (s->id == id) + { + s->flags.set(MessageFlag::Disabled); + break; + } + } +} + void NotificationController::fetchFakeChannels() { qCDebug(chatterinoNotification) << "fetching fake channels"; + QStringList channels; - for (std::vector::size_type i = 0; - i < channelMap[Platform::Twitch].raw().size(); i++) + for (size_t i = 0; i < channelMap[Platform::Twitch].raw().size(); i++) { - auto chan = getApp()->twitch->getChannelOrEmpty( - channelMap[Platform::Twitch].raw()[i]); + const auto &name = channelMap[Platform::Twitch].raw()[i]; + auto chan = getApp()->getTwitch()->getChannelOrEmpty(name); if (chan->isEmpty()) { - channels.push_back(channelMap[Platform::Twitch].raw()[i]); + channels.push_back(name); + } + else + { + this->fakeChannels_.erase(name); } } @@ -139,17 +194,26 @@ void NotificationController::fetchFakeChannels() { getHelix()->fetchStreams( {}, batch, - [batch, this](std::vector streams) { - std::unordered_set liveStreams; + [batch, this](const auto &streams) { + std::map, + QCompareCaseInsensitive> + liveStreams; for (const auto &stream : streams) { - liveStreams.insert(stream.userLogin); + liveStreams.emplace(stream.userLogin, stream); } for (const auto &name : batch) { - auto it = liveStreams.find(name.toLower()); - this->checkStream(it != liveStreams.end(), name); + auto it = liveStreams.find(name); + if (it == liveStreams.end()) + { + this->updateFakeChannel(name, std::nullopt); + } + else + { + this->updateFakeChannel(name, it->second); + } } }, [batch]() { @@ -162,82 +226,56 @@ void NotificationController::fetchFakeChannels() }); } } -void NotificationController::checkStream(bool live, QString channelName) +void NotificationController::updateFakeChannel( + const QString &channelName, const std::optional &stream) { - qCDebug(chatterinoNotification) - << "[TwitchChannel" << channelName << "] Refreshing live status"; + bool live = stream.has_value(); + qCDebug(chatterinoNotification).nospace().noquote() + << "[FakeTwitchChannel " << channelName + << "] New live status: " << stream.has_value(); + auto channelIt = this->fakeChannels_.find(channelName); + bool isInitialUpdate = false; + if (channelIt == this->fakeChannels_.end()) + { + channelIt = this->fakeChannels_ + .emplace(channelName, + FakeChannel{ + .id = {}, + .isLive = live, + }) + .first; + isInitialUpdate = true; + } + if (channelIt->second.isLive == live && !isInitialUpdate) + { + return; // nothing changed + } + + if (live && channelIt->second.id.isNull()) + { + channelIt->second.id = stream->userId; + } + + channelIt->second.isLive = live; + + // Similar code can be found in TwitchChannel::onLiveStatusChange. + // Since this is a fake channel, we don't send a live message in the + // TwitchChannel. if (!live) { // Stream is offline - this->removeFakeChannel(channelName); + this->notifyTwitchChannelOffline(channelIt->second.id); return; } - // Stream is online - auto i = std::find(fakeTwitchChannels.begin(), fakeTwitchChannels.end(), - channelName); - - if (i != fakeTwitchChannels.end()) - { - // We have already pushed the live state of this stream - // Could not find stream in fake Twitch channels! - return; - } - - if (Toasts::isEnabled()) - { - getApp()->toasts->sendChannelNotification(channelName, - Platform::Twitch); - } - if (getSettings()->notificationPlaySound && - !(isInStreamerMode() && - getSettings()->streamerModeSuppressLiveNotifications)) - { - getApp()->notifications->playSound(); - } - if (getSettings()->notificationFlashTaskbar && - !(isInStreamerMode() && - getSettings()->streamerModeSuppressLiveNotifications)) - { - getApp()->windows->sendAlert(); - } - MessageBuilder builder; - TwitchMessageBuilder::liveMessage(channelName, &builder); - getApp()->twitch->liveChannel->addMessage(builder.release()); - - // Indicate that we have pushed notifications for this stream - fakeTwitchChannels.push_back(channelName); -} - -void NotificationController::removeFakeChannel(const QString channelName) -{ - auto i = std::find(fakeTwitchChannels.begin(), fakeTwitchChannels.end(), - channelName); - if (i != fakeTwitchChannels.end()) - { - fakeTwitchChannels.erase(i); - // "delete" old 'CHANNEL is live' message - LimitedQueueSnapshot snapshot = - getApp()->twitch->liveChannel->getMessageSnapshot(); - int snapshotLength = snapshot.size(); - - // MSVC hates this code if the parens are not there - int end = (std::max)(0, snapshotLength - 200); - // this assumes that channelName is a login name therefore will only delete messages from fake channels - auto liveMessageSearchText = QString("%1 is live!").arg(channelName); - - for (int i = snapshotLength - 1; i >= end; --i) - { - auto &s = snapshot[i]; - - if (s->messageText == liveMessageSearchText) - { - s->flags.set(MessageFlag::Disabled); - break; - } - } - } + this->notifyTwitchChannelLive({ + .channelId = stream->userId, + .channelName = channelName, + .displayName = stream->userName, + .title = stream->title, + .isInitialUpdate = isInitialUpdate, + }); } } // namespace chatterino diff --git a/src/controllers/notifications/NotificationController.hpp b/src/controllers/notifications/NotificationController.hpp index 6828c3be2..e7ad1731f 100644 --- a/src/controllers/notifications/NotificationController.hpp +++ b/src/controllers/notifications/NotificationController.hpp @@ -1,8 +1,8 @@ #pragma once +#include "common/ChatterinoSetting.hpp" #include "common/SignalVector.hpp" -#include "common/Singleton.hpp" -#include "singletons/Settings.hpp" +#include "util/QCompareCaseInsensitive.hpp" #include @@ -10,6 +10,7 @@ namespace chatterino { class Settings; class Paths; +struct HelixStream; class NotificationModel; @@ -17,34 +18,61 @@ enum class Platform : uint8_t { Twitch, // 0 }; -class NotificationController final : public Singleton, private QObject +class NotificationController final { public: - virtual void initialize(Settings &settings, Paths &paths) override; + NotificationController(); - bool isChannelNotified(const QString &channelName, Platform p); + // Perform an initial load so we don't have to wait for the timer + void initialize(); + + bool isChannelNotified(const QString &channelName, Platform p) const; void updateChannelNotification(const QString &channelName, Platform p); void addChannelNotification(const QString &channelName, Platform p); void removeChannelNotification(const QString &channelName, Platform p); - void playSound(); + struct NotificationPayload { + QString channelId; + QString channelName; + QString displayName; + QString title; + bool isInitialUpdate = false; + }; - SignalVector getVector(Platform p); + /// @brief Sends out notifications for a channel that has gone live + /// + /// This doesn't check for duplicate notifications. + void notifyTwitchChannelLive(const NotificationPayload &payload) const; - std::map> channelMap; + /// @brief Sends out notifications for a channel that has gone offline + /// + /// This doesn't check for duplicate notifications. + void notifyTwitchChannelOffline(const QString &id) const; + + void playSound() const; NotificationModel *createModel(QObject *parent, Platform p); private: - bool initialized_ = false; - void fetchFakeChannels(); - void removeFakeChannel(const QString channelName); - void checkStream(bool live, QString channelName); + void removeFakeChannel(const QString &channelName); + void updateFakeChannel(const QString &channelName, + const std::optional &stream); - // fakeTwitchChannels is a list of streams who are live that we have already sent out a notification for - std::vector fakeTwitchChannels; - QTimer *liveStatusTimer_; + struct FakeChannel { + QString id; + bool isLive = false; + }; + + /// @brief This map tracks channels without an associated TwitchChannel + /// + /// These channels won't be tracked in LiveController. + /// Channels are identified by their login name (case insensitive). + std::map fakeChannels_; + + QTimer liveStatusTimer_; + + std::map> channelMap; ChatterinoSetting> twitchSetting_ = { "/notifications/twitch"}; diff --git a/src/controllers/notifications/NotificationModel.cpp b/src/controllers/notifications/NotificationModel.cpp index 5de8bc903..b68a50441 100644 --- a/src/controllers/notifications/NotificationModel.cpp +++ b/src/controllers/notifications/NotificationModel.cpp @@ -1,4 +1,4 @@ -#include "NotificationModel.hpp" +#include "controllers/notifications/NotificationModel.hpp" #include "Application.hpp" #include "singletons/Settings.hpp" diff --git a/src/controllers/notifications/NotificationModel.hpp b/src/controllers/notifications/NotificationModel.hpp index 83d14ba08..dc53e953f 100644 --- a/src/controllers/notifications/NotificationModel.hpp +++ b/src/controllers/notifications/NotificationModel.hpp @@ -1,9 +1,8 @@ #pragma once -#include - #include "common/SignalVectorModel.hpp" -#include "controllers/notifications/NotificationController.hpp" + +#include namespace chatterino { @@ -15,12 +14,12 @@ class NotificationModel : public SignalVectorModel protected: // turn a vector item into a model row - virtual QString getItemFromRow(std::vector &row, - const QString &original) override; + QString getItemFromRow(std::vector &row, + const QString &original) override; // turns a row in the model into a vector item - virtual void getRowFromItem(const QString &item, - std::vector &row) override; + void getRowFromItem(const QString &item, + std::vector &row) override; friend class NotificationController; }; diff --git a/src/controllers/pings/MutedChannelModel.cpp b/src/controllers/pings/MutedChannelModel.cpp index fc1473506..df32f53d5 100644 --- a/src/controllers/pings/MutedChannelModel.cpp +++ b/src/controllers/pings/MutedChannelModel.cpp @@ -1,4 +1,4 @@ -#include "MutedChannelModel.hpp" +#include "controllers/pings/MutedChannelModel.hpp" #include "Application.hpp" #include "singletons/Settings.hpp" diff --git a/src/controllers/pings/MutedChannelModel.hpp b/src/controllers/pings/MutedChannelModel.hpp index c53b3177c..926285057 100644 --- a/src/controllers/pings/MutedChannelModel.hpp +++ b/src/controllers/pings/MutedChannelModel.hpp @@ -1,26 +1,23 @@ #pragma once +#include "common/SignalVectorModel.hpp" + #include -#include "common/SignalVectorModel.hpp" -#include "controllers/notifications/NotificationController.hpp" - namespace chatterino { -class MutedChannelController; - class MutedChannelModel : public SignalVectorModel { explicit MutedChannelModel(QObject *parent); protected: // turn a vector item into a model row - virtual QString getItemFromRow(std::vector &row, - const QString &original) override; + QString getItemFromRow(std::vector &row, + const QString &original) override; // turns a row in the model into a vector item - virtual void getRowFromItem(const QString &item, - std::vector &row) override; + void getRowFromItem(const QString &item, + std::vector &row) override; }; } // namespace chatterino diff --git a/src/controllers/plugins/LuaAPI.cpp b/src/controllers/plugins/LuaAPI.cpp new file mode 100644 index 000000000..03d11750a --- /dev/null +++ b/src/controllers/plugins/LuaAPI.cpp @@ -0,0 +1,369 @@ +#ifdef CHATTERINO_HAVE_PLUGINS +# include "controllers/plugins/LuaAPI.hpp" + +# include "Application.hpp" +# include "common/QLogging.hpp" +# include "controllers/commands/CommandController.hpp" +# include "controllers/plugins/LuaUtilities.hpp" +# include "controllers/plugins/PluginController.hpp" +# include "messages/MessageBuilder.hpp" +# include "providers/twitch/TwitchIrcServer.hpp" + +extern "C" { +# include +# include +# include +} +# include +# include +# include +# include + +namespace { +using namespace chatterino; + +void logHelper(lua_State *L, Plugin *pl, QDebug stream, int argc) +{ + stream.noquote(); + stream << "[" + pl->id + ":" + pl->meta.name + "]"; + for (int i = 1; i <= argc; i++) + { + stream << lua::toString(L, i); + } + lua_pop(L, argc); +} + +QDebug qdebugStreamForLogLevel(lua::api::LogLevel lvl) +{ + auto base = + (QMessageLogger(QT_MESSAGELOG_FILE, QT_MESSAGELOG_LINE, + QT_MESSAGELOG_FUNC, chatterinoLua().categoryName())); + + using LogLevel = lua::api::LogLevel; + + switch (lvl) + { + case LogLevel::Debug: + return base.debug(); + case LogLevel::Info: + return base.info(); + case LogLevel::Warning: + return base.warning(); + case LogLevel::Critical: + return base.critical(); + default: + assert(false && "if this happens magic_enum must have failed us"); + return QDebug((QString *)nullptr); + } +} + +} // namespace + +// NOLINTBEGIN(*vararg) +// luaL_error is a c-style vararg function, this makes clang-tidy not dislike it so much +namespace chatterino::lua::api { + +int c2_register_command(lua_State *L) +{ + auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L); + if (pl == nullptr) + { + luaL_error(L, "internal error: no plugin"); + return 0; + } + + QString name; + if (!lua::peek(L, &name, 1)) + { + luaL_error(L, "cannot get command name (1st arg of register_command, " + "expected a string)"); + return 0; + } + if (lua_isnoneornil(L, 2)) + { + luaL_error(L, "missing argument for register_command: function " + "\"pointer\""); + return 0; + } + + auto callbackSavedName = QString("c2commandcb-%1").arg(name); + lua_setfield(L, LUA_REGISTRYINDEX, callbackSavedName.toStdString().c_str()); + auto ok = pl->registerCommand(name, callbackSavedName); + + // delete both name and callback + lua_pop(L, 2); + + lua::push(L, ok); + return 1; +} + +int c2_register_callback(lua_State *L) +{ + auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L); + if (pl == nullptr) + { + luaL_error(L, "internal error: no plugin"); + return 0; + } + EventType evtType{}; + if (!lua::peek(L, &evtType, 1)) + { + luaL_error(L, "cannot get event name (1st arg of register_callback, " + "expected a string)"); + return 0; + } + if (lua_isnoneornil(L, 2)) + { + luaL_error(L, "missing argument for register_callback: function " + "\"pointer\""); + return 0; + } + + auto typeName = magic_enum::enum_name(evtType); + std::string callbackSavedName; + callbackSavedName.reserve(5 + typeName.size()); + callbackSavedName += "c2cb-"; + callbackSavedName += typeName; + lua_setfield(L, LUA_REGISTRYINDEX, callbackSavedName.c_str()); + + lua_pop(L, 2); + + return 0; +} + +int c2_log(lua_State *L) +{ + auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L); + if (pl == nullptr) + { + luaL_error(L, "c2_log: internal error: no plugin?"); + return 0; + } + auto logc = lua_gettop(L) - 1; + // This is almost the expansion of qCDebug() macro, actual thing is wrapped in a for loop + LogLevel lvl{}; + if (!lua::pop(L, &lvl, 1)) + { + luaL_error(L, "Invalid log level, use one from c2.LogLevel."); + return 0; + } + QDebug stream = qdebugStreamForLogLevel(lvl); + logHelper(L, pl, stream, logc); + return 0; +} + +int c2_later(lua_State *L) +{ + auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L); + if (pl == nullptr) + { + return luaL_error(L, "c2.later: internal error: no plugin?"); + } + if (lua_gettop(L) != 2) + { + return luaL_error( + L, "c2.later expects two arguments (a callback that takes no " + "arguments and returns nothing and a number the time in " + "milliseconds to wait)\n"); + } + int time{}; + if (!lua::pop(L, &time)) + { + return luaL_error(L, "cannot get time (2nd arg of c2.later, " + "expected a number)"); + } + + if (!lua_isfunction(L, lua_gettop(L))) + { + return luaL_error(L, "cannot get callback (1st arg of c2.later, " + "expected a function)"); + } + + auto *timer = new QTimer(); + timer->setInterval(time); + auto id = pl->addTimeout(timer); + auto name = QString("timeout_%1").arg(id); + auto *coro = lua_newthread(L); + + QObject::connect(timer, &QTimer::timeout, [pl, coro, name, timer]() { + timer->deleteLater(); + pl->removeTimeout(timer); + int nres{}; + lua_resume(coro, nullptr, 0, &nres); + + lua_pushnil(coro); + lua_setfield(coro, LUA_REGISTRYINDEX, name.toStdString().c_str()); + if (lua_gettop(coro) != 0) + { + stackDump(coro, + pl->id + + ": timer returned a value, this shouldn't happen " + "and is probably a plugin bug"); + } + }); + stackDump(L, "before setfield"); + lua_setfield(L, LUA_REGISTRYINDEX, name.toStdString().c_str()); + lua_xmove(L, coro, 1); // move function to thread + timer->start(); + + return 0; +} + +int g_load(lua_State *L) +{ +# ifdef NDEBUG + luaL_error(L, "load() is only usable in debug mode"); + return 0; +# else + auto countArgs = lua_gettop(L); + QByteArray data; + if (lua::peek(L, &data, 1)) + { + auto *utf8 = QTextCodec::codecForName("UTF-8"); + QTextCodec::ConverterState state; + utf8->toUnicode(data.constData(), data.size(), &state); + if (state.invalidChars != 0) + { + luaL_error(L, "invalid utf-8 in load() is not allowed"); + return 0; + } + } + else + { + luaL_error(L, "using reader function in load() is not allowed"); + return 0; + } + + for (int i = 0; i < countArgs; i++) + { + lua_seti(L, LUA_REGISTRYINDEX, i); + } + + // fetch load and call it + lua_getfield(L, LUA_REGISTRYINDEX, "real_load"); + + for (int i = 0; i < countArgs; i++) + { + lua_geti(L, LUA_REGISTRYINDEX, i); + lua_pushnil(L); + lua_seti(L, LUA_REGISTRYINDEX, i); + } + + lua_call(L, countArgs, LUA_MULTRET); + + return lua_gettop(L); +# endif +} + +int loadfile(lua_State *L, const QString &str) +{ + auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L); + if (pl == nullptr) + { + return luaL_error(L, "loadfile: internal error: no plugin?"); + } + auto dir = QUrl(pl->loadDirectory().canonicalPath() + "/"); + + if (!dir.isParentOf(str)) + { + // XXX: This intentionally hides the resolved path to not leak it + lua::push( + L, QString("requested module is outside of the plugin directory")); + return 1; + } + auto datadir = QUrl(pl->dataDirectory().canonicalPath() + "/"); + if (datadir.isParentOf(str)) + { + lua::push(L, QString("requested file is data, not code, see Chatterino " + "documentation")); + return 1; + } + + QFileInfo info(str); + if (!info.exists()) + { + lua::push(L, QString("no file '%1'").arg(str)); + return 1; + } + + auto temp = str.toStdString(); + const auto *filename = temp.c_str(); + + auto res = luaL_loadfilex(L, filename, "t"); + // Yoinked from checkload lib/lua/src/loadlib.c + if (res == LUA_OK) + { + lua_pushstring(L, filename); + return 2; + } + + return luaL_error(L, "error loading module '%s' from file '%s':\n\t%s", + lua_tostring(L, 1), filename, lua_tostring(L, -1)); +} + +int searcherAbsolute(lua_State *L) +{ + auto name = QString::fromUtf8(luaL_checkstring(L, 1)); + name = name.replace('.', QDir::separator()); + + QString filename; + auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L); + if (pl == nullptr) + { + return luaL_error(L, "searcherAbsolute: internal error: no plugin?"); + } + + QFileInfo file(pl->loadDirectory().filePath(name + ".lua")); + return loadfile(L, file.canonicalFilePath()); +} + +int searcherRelative(lua_State *L) +{ + lua_Debug dbg; + lua_getstack(L, 1, &dbg); + lua_getinfo(L, "S", &dbg); + auto currentFile = QString::fromUtf8(dbg.source, dbg.srclen); + if (currentFile.startsWith("@")) + { + currentFile = currentFile.mid(1); + } + if (currentFile == "=[C]" || currentFile == "") + { + lua::push( + L, + QString( + "Unable to load relative to file:caller has no source file")); + return 1; + } + + auto parent = QFileInfo(currentFile).dir(); + + auto name = QString::fromUtf8(luaL_checkstring(L, 1)); + name = name.replace('.', QDir::separator()); + QString filename = + parent.canonicalPath() + QDir::separator() + name + ".lua"; + + return loadfile(L, filename); +} + +int g_print(lua_State *L) +{ + auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L); + if (pl == nullptr) + { + luaL_error(L, "c2_print: internal error: no plugin?"); + return 0; + } + auto argc = lua_gettop(L); + // This is almost the expansion of qCDebug() macro, actual thing is wrapped in a for loop + auto stream = + (QMessageLogger(QT_MESSAGELOG_FILE, QT_MESSAGELOG_LINE, + QT_MESSAGELOG_FUNC, chatterinoLua().categoryName()) + .debug()); + logHelper(L, pl, stream, argc); + return 0; +} + +} // namespace chatterino::lua::api +// NOLINTEND(*vararg) +#endif diff --git a/src/controllers/plugins/LuaAPI.hpp b/src/controllers/plugins/LuaAPI.hpp new file mode 100644 index 000000000..904c6daa4 --- /dev/null +++ b/src/controllers/plugins/LuaAPI.hpp @@ -0,0 +1,251 @@ +#pragma once + +#ifdef CHATTERINO_HAVE_PLUGINS + +extern "C" { +# include +} +# include "controllers/plugins/LuaUtilities.hpp" + +# include + +# include +# include +# include + +struct lua_State; +namespace chatterino::lua::api { +// function names in this namespace reflect what's visible inside Lua and follow the lua naming scheme + +// NOLINTBEGIN(readability-identifier-naming) +// Following functions are exposed in c2 table. + +// Comments in this file are special, the docs/plugin-meta.lua file is generated from them +// All multiline comments will be added into that file. See scripts/make_luals_meta.py script for more info. + +/** + * @exposeenum c2.LogLevel + */ +// Represents "calls" to qCDebug, qCInfo ... +enum class LogLevel { Debug, Info, Warning, Critical }; + +/** + * @exposeenum c2.EventType + */ +enum class EventType { + CompletionRequested, +}; + +/** + * @lua@class CommandContext + * @lua@field words string[] The words typed when executing the command. For example `/foo bar baz` will result in `{"/foo", "bar", "baz"}`. + * @lua@field channel c2.Channel The channel the command was executed in. + */ + +/** + * @lua@class CompletionList + */ +struct CompletionList { + /** + * @lua@field values string[] The completions + */ + std::vector values{}; + + /** + * @lua@field hide_others boolean Whether other completions from Chatterino should be hidden/ignored. + */ + bool hideOthers{}; +}; + +/** + * @lua@class CompletionEvent + */ +struct CompletionEvent { + /** + * @lua@field query string The word being completed + */ + QString query; + /** + * @lua@field full_text_content string Content of the text input + */ + QString full_text_content; + /** + * @lua@field cursor_position integer Position of the cursor in the text input in unicode codepoints (not bytes) + */ + int cursor_position{}; + /** + * @lua@field is_first_word boolean True if this is the first word in the input + */ + bool is_first_word{}; +}; + +/** + * @includefile common/Channel.hpp + * @includefile controllers/plugins/api/ChannelRef.hpp + * @includefile controllers/plugins/api/HTTPResponse.hpp + * @includefile controllers/plugins/api/HTTPRequest.hpp + * @includefile common/network/NetworkCommon.hpp + */ + +/** + * Registers a new command called `name` which when executed will call `handler`. + * + * @lua@param name string The name of the command. + * @lua@param handler fun(ctx: CommandContext) The handler to be invoked when the command gets executed. + * @lua@return boolean ok Returns `true` if everything went ok, `false` if a command with this name exists. + * @exposed c2.register_command + */ +int c2_register_command(lua_State *L); + +/** + * Registers a callback to be invoked when completions for a term are requested. + * + * @lua@param type "CompletionRequested" + * @lua@param func fun(event: CompletionEvent): CompletionList The callback to be invoked. + * @exposed c2.register_callback + */ +int c2_register_callback(lua_State *L); + +/** + * Writes a message to the Chatterino log. + * + * @lua@param level c2.LogLevel The desired level. + * @lua@param ... any Values to log. Should be convertible to a string with `tostring()`. + * @exposed c2.log + */ +int c2_log(lua_State *L); + +/** + * Calls callback around msec milliseconds later. Does not freeze Chatterino. + * + * @lua@param callback fun() The callback that will be called. + * @lua@param msec number How long to wait. + * @exposed c2.later + */ +int c2_later(lua_State *L); + +// These ones are global +int g_load(lua_State *L); +int g_print(lua_State *L); +// NOLINTEND(readability-identifier-naming) + +// This is for require() exposed as an element of package.searchers +int searcherAbsolute(lua_State *L); +int searcherRelative(lua_State *L); + +// This is a fat pointer that allows us to type check values given to functions needing a userdata. +// Ensure ALL userdata given to Lua are a subclass of this! Otherwise we garbage as a pointer! +struct UserData { + enum class Type { + Channel, + HTTPRequest, + HTTPResponse, + }; + Type type; + bool isWeak; +}; + +template +struct WeakPtrUserData : public UserData { + std::weak_ptr target; + + WeakPtrUserData(std::weak_ptr t) + : UserData() + , target(t) + { + this->type = T; + this->isWeak = true; + } + + static WeakPtrUserData *create(lua_State *L, std::weak_ptr target) + { + void *ptr = lua_newuserdata(L, sizeof(WeakPtrUserData)); + return new (ptr) WeakPtrUserData(target); + } + + static WeakPtrUserData *from(UserData *target) + { + if (!target->isWeak) + { + return nullptr; + } + if (target->type != T) + { + return nullptr; + } + return reinterpret_cast *>(target); + } + + static WeakPtrUserData *from(void *target) + { + return from(reinterpret_cast(target)); + } + + static int destroy(lua_State *L) + { + auto self = WeakPtrUserData::from(lua_touserdata(L, -1)); + // Note it is safe to only check the weakness of the pointer, as + // std::weak_ptr seems to have identical representation regardless of + // what it points to + assert(self->isWeak); + + self->target.reset(); + lua_pop(L, 1); // Lua deallocates the memory for full user data + return 0; + } +}; + +template +struct SharedPtrUserData : public UserData { + std::shared_ptr target; + + SharedPtrUserData(std::shared_ptr t) + : UserData() + , target(t) + { + this->type = T; + this->isWeak = false; + } + + static SharedPtrUserData *create(lua_State *L, + std::shared_ptr target) + { + void *ptr = lua_newuserdata(L, sizeof(SharedPtrUserData)); + return new (ptr) SharedPtrUserData(target); + } + + static SharedPtrUserData *from(UserData *target) + { + if (target->isWeak) + { + return nullptr; + } + if (target->type != T) + { + return nullptr; + } + return reinterpret_cast *>(target); + } + + static SharedPtrUserData *from(void *target) + { + return from(reinterpret_cast(target)); + } + + static int destroy(lua_State *L) + { + auto self = SharedPtrUserData::from(lua_touserdata(L, -1)); + // Note it is safe to only check the weakness of the pointer, as + // std::shared_ptr seems to have identical representation regardless of + // what it points to + assert(!self->isWeak); + + self->target.reset(); + lua_pop(L, 1); // Lua deallocates the memory for full user data + return 0; + } +}; + +} // namespace chatterino::lua::api + +#endif diff --git a/src/controllers/plugins/LuaUtilities.cpp b/src/controllers/plugins/LuaUtilities.cpp new file mode 100644 index 000000000..64af18c01 --- /dev/null +++ b/src/controllers/plugins/LuaUtilities.cpp @@ -0,0 +1,270 @@ +#ifdef CHATTERINO_HAVE_PLUGINS +# include "controllers/plugins/LuaUtilities.hpp" + +# include "common/Channel.hpp" +# include "common/QLogging.hpp" +# include "controllers/commands/CommandContext.hpp" +# include "controllers/plugins/api/ChannelRef.hpp" +# include "controllers/plugins/LuaAPI.hpp" + +extern "C" { +# include +# include +} + +# include +# include + +namespace chatterino::lua { + +void stackDump(lua_State *L, const QString &tag) +{ + qCDebug(chatterinoLua) << "--------------------"; + auto count = lua_gettop(L); + if (!tag.isEmpty()) + { + qCDebug(chatterinoLua) << "Tag: " << tag; + } + qCDebug(chatterinoLua) << "Count elems: " << count; + for (int i = 1; i <= count; i++) + { + auto typeint = lua_type(L, i); + if (typeint == LUA_TSTRING) + { + QString str; + lua::peek(L, &str, i); + qCDebug(chatterinoLua) + << "At" << i << "is a" << lua_typename(L, typeint) << "(" + << typeint << "): " << str; + } + else if (typeint == LUA_TTABLE) + { + qCDebug(chatterinoLua) + << "At" << i << "is a" << lua_typename(L, typeint) << "(" + << typeint << ")" + << "its length is " << lua_rawlen(L, i); + } + else + { + qCDebug(chatterinoLua) + << "At" << i << "is a" << lua_typename(L, typeint) << "(" + << typeint << ")"; + } + } + qCDebug(chatterinoLua) << "--------------------"; +} + +QString humanErrorText(lua_State *L, int errCode) +{ + QString errName; + switch (errCode) + { + case LUA_OK: + return "ok"; + case LUA_ERRRUN: + errName = "(runtime error)"; + break; + case LUA_ERRMEM: + errName = "(memory error)"; + break; + case LUA_ERRERR: + errName = "(error while handling another error)"; + break; + case LUA_ERRSYNTAX: + errName = "(syntax error)"; + break; + case LUA_YIELD: + errName = "(illegal coroutine yield)"; + break; + case LUA_ERRFILE: + errName = "(file error)"; + break; + case ERROR_BAD_PEEK: + errName = "(unable to convert value to c++)"; + break; + default: + errName = "(unknown error type)"; + } + QString errText; + if (peek(L, &errText)) + { + errName += " " + errText; + } + return errName; +} + +StackIdx pushEmptyArray(lua_State *L, int countArray) +{ + lua_createtable(L, countArray, 0); + return lua_gettop(L); +} + +StackIdx pushEmptyTable(lua_State *L, int countProperties) +{ + lua_createtable(L, 0, countProperties); + return lua_gettop(L); +} + +StackIdx push(lua_State *L, const QString &str) +{ + return lua::push(L, str.toStdString()); +} + +StackIdx push(lua_State *L, const std::string &str) +{ + lua_pushstring(L, str.c_str()); + return lua_gettop(L); +} + +StackIdx push(lua_State *L, const CommandContext &ctx) +{ + StackGuard guard(L, 1); + auto outIdx = pushEmptyTable(L, 2); + + push(L, ctx.words); + lua_setfield(L, outIdx, "words"); + + push(L, ctx.channel); + lua_setfield(L, outIdx, "channel"); + + return outIdx; +} + +StackIdx push(lua_State *L, const bool &b) +{ + lua_pushboolean(L, int(b)); + return lua_gettop(L); +} + +StackIdx push(lua_State *L, const int &b) +{ + lua_pushinteger(L, b); + return lua_gettop(L); +} + +StackIdx push(lua_State *L, const api::CompletionEvent &ev) +{ + auto idx = pushEmptyTable(L, 4); +# define PUSH(field) \ + lua::push(L, ev.field); \ + lua_setfield(L, idx, #field) + PUSH(query); + PUSH(full_text_content); + PUSH(cursor_position); + PUSH(is_first_word); +# undef PUSH + return idx; +} + +bool peek(lua_State *L, int *out, StackIdx idx) +{ + StackGuard guard(L); + if (lua_isnumber(L, idx) == 0) + { + return false; + } + + *out = lua_tointeger(L, idx); + return true; +} + +bool peek(lua_State *L, bool *out, StackIdx idx) +{ + StackGuard guard(L); + if (!lua_isboolean(L, idx)) + { + return false; + } + + *out = bool(lua_toboolean(L, idx)); + return true; +} + +bool peek(lua_State *L, double *out, StackIdx idx) +{ + StackGuard guard(L); + int ok{0}; + auto v = lua_tonumberx(L, idx, &ok); + if (ok != 0) + { + *out = v; + } + return ok != 0; +} + +bool peek(lua_State *L, QString *out, StackIdx idx) +{ + StackGuard guard(L); + size_t len{0}; + const char *str = lua_tolstring(L, idx, &len); + if (str == nullptr) + { + return false; + } + if (len >= INT_MAX) + { + assert(false && "string longer than INT_MAX, shit's fucked, yo"); + } + *out = QString::fromUtf8(str, int(len)); + return true; +} + +bool peek(lua_State *L, QByteArray *out, StackIdx idx) +{ + StackGuard guard(L); + size_t len{0}; + const char *str = lua_tolstring(L, idx, &len); + if (str == nullptr) + { + return false; + } + if (len >= INT_MAX) + { + assert(false && "string longer than INT_MAX, shit's fucked, yo"); + } + *out = QByteArray(str, int(len)); + return true; +} + +bool peek(lua_State *L, std::string *out, StackIdx idx) +{ + StackGuard guard(L); + size_t len{0}; + const char *str = lua_tolstring(L, idx, &len); + if (str == nullptr) + { + return false; + } + if (len >= INT_MAX) + { + assert(false && "string longer than INT_MAX, shit's fucked, yo"); + } + *out = std::string(str, len); + return true; +} + +bool peek(lua_State *L, api::CompletionList *out, StackIdx idx) +{ + StackGuard guard(L); + int typ = lua_getfield(L, idx, "values"); + if (typ != LUA_TTABLE) + { + lua_pop(L, 1); + return false; + } + if (!lua::pop(L, &out->values, -1)) + { + return false; + } + lua_getfield(L, idx, "hide_others"); + return lua::pop(L, &out->hideOthers); +} + +QString toString(lua_State *L, StackIdx idx) +{ + size_t len{}; + const auto *ptr = luaL_tolstring(L, idx, &len); + return QString::fromUtf8(ptr, int(len)); +} +} // namespace chatterino::lua +#endif diff --git a/src/controllers/plugins/LuaUtilities.hpp b/src/controllers/plugins/LuaUtilities.hpp new file mode 100644 index 000000000..5443a751f --- /dev/null +++ b/src/controllers/plugins/LuaUtilities.hpp @@ -0,0 +1,385 @@ +#pragma once + +#ifdef CHATTERINO_HAVE_PLUGINS + +# include "common/QLogging.hpp" + +extern "C" { +# include +# include +} +# include +# include + +# include +# include +# include +# include +# include +# include +# include +struct lua_State; +class QJsonObject; +namespace chatterino { +struct CommandContext; +} // namespace chatterino + +namespace chatterino::lua { + +namespace api { + struct CompletionList; + struct CompletionEvent; +} // namespace api + +constexpr int ERROR_BAD_PEEK = LUA_OK - 1; + +/** + * @brief Dumps the Lua stack into qCDebug(chatterinoLua) + * + * @param tag is a string to let you know which dump is which when browsing logs + */ +void stackDump(lua_State *L, const QString &tag); + +/** + * @brief Converts a lua error code and potentially string on top of the stack into a human readable message + */ +QString humanErrorText(lua_State *L, int errCode); + +/** + * Represents an index into Lua's stack + */ +using StackIdx = int; + +/** + * @brief Creates a table with countArray array properties on the Lua stack + * @return stack index of the newly created table + */ +StackIdx pushEmptyArray(lua_State *L, int countArray); + +/** + * @brief Creates a table with countProperties named properties on the Lua stack + * @return stack index of the newly created table + */ +StackIdx pushEmptyTable(lua_State *L, int countProperties); + +StackIdx push(lua_State *L, const CommandContext &ctx); +StackIdx push(lua_State *L, const QString &str); +StackIdx push(lua_State *L, const std::string &str); +StackIdx push(lua_State *L, const bool &b); +StackIdx push(lua_State *L, const int &b); +StackIdx push(lua_State *L, const api::CompletionEvent &ev); + +// returns OK? +bool peek(lua_State *L, int *out, StackIdx idx = -1); +bool peek(lua_State *L, bool *out, StackIdx idx = -1); +bool peek(lua_State *L, double *out, StackIdx idx = -1); +bool peek(lua_State *L, QString *out, StackIdx idx = -1); +bool peek(lua_State *L, QByteArray *out, StackIdx idx = -1); +bool peek(lua_State *L, std::string *out, StackIdx idx = -1); +bool peek(lua_State *L, api::CompletionList *out, StackIdx idx = -1); + +/** + * @brief Converts Lua object at stack index idx to a string. + */ +QString toString(lua_State *L, StackIdx idx = -1); + +// This object ensures that the stack is of expected size when it is destroyed +class StackGuard +{ + int expected; + lua_State *L; + +public: + /** + * Use this constructor if you expect the stack size to be the same on the + * destruction of the object as its creation + */ + StackGuard(lua_State *L) + : expected(lua_gettop(L)) + , L(L) + { + } + + /** + * Use this if you expect the stack size changing, diff is the expected difference + * Ex: diff=3 means three elements added to the stack + */ + StackGuard(lua_State *L, int diff) + : expected(lua_gettop(L) + diff) + , L(L) + { + } + + ~StackGuard() + { + if (expected < 0) + { + return; + } + int after = lua_gettop(this->L); + if (this->expected != after) + { + stackDump(this->L, "StackGuard check tripped"); + // clang-format off + // clang format likes to insert a new line which means that some builds won't show this message fully + assert(false && "internal error: lua stack was not in an expected state"); + // clang-format on + } + } + + // This object isn't meant to be passed around + StackGuard operator=(StackGuard &) = delete; + StackGuard &operator=(StackGuard &&) = delete; + StackGuard(StackGuard &) = delete; + StackGuard(StackGuard &&) = delete; + + // This function tells the StackGuard that the stack isn't in an expected state but it was handled + void handled() + { + this->expected = -1; + } +}; + +/// TEMPLATES + +template +StackIdx push(lua_State *L, std::optional val) +{ + if (val.has_value()) + { + return lua::push(L, *val); + } + lua_pushnil(L); + return lua_gettop(L); +} + +template +bool peek(lua_State *L, std::optional *out, StackIdx idx = -1) +{ + if (lua_isnil(L, idx)) + { + *out = std::nullopt; + return true; + } + + *out = T(); + return peek(L, out->operator->(), idx); +} + +template +bool peek(lua_State *L, std::vector *vec, StackIdx idx = -1) +{ + StackGuard guard(L); + + if (!lua_istable(L, idx)) + { + lua::stackDump(L, "!table"); + qCDebug(chatterinoLua) + << "value is not a table, type is" << lua_type(L, idx); + return false; + } + auto len = lua_rawlen(L, idx); + if (len == 0) + { + qCDebug(chatterinoLua) << "value has 0 length"; + return true; + } + if (len > 1'000'000) + { + qCDebug(chatterinoLua) << "value is too long"; + return false; + } + // count like lua + for (int i = 1; i <= len; i++) + { + lua_geti(L, idx, i); + std::optional obj; + if (!lua::peek(L, &obj)) + { + //lua_seti(L, LUA_REGISTRYINDEX, 1); // lazy + qCDebug(chatterinoLua) + << "Failed to convert lua object into c++: at array index " << i + << ":"; + stackDump(L, "bad conversion into string"); + return false; + } + lua_pop(L, 1); + vec->push_back(obj.value()); + } + return true; +} + +/** + * @brief Converts object at stack index idx to enum given by template parameter T + */ +template , bool>::type = true> +bool peek(lua_State *L, T *out, StackIdx idx = -1) +{ + std::string tmp; + if (!lua::peek(L, &tmp, idx)) + { + return false; + } + std::optional opt = magic_enum::enum_cast(tmp); + if (opt.has_value()) + { + *out = opt.value(); + return true; + } + + return false; +} + +/** + * @brief Converts a vector to Lua and pushes it onto the stack. + * + * Needs StackIdx push(lua_State*, T); to work. + * + * @return Stack index of newly created table. + */ +template +StackIdx push(lua_State *L, std::vector vec) +{ + auto out = pushEmptyArray(L, vec.size()); + int i = 1; + for (const auto &el : vec) + { + push(L, el); + lua_seti(L, out, i); + i += 1; + } + return out; +} + +/** + * @brief Converts a QList to Lua and pushes it onto the stack. + * + * Needs StackIdx push(lua_State*, T); to work. + * + * @return Stack index of newly created table. + */ +template +StackIdx push(lua_State *L, QList vec) +{ + auto out = pushEmptyArray(L, vec.size()); + int i = 1; + for (const auto &el : vec) + { + push(L, el); + lua_seti(L, out, i); + i += 1; + } + return out; +} + +/** + * @brief Converts an enum given by T to Lua (into a string) and pushes it onto the stack. + * + * @return Stack index of newly created string. + */ +template , bool> = true> +StackIdx push(lua_State *L, T inp) +{ + std::string_view name = magic_enum::enum_name(inp); + return lua::push(L, std::string(name)); +} + +/** + * @brief Converts a Lua object into c++ and removes it from the stack. + * If peek fails, the object is still removed from the stack. + * + * Relies on bool peek(lua_State*, T*, StackIdx) existing. + */ +template +bool pop(lua_State *L, T *out, StackIdx idx = -1) +{ + StackGuard guard(L, -1); + auto ok = peek(L, out, idx); + if (idx < 0) + { + idx = lua_gettop(L) + idx + 1; + } + lua_remove(L, idx); + return ok; +} + +/** + * @brief Creates a table mapping enum names to unique values. + * + * Values in this table may change. + * + * @returns stack index of newly created table + */ +template +StackIdx pushEnumTable(lua_State *L) +{ + // std::array + auto values = magic_enum::enum_values(); + StackIdx out = lua::pushEmptyTable(L, values.size()); + for (const T v : values) + { + std::string_view name = magic_enum::enum_name(v); + std::string str(name); + + lua::push(L, str); + lua_setfield(L, out, str.c_str()); + } + return out; +} + +// Represents a Lua function on the stack +template +class CallbackFunction +{ + StackIdx stackIdx_; + lua_State *L; + +public: + CallbackFunction(lua_State *L, StackIdx stackIdx) + : stackIdx_(stackIdx) + , L(L) + { + } + + // this type owns the stackidx, it must not be trivially copiable + CallbackFunction operator=(CallbackFunction &) = delete; + CallbackFunction(CallbackFunction &) = delete; + + // Permit only move + CallbackFunction &operator=(CallbackFunction &&) = default; + CallbackFunction(CallbackFunction &&) = default; + + ~CallbackFunction() + { + lua_remove(L, this->stackIdx_); + } + + std::variant operator()(Args... arguments) + { + lua_pushvalue(this->L, this->stackIdx_); + ( // apparently this calls lua::push() for every Arg + [this, &arguments] { + lua::push(this->L, arguments); + }(), + ...); + + int res = lua_pcall(L, sizeof...(Args), 1, 0); + if (res != LUA_OK) + { + qCDebug(chatterinoLua) << "error is: " << res; + return {res}; + } + + ReturnType val; + if (!lua::pop(L, &val)) + { + return {ERROR_BAD_PEEK}; + } + return {val}; + } +}; + +} // namespace chatterino::lua + +#endif diff --git a/src/controllers/plugins/Plugin.cpp b/src/controllers/plugins/Plugin.cpp new file mode 100644 index 000000000..dcf7357e8 --- /dev/null +++ b/src/controllers/plugins/Plugin.cpp @@ -0,0 +1,287 @@ +#ifdef CHATTERINO_HAVE_PLUGINS +# include "controllers/plugins/Plugin.hpp" + +# include "common/network/NetworkCommon.hpp" +# include "common/QLogging.hpp" +# include "controllers/commands/CommandController.hpp" +# include "controllers/plugins/PluginPermission.hpp" +# include "util/QMagicEnum.hpp" + +extern "C" { +# include +} +# include +# include +# include +# include +# include + +# include +# include +# include + +namespace chatterino { + +PluginMeta::PluginMeta(const QJsonObject &obj) +{ + auto homepageObj = obj.value("homepage"); + if (homepageObj.isString()) + { + this->homepage = homepageObj.toString(); + } + else if (!homepageObj.isUndefined()) + { + auto type = qmagicenum::enumName(homepageObj.type()); + this->errors.emplace_back( + QString("homepage is defined but is not a string (its type is %1)") + .arg(type)); + } + auto nameObj = obj.value("name"); + if (nameObj.isString()) + { + this->name = nameObj.toString(); + } + else + { + auto type = qmagicenum::enumName(nameObj.type()); + this->errors.emplace_back( + QString("name is not a string (its type is %1)").arg(type)); + } + + auto descrObj = obj.value("description"); + if (descrObj.isString()) + { + this->description = descrObj.toString(); + } + else + { + auto type = qmagicenum::enumName(descrObj.type()); + this->errors.emplace_back( + QString("description is not a string (its type is %1)").arg(type)); + } + + auto authorsObj = obj.value("authors"); + if (authorsObj.isArray()) + { + auto authorsArr = authorsObj.toArray(); + for (int i = 0; i < authorsArr.size(); i++) + { + const auto &t = authorsArr.at(i); + if (!t.isString()) + { + auto type = qmagicenum::enumName(t.type()); + this->errors.push_back( + QString("authors element #%1 is not a string (it is a %2)") + .arg(i) + .arg(type)); + break; + } + this->authors.push_back(t.toString()); + } + } + else + { + auto type = qmagicenum::enumName(authorsObj.type()); + this->errors.emplace_back( + QString("authors is not an array (its type is %1)").arg(type)); + } + + auto licenseObj = obj.value("license"); + if (licenseObj.isString()) + { + this->license = licenseObj.toString(); + } + else + { + auto type = qmagicenum::enumName(licenseObj.type()); + this->errors.emplace_back( + QString("license is not a string (its type is %1)").arg(type)); + } + + auto verObj = obj.value("version"); + if (verObj.isString()) + { + auto v = semver::from_string_noexcept(verObj.toString().toStdString()); + if (v.has_value()) + { + this->version = v.value(); + } + else + { + this->errors.emplace_back("unable to parse version (use semver)"); + this->version = semver::version(0, 0, 0); + } + } + else + { + auto type = qmagicenum::enumName(verObj.type()); + this->errors.emplace_back( + QString("version is not a string (its type is %1)").arg(type)); + this->version = semver::version(0, 0, 0); + } + auto permsObj = obj.value("permissions"); + if (!permsObj.isUndefined()) + { + if (!permsObj.isArray()) + { + auto type = qmagicenum::enumName(permsObj.type()); + this->errors.emplace_back( + QString("permissions is not an array (its type is %1)") + .arg(type)); + return; + } + + auto permsArr = permsObj.toArray(); + for (int i = 0; i < permsArr.size(); i++) + { + const auto &t = permsArr.at(i); + if (!t.isObject()) + { + auto type = qmagicenum::enumName(t.type()); + this->errors.push_back(QString("permissions element #%1 is not " + "an object (its type is %2)") + .arg(i) + .arg(type)); + return; + } + auto parsed = PluginPermission(t.toObject()); + if (parsed.isValid()) + { + // ensure no invalid permissions slip through this + this->permissions.push_back(parsed); + } + else + { + for (const auto &err : parsed.errors) + { + this->errors.push_back( + QString("permissions element #%1: %2").arg(i).arg(err)); + } + } + } + } + + auto tagsObj = obj.value("tags"); + if (!tagsObj.isUndefined()) + { + if (!tagsObj.isArray()) + { + auto type = qmagicenum::enumName(tagsObj.type()); + this->errors.emplace_back( + QString("tags is not an array (its type is %1)").arg(type)); + return; + } + + auto tagsArr = tagsObj.toArray(); + for (int i = 0; i < tagsArr.size(); i++) + { + const auto &t = tagsArr.at(i); + if (!t.isString()) + { + auto type = qmagicenum::enumName(t.type()); + this->errors.push_back( + QString("tags element #%1 is not a string (its type is %2)") + .arg(i) + .arg(type)); + return; + } + this->tags.push_back(t.toString()); + } + } +} + +bool Plugin::registerCommand(const QString &name, const QString &functionName) +{ + if (this->ownedCommands.find(name) != this->ownedCommands.end()) + { + return false; + } + + auto ok = getApp()->getCommands()->registerPluginCommand(name); + if (!ok) + { + return false; + } + this->ownedCommands.insert({name, functionName}); + return true; +} + +std::unordered_set Plugin::listRegisteredCommands() +{ + std::unordered_set out; + for (const auto &[name, _] : this->ownedCommands) + { + out.insert(name); + } + return out; +} + +Plugin::~Plugin() +{ + for (auto *timer : this->activeTimeouts) + { + QObject::disconnect(timer, nullptr, nullptr, nullptr); + timer->deleteLater(); + } + qCDebug(chatterinoLua) << "Destroyed" << this->activeTimeouts.size() + << "timers for plugin" << this->id + << "while destroying the object"; + this->activeTimeouts.clear(); + if (this->state_ != nullptr) + { + lua_close(this->state_); + } +} +int Plugin::addTimeout(QTimer *timer) +{ + this->activeTimeouts.push_back(timer); + return ++this->lastTimerId; +} + +void Plugin::removeTimeout(QTimer *timer) +{ + for (auto it = this->activeTimeouts.begin(); + it != this->activeTimeouts.end(); ++it) + { + if (*it == timer) + { + this->activeTimeouts.erase(it); + break; + } + } +} + +bool Plugin::hasFSPermissionFor(bool write, const QString &path) +{ + auto canon = QUrl(this->dataDirectory().absolutePath() + "/"); + if (!canon.isParentOf(path)) + { + return false; + } + + using PType = PluginPermission::Type; + auto typ = write ? PType::FilesystemWrite : PType::FilesystemRead; + + return std::ranges::any_of(this->meta.permissions, [=](const auto &p) { + return p.type == typ; + }); +} + +bool Plugin::hasHTTPPermissionFor(const QUrl &url) +{ + auto proto = url.scheme(); + if (proto != "http" && proto != "https") + { + qCWarning(chatterinoLua).nospace() + << "Plugin " << this->id << " (" << this->meta.name + << ") is trying to use a non-http protocol"; + return false; + } + + return std::ranges::any_of(this->meta.permissions, [](const auto &p) { + return p.type == PluginPermission::Type::Network; + }); +} + +} // namespace chatterino +#endif diff --git a/src/controllers/plugins/Plugin.hpp b/src/controllers/plugins/Plugin.hpp new file mode 100644 index 000000000..f8375247f --- /dev/null +++ b/src/controllers/plugins/Plugin.hpp @@ -0,0 +1,160 @@ +#pragma once + +#ifdef CHATTERINO_HAVE_PLUGINS +# include "Application.hpp" +# include "common/network/NetworkCommon.hpp" +# include "controllers/plugins/LuaAPI.hpp" +# include "controllers/plugins/LuaUtilities.hpp" +# include "controllers/plugins/PluginPermission.hpp" + +# include +# include +# include +# include + +# include +# include +# include + +struct lua_State; +class QTimer; + +namespace chatterino { + +struct PluginMeta { + // for more info on these fields see docs/plugin-info.schema.json + + // display name of the plugin + QString name; + + // description shown to the user + QString description; + + // plugin authors shown to the user + std::vector authors; + + // license name + QString license; + + // version of the plugin + semver::version version; + + // optionally a homepage link + QString homepage; + + // optionally tags that might help in searching for the plugin + std::vector tags; + + std::vector permissions; + + // errors that occurred while parsing info.json + std::vector errors; + + bool isValid() const + { + return this->errors.empty(); + } + + explicit PluginMeta(const QJsonObject &obj); +}; + +class Plugin +{ +public: + QString id; + PluginMeta meta; + + Plugin(QString id, lua_State *state, PluginMeta meta, + const QDir &loadDirectory) + : id(std::move(id)) + , meta(std::move(meta)) + , loadDirectory_(loadDirectory) + , state_(state) + { + } + + ~Plugin(); + + /** + * @brief Perform all necessary tasks to bind a command name to this plugin + * @param name name of the command to create + * @param functionName name of the function that should be called when the command is executed + * @return true if addition succeeded, false otherwise (for example because the command name is already taken) + */ + bool registerCommand(const QString &name, const QString &functionName); + + /** + * @brief Get names of all commands belonging to this plugin + */ + std::unordered_set listRegisteredCommands(); + + const QDir &loadDirectory() const + { + return this->loadDirectory_; + } + + QDir dataDirectory() const + { + return this->loadDirectory_.absoluteFilePath("data"); + } + + // Note: The CallbackFunction object's destructor will remove the function from the lua stack + using LuaCompletionCallback = + lua::CallbackFunction; + std::optional getCompletionCallback() + { + if (this->state_ == nullptr || !this->error_.isNull()) + { + return {}; + } + // this uses magic enum to help automatic tooling find usages + auto typeName = + magic_enum::enum_name(lua::api::EventType::CompletionRequested); + std::string cbName; + cbName.reserve(5 + typeName.size()); + cbName += "c2cb-"; + cbName += typeName; + auto typ = + lua_getfield(this->state_, LUA_REGISTRYINDEX, cbName.c_str()); + if (typ != LUA_TFUNCTION) + { + lua_pop(this->state_, 1); + return {}; + } + + // move + return std::make_optional>( + this->state_, lua_gettop(this->state_)); + } + + /** + * If the plugin crashes while evaluating the main file, this function will return the error + */ + QString error() + { + return this->error_; + } + + int addTimeout(QTimer *timer); + void removeTimeout(QTimer *timer); + + bool hasFSPermissionFor(bool write, const QString &path); + bool hasHTTPPermissionFor(const QUrl &url); + +private: + QDir loadDirectory_; + lua_State *state_; + + QString error_; + + // maps command name -> function name + std::unordered_map ownedCommands; + std::vector activeTimeouts; + int lastTimerId = 0; + + friend class PluginController; +}; +} // namespace chatterino +#endif diff --git a/src/controllers/plugins/PluginController.cpp b/src/controllers/plugins/PluginController.cpp new file mode 100644 index 000000000..5ca77ed40 --- /dev/null +++ b/src/controllers/plugins/PluginController.cpp @@ -0,0 +1,477 @@ +#ifdef CHATTERINO_HAVE_PLUGINS +# include "controllers/plugins/PluginController.hpp" + +# include "Application.hpp" +# include "common/Args.hpp" +# include "common/network/NetworkCommon.hpp" +# include "common/QLogging.hpp" +# include "controllers/commands/CommandContext.hpp" +# include "controllers/commands/CommandController.hpp" +# include "controllers/plugins/api/ChannelRef.hpp" +# include "controllers/plugins/api/HTTPRequest.hpp" +# include "controllers/plugins/api/HTTPResponse.hpp" +# include "controllers/plugins/api/IOWrapper.hpp" +# include "controllers/plugins/LuaAPI.hpp" +# include "controllers/plugins/LuaUtilities.hpp" +# include "messages/MessageBuilder.hpp" +# include "singletons/Paths.hpp" +# include "singletons/Settings.hpp" + +extern "C" { +# include +# include +# include +} +# include + +# include +# include +# include + +namespace chatterino { + +PluginController::PluginController(const Paths &paths_) + : paths(paths_) +{ +} + +void PluginController::initialize(Settings &settings) +{ + // actuallyInitialize will be called by this connection + settings.pluginsEnabled.connect([this](bool enabled) { + if (enabled) + { + this->loadPlugins(); + } + else + { + // uninitialize plugins + this->plugins_.clear(); + } + }); +} + +void PluginController::loadPlugins() +{ + this->plugins_.clear(); + auto dir = QDir(this->paths.pluginsDirectory); + qCDebug(chatterinoLua) << "Loading plugins in" << dir.path(); + for (const auto &info : + dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot)) + { + auto pluginDir = QDir(info.absoluteFilePath()); + this->tryLoadFromDir(pluginDir); + } +} + +bool PluginController::tryLoadFromDir(const QDir &pluginDir) +{ + // look for init.lua + auto index = QFileInfo(pluginDir.filePath("init.lua")); + qCDebug(chatterinoLua) << "Looking for init.lua and info.json in" + << pluginDir.path(); + if (!index.exists()) + { + qCWarning(chatterinoLua) + << "Missing init.lua in plugin directory:" << pluginDir.path(); + return false; + } + qCDebug(chatterinoLua) << "Found init.lua, now looking for info.json!"; + auto infojson = QFileInfo(pluginDir.filePath("info.json")); + if (!infojson.exists()) + { + qCWarning(chatterinoLua) + << "Missing info.json in plugin directory" << pluginDir.path(); + return false; + } + QFile infoFile(infojson.absoluteFilePath()); + infoFile.open(QIODevice::ReadOnly); + auto everything = infoFile.readAll(); + auto doc = QJsonDocument::fromJson(everything); + if (!doc.isObject()) + { + qCWarning(chatterinoLua) + << "info.json root is not an object" << pluginDir.path(); + return false; + } + + auto meta = PluginMeta(doc.object()); + if (!meta.isValid()) + { + qCWarning(chatterinoLua) + << "Plugin from" << pluginDir << "is invalid because:"; + for (const auto &why : meta.errors) + { + qCWarning(chatterinoLua) << "- " << why; + } + auto plugin = std::make_unique(pluginDir.dirName(), nullptr, + meta, pluginDir); + this->plugins_.insert({pluginDir.dirName(), std::move(plugin)}); + return false; + } + this->load(index, pluginDir, meta); + return true; +} + +void PluginController::openLibrariesFor(lua_State *L, const PluginMeta &meta, + const QDir &pluginDir) +{ + lua::StackGuard guard(L); + // Stuff to change, remove or hide behind a permission system: + static const std::vector loadedlibs = { + luaL_Reg{LUA_GNAME, luaopen_base}, + // - load - don't allow in release mode + + luaL_Reg{LUA_COLIBNAME, luaopen_coroutine}, + luaL_Reg{LUA_TABLIBNAME, luaopen_table}, + // luaL_Reg{LUA_IOLIBNAME, luaopen_io}, + // - explicit fs access, needs wrapper with permissions, no usage ideas yet + // luaL_Reg{LUA_OSLIBNAME, luaopen_os}, + // - fs access + // - environ access + // - exit + luaL_Reg{LUA_STRLIBNAME, luaopen_string}, + luaL_Reg{LUA_MATHLIBNAME, luaopen_math}, + luaL_Reg{LUA_UTF8LIBNAME, luaopen_utf8}, + luaL_Reg{LUA_LOADLIBNAME, luaopen_package}, + }; + // Warning: Do not add debug library to this, it would make the security of + // this a living nightmare due to stuff like registry access + // - Mm2PL + + for (const auto ® : loadedlibs) + { + luaL_requiref(L, reg.name, reg.func, int(true)); + lua_pop(L, 1); + } + luaL_requiref(L, LUA_IOLIBNAME, luaopen_io, int(false)); + lua_setfield(L, LUA_REGISTRYINDEX, lua::api::REG_REAL_IO_NAME); + + // NOLINTNEXTLINE(*-avoid-c-arrays) + static const luaL_Reg c2Lib[] = { + {"register_command", lua::api::c2_register_command}, + {"register_callback", lua::api::c2_register_callback}, + {"log", lua::api::c2_log}, + {"later", lua::api::c2_later}, + {nullptr, nullptr}, + }; + lua_pushglobaltable(L); + auto gtable = lua_gettop(L); + + // count of elements in C2LIB + LogLevel + EventType + auto c2libIdx = lua::pushEmptyTable(L, 8); + + luaL_setfuncs(L, c2Lib, 0); + + lua::pushEnumTable(L); + lua_setfield(L, c2libIdx, "LogLevel"); + + lua::pushEnumTable(L); + lua_setfield(L, c2libIdx, "EventType"); + + lua::pushEnumTable(L); + lua_setfield(L, c2libIdx, "Platform"); + + lua::pushEnumTable(L); + lua_setfield(L, c2libIdx, "ChannelType"); + + lua::pushEnumTable(L); + lua_setfield(L, c2libIdx, "HTTPMethod"); + + // Initialize metatables for objects + lua::api::ChannelRef::createMetatable(L); + lua_setfield(L, c2libIdx, "Channel"); + + lua::api::HTTPRequest::createMetatable(L); + lua_setfield(L, c2libIdx, "HTTPRequest"); + + lua::api::HTTPResponse::createMetatable(L); + lua_setfield(L, c2libIdx, "HTTPResponse"); + + lua_setfield(L, gtable, "c2"); + + // ban functions + // Note: this might not be fully secure? some kind of metatable fuckery might come up? + + // possibly randomize this name at runtime to prevent some attacks? + +# ifndef NDEBUG + lua_getfield(L, gtable, "load"); + lua_setfield(L, LUA_REGISTRYINDEX, "real_load"); +# endif + + // NOLINTNEXTLINE(*-avoid-c-arrays) + static const luaL_Reg replacementFuncs[] = { + {"load", lua::api::g_load}, + {"print", lua::api::g_print}, + {nullptr, nullptr}, + }; + luaL_setfuncs(L, replacementFuncs, 0); + + lua_pushnil(L); + lua_setfield(L, gtable, "loadfile"); + + lua_pushnil(L); + lua_setfield(L, gtable, "dofile"); + + // set up package lib + lua_getfield(L, gtable, "package"); + + auto package = lua_gettop(L); + lua_pushstring(L, ""); + lua_setfield(L, package, "cpath"); + + // we don't use path + lua_pushstring(L, ""); + lua_setfield(L, package, "path"); + + { + lua_getfield(L, gtable, "table"); + auto table = lua_gettop(L); + lua_getfield(L, -1, "remove"); + lua_remove(L, table); + } + auto remove = lua_gettop(L); + + // remove searcher_Croot, searcher_C and searcher_Lua leaving only searcher_preload + for (int i = 0; i < 3; i++) + { + lua_pushvalue(L, remove); + lua_getfield(L, package, "searchers"); + lua_pcall(L, 1, 0, 0); + } + lua_pop(L, 1); // get rid of remove + + lua_getfield(L, package, "searchers"); + lua_pushcclosure(L, lua::api::searcherRelative, 0); + lua_seti(L, -2, 2); + + lua::push(L, QString(pluginDir.absolutePath())); + lua_pushcclosure(L, lua::api::searcherAbsolute, 1); + lua_seti(L, -2, 3); + lua_pop(L, 2); // remove package, package.searchers + + // NOLINTNEXTLINE(*-avoid-c-arrays) + static const luaL_Reg ioLib[] = { + {"close", lua::api::io_close}, + {"flush", lua::api::io_flush}, + {"input", lua::api::io_input}, + {"lines", lua::api::io_lines}, + {"open", lua::api::io_open}, + {"output", lua::api::io_output}, + {"popen", lua::api::io_popen}, // stub + {"read", lua::api::io_read}, + {"tmpfile", lua::api::io_tmpfile}, // stub + {"write", lua::api::io_write}, + // type = realio.type + {nullptr, nullptr}, + }; + // TODO: io.popen stub + auto iolibIdx = lua::pushEmptyTable(L, 1); + luaL_setfuncs(L, ioLib, 0); + + // set ourio.type = realio.type + lua_pushvalue(L, iolibIdx); + lua_getfield(L, LUA_REGISTRYINDEX, lua::api::REG_REAL_IO_NAME); + lua_getfield(L, -1, "type"); + lua_remove(L, -2); // remove realio + lua_setfield(L, iolibIdx, "type"); + lua_pop(L, 1); // still have iolib on top of stack + + lua_pushvalue(L, iolibIdx); + lua_setfield(L, gtable, "io"); + + lua_pushvalue(L, iolibIdx); + lua_setfield(L, LUA_REGISTRYINDEX, lua::api::REG_C2_IO_NAME); + + luaL_getsubtable(L, LUA_REGISTRYINDEX, LUA_LOADED_TABLE); + lua_pushvalue(L, iolibIdx); + lua_setfield(L, -2, "io"); + + lua_pop(L, 3); // remove gtable, iolib, LOADED + + // Don't give plugins the option to shit into our stdio + lua_pushnil(L); + lua_setfield(L, LUA_REGISTRYINDEX, "_IO_input"); + + lua_pushnil(L); + lua_setfield(L, LUA_REGISTRYINDEX, "_IO_output"); +} + +void PluginController::load(const QFileInfo &index, const QDir &pluginDir, + const PluginMeta &meta) +{ + auto pluginName = pluginDir.dirName(); + lua_State *l = luaL_newstate(); + auto plugin = std::make_unique(pluginName, l, meta, pluginDir); + auto *temp = plugin.get(); + this->plugins_.insert({pluginName, std::move(plugin)}); + + if (getApp()->getArgs().safeMode) + { + // This isn't done earlier to ensure the user can disable a misbehaving plugin + qCWarning(chatterinoLua) << "Skipping loading plugin " << meta.name + << " because safe mode is enabled."; + return; + } + PluginController::openLibrariesFor(l, meta, pluginDir); + + if (!PluginController::isPluginEnabled(pluginName) || + !getSettings()->pluginsEnabled) + { + qCDebug(chatterinoLua) << "Skipping loading" << pluginName << "(" + << meta.name << ") because it is disabled"; + return; + } + temp->dataDirectory().mkpath("."); + + qCDebug(chatterinoLua) << "Running lua file:" << index; + int err = luaL_dofile(l, index.absoluteFilePath().toStdString().c_str()); + if (err != 0) + { + temp->error_ = lua::humanErrorText(l, err); + qCWarning(chatterinoLua) + << "Failed to load" << pluginName << "plugin from" << index << ": " + << temp->error_; + return; + } + qCInfo(chatterinoLua) << "Loaded" << pluginName << "plugin from" << index; +} + +bool PluginController::reload(const QString &id) +{ + auto it = this->plugins_.find(id); + if (it == this->plugins_.end()) + { + return false; + } + if (it->second->state_ != nullptr) + { + lua_close(it->second->state_); + it->second->state_ = nullptr; + } + for (const auto &[cmd, _] : it->second->ownedCommands) + { + getApp()->getCommands()->unregisterPluginCommand(cmd); + } + it->second->ownedCommands.clear(); + QDir loadDir = it->second->loadDirectory_; + this->plugins_.erase(id); + this->tryLoadFromDir(loadDir); + return true; +} + +QString PluginController::tryExecPluginCommand(const QString &commandName, + const CommandContext &ctx) +{ + for (auto &[name, plugin] : this->plugins_) + { + if (auto it = plugin->ownedCommands.find(commandName); + it != plugin->ownedCommands.end()) + { + const auto &funcName = it->second; + + auto *L = plugin->state_; + lua_getfield(L, LUA_REGISTRYINDEX, funcName.toStdString().c_str()); + lua::push(L, ctx); + + auto res = lua_pcall(L, 1, 0, 0); + if (res != LUA_OK) + { + ctx.channel->addSystemMessage("Lua error: " + + lua::humanErrorText(L, res)); + return ""; + } + return ""; + } + } + qCCritical(chatterinoLua) + << "Something's seriously up, no plugin owns command" << commandName + << "yet a call to execute it came in"; + assert(false && "missing plugin command owner"); + return ""; +} + +bool PluginController::isPluginEnabled(const QString &id) +{ + auto vec = getSettings()->enabledPlugins.getValue(); + auto it = std::find(vec.begin(), vec.end(), id); + return it != vec.end(); +} + +Plugin *PluginController::getPluginByStatePtr(lua_State *L) +{ + lua_geti(L, LUA_REGISTRYINDEX, LUA_RIDX_MAINTHREAD); + // Use the main thread for identification, not a coroutine instance + auto *mainL = lua_tothread(L, -1); + lua_pop(L, 1); + L = mainL; + for (auto &[name, plugin] : this->plugins_) + { + if (plugin->state_ == L) + { + return plugin.get(); + } + } + return nullptr; +} + +const std::map> &PluginController::plugins() + const +{ + return this->plugins_; +} + +std::pair PluginController::updateCustomCompletions( + const QString &query, const QString &fullTextContent, int cursorPosition, + bool isFirstWord) const +{ + QStringList results; + + for (const auto &[name, pl] : this->plugins()) + { + if (!pl->error().isNull() || pl->state_ == nullptr) + { + continue; + } + + lua::StackGuard guard(pl->state_); + + auto opt = pl->getCompletionCallback(); + if (opt) + { + qCDebug(chatterinoLua) + << "Processing custom completions from plugin" << name; + auto &cb = *opt; + auto errOrList = cb(lua::api::CompletionEvent{ + .query = query, + .full_text_content = fullTextContent, + .cursor_position = cursorPosition, + .is_first_word = isFirstWord, + }); + if (std::holds_alternative(errOrList)) + { + guard.handled(); + int err = std::get(errOrList); + qCDebug(chatterinoLua) + << "Got error from plugin " << pl->meta.name + << " while refreshing tab completion: " + << lua::humanErrorText(pl->state_, err); + continue; + } + + auto list = std::get(errOrList); + if (list.hideOthers) + { + results = QStringList(list.values.begin(), list.values.end()); + return {true, results}; + } + results += QStringList(list.values.begin(), list.values.end()); + } + } + + return {false, results}; +} + +} // namespace chatterino +#endif diff --git a/src/controllers/plugins/PluginController.hpp b/src/controllers/plugins/PluginController.hpp new file mode 100644 index 000000000..50bc88c7e --- /dev/null +++ b/src/controllers/plugins/PluginController.hpp @@ -0,0 +1,77 @@ +#pragma once + +#ifdef CHATTERINO_HAVE_PLUGINS + +# include "controllers/commands/CommandContext.hpp" +# include "controllers/plugins/Plugin.hpp" + +# include +# include +# include +# include +# include + +# include +# include +# include +# include +# include + +struct lua_State; + +namespace chatterino { + +class Paths; + +class PluginController +{ + const Paths &paths; + +public: + explicit PluginController(const Paths &paths_); + + void initialize(Settings &settings); + + QString tryExecPluginCommand(const QString &commandName, + const CommandContext &ctx); + + // NOTE: this pointer does not own the Plugin, unique_ptr still owns it + // This is required to be public because of c functions + Plugin *getPluginByStatePtr(lua_State *L); + + // TODO: make a function that iterates plugins that aren't errored/enabled + const std::map> &plugins() const; + + /** + * @brief Reload plugin given by id + * + * @param id This is the unique identifier of the plugin, the name of the directory it is in + */ + bool reload(const QString &id); + + /** + * @brief Checks settings to tell if a plugin named by id is enabled. + * + * It is the callers responsibility to check Settings::pluginsEnabled + */ + static bool isPluginEnabled(const QString &id); + + std::pair updateCustomCompletions( + const QString &query, const QString &fullTextContent, + int cursorPosition, bool isFirstWord) const; + +private: + void loadPlugins(); + void load(const QFileInfo &index, const QDir &pluginDir, + const PluginMeta &meta); + + // This function adds lua standard libraries into the state + static void openLibrariesFor(lua_State *L, const PluginMeta & /*meta*/, + const QDir &pluginDir); + static void loadChatterinoLib(lua_State *l); + bool tryLoadFromDir(const QDir &pluginDir); + std::map> plugins_; +}; + +} // namespace chatterino +#endif diff --git a/src/controllers/plugins/PluginPermission.cpp b/src/controllers/plugins/PluginPermission.cpp new file mode 100644 index 000000000..9a4bc925e --- /dev/null +++ b/src/controllers/plugins/PluginPermission.cpp @@ -0,0 +1,49 @@ +#ifdef CHATTERINO_HAVE_PLUGINS +# include "controllers/plugins/PluginPermission.hpp" + +# include "util/QMagicEnum.hpp" + +# include +# include + +namespace chatterino { + +PluginPermission::PluginPermission(const QJsonObject &obj) +{ + auto jsontype = obj.value("type"); + if (!jsontype.isString()) + { + auto tn = qmagicenum::enumName(jsontype.type()); + this->errors.emplace_back(QString("permission type is defined but is " + "not a string (its type is %1)") + .arg(tn)); + } + auto opt = qmagicenum::enumCast( + jsontype.toString(), qmagicenum::CASE_INSENSITIVE); + if (!opt.has_value()) + { + this->errors.emplace_back(QString("permission type is an unknown (%1)") + .arg(jsontype.toString())); + return; // There is no more data to get, we don't know what to do + } + this->type = opt.value(); +} + +QString PluginPermission::toHtml() const +{ + switch (this->type) + { + case PluginPermission::Type::FilesystemRead: + return "Read files in its data directory"; + case PluginPermission::Type::FilesystemWrite: + return "Write to or create files in its data directory"; + case PluginPermission::Type::Network: + return "Make requests over the internet to third party websites"; + default: + assert(false && "invalid PluginPermission type in toHtml()"); + return "shut up compiler, this never happens"; + } +} + +} // namespace chatterino +#endif diff --git a/src/controllers/plugins/PluginPermission.hpp b/src/controllers/plugins/PluginPermission.hpp new file mode 100644 index 000000000..ffa728e7b --- /dev/null +++ b/src/controllers/plugins/PluginPermission.hpp @@ -0,0 +1,31 @@ +#pragma once +#ifdef CHATTERINO_HAVE_PLUGINS + +# include +# include + +# include + +namespace chatterino { + +struct PluginPermission { + explicit PluginPermission(const QJsonObject &obj); + + enum class Type { + FilesystemRead, + FilesystemWrite, + Network, + }; + Type type; + std::vector errors; + + bool isValid() const + { + return this->errors.empty(); + } + + QString toHtml() const; +}; + +} // namespace chatterino +#endif diff --git a/src/controllers/plugins/api/ChannelRef.cpp b/src/controllers/plugins/api/ChannelRef.cpp new file mode 100644 index 000000000..a57e60119 --- /dev/null +++ b/src/controllers/plugins/api/ChannelRef.cpp @@ -0,0 +1,397 @@ +#ifdef CHATTERINO_HAVE_PLUGINS +# include "controllers/plugins/api/ChannelRef.hpp" + +# include "common/Channel.hpp" +# include "controllers/commands/CommandController.hpp" +# include "controllers/plugins/LuaAPI.hpp" +# include "controllers/plugins/LuaUtilities.hpp" +# include "messages/MessageBuilder.hpp" +# include "providers/twitch/TwitchChannel.hpp" +# include "providers/twitch/TwitchIrcServer.hpp" + +extern "C" { +# include +# include +} + +# include +# include +# include + +namespace chatterino::lua::api { +// NOLINTBEGIN(*vararg) + +// NOLINTNEXTLINE(*-avoid-c-arrays) +static const luaL_Reg CHANNEL_REF_METHODS[] = { + {"is_valid", &ChannelRef::is_valid}, + {"get_name", &ChannelRef::get_name}, + {"get_type", &ChannelRef::get_type}, + {"get_display_name", &ChannelRef::get_display_name}, + {"send_message", &ChannelRef::send_message}, + {"add_system_message", &ChannelRef::add_system_message}, + {"is_twitch_channel", &ChannelRef::is_twitch_channel}, + + // Twitch + {"get_room_modes", &ChannelRef::get_room_modes}, + {"get_stream_status", &ChannelRef::get_stream_status}, + {"get_twitch_id", &ChannelRef::get_twitch_id}, + {"is_broadcaster", &ChannelRef::is_broadcaster}, + {"is_mod", &ChannelRef::is_mod}, + {"is_vip", &ChannelRef::is_vip}, + + // misc + {"__tostring", &ChannelRef::to_string}, + + // static + {"by_name", &ChannelRef::get_by_name}, + {"by_twitch_id", &ChannelRef::get_by_twitch_id}, + {nullptr, nullptr}, +}; + +void ChannelRef::createMetatable(lua_State *L) +{ + lua::StackGuard guard(L, 1); + + luaL_newmetatable(L, "c2.Channel"); + lua_pushstring(L, "__index"); + lua_pushvalue(L, -2); // clone metatable + lua_settable(L, -3); // metatable.__index = metatable + + // Generic IWeakResource stuff + lua_pushstring(L, "__gc"); + lua_pushcfunction( + L, (&WeakPtrUserData::destroy)); + lua_settable(L, -3); // metatable.__gc = WeakPtrUserData<...>::destroy + + luaL_setfuncs(L, CHANNEL_REF_METHODS, 0); +} + +ChannelPtr ChannelRef::getOrError(lua_State *L, bool expiredOk) +{ + if (lua_gettop(L) < 1) + { + luaL_error(L, "Called c2.Channel method without a channel object"); + return nullptr; + } + if (lua_isuserdata(L, lua_gettop(L)) == 0) + { + luaL_error( + L, "Called c2.Channel method with a non-userdata 'self' argument"); + return nullptr; + } + // luaL_checkudata is no-return if check fails + auto *checked = luaL_checkudata(L, lua_gettop(L), "c2.Channel"); + auto *data = + WeakPtrUserData::from(checked); + if (data == nullptr) + { + luaL_error(L, + "Called c2.Channel method with an invalid channel pointer"); + return nullptr; + } + lua_pop(L, 1); + if (data->target.expired()) + { + if (!expiredOk) + { + luaL_error(L, + "Usage of expired c2.Channel object. Underlying " + "resource was freed. Use Channel:is_valid() to check"); + } + return nullptr; + } + return data->target.lock(); +} + +std::shared_ptr ChannelRef::getTwitchOrError(lua_State *L) +{ + auto ref = ChannelRef::getOrError(L); + auto ptr = dynamic_pointer_cast(ref); + if (ptr == nullptr) + { + luaL_error(L, + "c2.Channel Twitch-only operation on non-Twitch channel."); + } + return ptr; +} + +int ChannelRef::is_valid(lua_State *L) +{ + ChannelPtr that = ChannelRef::getOrError(L, true); + lua::push(L, that != nullptr); + return 1; +} + +int ChannelRef::get_name(lua_State *L) +{ + ChannelPtr that = ChannelRef::getOrError(L); + lua::push(L, that->getName()); + return 1; +} + +int ChannelRef::get_type(lua_State *L) +{ + ChannelPtr that = ChannelRef::getOrError(L); + lua::push(L, that->getType()); + return 1; +} + +int ChannelRef::get_display_name(lua_State *L) +{ + ChannelPtr that = ChannelRef::getOrError(L); + lua::push(L, that->getDisplayName()); + return 1; +} + +int ChannelRef::send_message(lua_State *L) +{ + if (lua_gettop(L) != 2 && lua_gettop(L) != 3) + { + luaL_error(L, "Channel:send_message needs 1 or 2 arguments (message " + "text and optionally execute_commands flag)"); + return 0; + } + bool execcmds = false; + if (lua_gettop(L) == 3) + { + if (!lua::pop(L, &execcmds)) + { + luaL_error(L, "cannot get execute_commands (2nd argument of " + "Channel:send_message)"); + return 0; + } + } + + QString text; + if (!lua::pop(L, &text)) + { + luaL_error(L, "cannot get text (1st argument of Channel:send_message)"); + return 0; + } + + ChannelPtr that = ChannelRef::getOrError(L); + + text = text.replace('\n', ' '); + if (execcmds) + { + text = getApp()->getCommands()->execCommand(text, that, false); + } + that->sendMessage(text); + return 0; +} + +int ChannelRef::add_system_message(lua_State *L) +{ + // needs to account for the hidden self argument + if (lua_gettop(L) != 2) + { + luaL_error( + L, "Channel:add_system_message needs exactly 1 argument (message " + "text)"); + return 0; + } + + QString text; + if (!lua::pop(L, &text)) + { + luaL_error( + L, "cannot get text (1st argument of Channel:add_system_message)"); + return 0; + } + ChannelPtr that = ChannelRef::getOrError(L); + text = text.replace('\n', ' '); + that->addSystemMessage(text); + return 0; +} + +int ChannelRef::is_twitch_channel(lua_State *L) +{ + ChannelPtr that = ChannelRef::getOrError(L); + lua::push(L, that->isTwitchChannel()); + return 1; +} + +int ChannelRef::get_room_modes(lua_State *L) +{ + auto tc = ChannelRef::getTwitchOrError(L); + const auto m = tc->accessRoomModes(); + const auto modes = LuaRoomModes{ + .unique_chat = m->r9k, + .subscriber_only = m->submode, + .emotes_only = m->emoteOnly, + .follower_only = (m->followerOnly == -1) + ? std::nullopt + : std::optional(m->followerOnly), + .slow_mode = + (m->slowMode == 0) ? std::nullopt : std::optional(m->slowMode), + + }; + lua::push(L, modes); + return 1; +} + +int ChannelRef::get_stream_status(lua_State *L) +{ + auto tc = ChannelRef::getTwitchOrError(L); + const auto s = tc->accessStreamStatus(); + const auto status = LuaStreamStatus{ + .live = s->live, + .viewer_count = static_cast(s->viewerCount), + .uptime = s->uptimeSeconds, + .title = s->title, + .game_name = s->game, + .game_id = s->gameId, + }; + lua::push(L, status); + return 1; +} + +int ChannelRef::get_twitch_id(lua_State *L) +{ + auto tc = ChannelRef::getTwitchOrError(L); + lua::push(L, tc->roomId()); + return 1; +} + +int ChannelRef::is_broadcaster(lua_State *L) +{ + auto tc = ChannelRef::getTwitchOrError(L); + lua::push(L, tc->isBroadcaster()); + return 1; +} + +int ChannelRef::is_mod(lua_State *L) +{ + auto tc = ChannelRef::getTwitchOrError(L); + lua::push(L, tc->isMod()); + return 1; +} + +int ChannelRef::is_vip(lua_State *L) +{ + auto tc = ChannelRef::getTwitchOrError(L); + lua::push(L, tc->isVip()); + return 1; +} + +int ChannelRef::get_by_name(lua_State *L) +{ + if (lua_gettop(L) != 2) + { + luaL_error(L, "Channel.by_name needs exactly 2 arguments (channel " + "name and platform)"); + lua_pushnil(L); + return 1; + } + LPlatform platform{}; + if (!lua::pop(L, &platform)) + { + luaL_error(L, "cannot get platform (2nd argument of Channel.by_name, " + "expected a string)"); + lua_pushnil(L); + return 1; + } + QString name; + if (!lua::pop(L, &name)) + { + luaL_error(L, + "cannot get channel name (1st argument of Channel.by_name, " + "expected a string)"); + lua_pushnil(L); + return 1; + } + auto chn = getApp()->getTwitch()->getChannelOrEmpty(name); + lua::push(L, chn); + return 1; +} + +int ChannelRef::get_by_twitch_id(lua_State *L) +{ + if (lua_gettop(L) != 1) + { + luaL_error( + L, "Channel.by_twitch_id needs exactly 1 arguments (channel owner " + "id)"); + lua_pushnil(L); + return 1; + } + QString id; + if (!lua::pop(L, &id)) + { + luaL_error(L, + "cannot get channel name (1st argument of Channel.by_name, " + "expected a string)"); + lua_pushnil(L); + return 1; + } + auto chn = getApp()->getTwitch()->getChannelOrEmptyByID(id); + + lua::push(L, chn); + return 1; +} + +int ChannelRef::to_string(lua_State *L) +{ + ChannelPtr that = ChannelRef::getOrError(L, true); + if (that == nullptr) + { + lua_pushstring(L, ""); + return 1; + } + QString formated = QString("").arg(that->getName()); + lua::push(L, formated); + return 1; +} +} // namespace chatterino::lua::api +// NOLINTEND(*vararg) +// +namespace chatterino::lua { +StackIdx push(lua_State *L, const api::LuaRoomModes &modes) +{ + auto out = lua::pushEmptyTable(L, 6); +# define PUSH(field) \ + lua::push(L, modes.field); \ + lua_setfield(L, out, #field) + PUSH(unique_chat); + PUSH(subscriber_only); + PUSH(emotes_only); + PUSH(follower_only); + PUSH(slow_mode); +# undef PUSH + return out; +} + +StackIdx push(lua_State *L, const api::LuaStreamStatus &status) +{ + auto out = lua::pushEmptyTable(L, 6); +# define PUSH(field) \ + lua::push(L, status.field); \ + lua_setfield(L, out, #field) + PUSH(live); + PUSH(viewer_count); + PUSH(uptime); + PUSH(title); + PUSH(game_name); + PUSH(game_id); +# undef PUSH + return out; +} + +StackIdx push(lua_State *L, ChannelPtr chn) +{ + using namespace chatterino::lua::api; + + if (chn->isEmpty()) + { + lua_pushnil(L); + return lua_gettop(L); + } + WeakPtrUserData::create( + L, chn->weak_from_this()); + luaL_getmetatable(L, "c2.Channel"); + lua_setmetatable(L, -2); + return lua_gettop(L); +} + +} // namespace chatterino::lua +#endif diff --git a/src/controllers/plugins/api/ChannelRef.hpp b/src/controllers/plugins/api/ChannelRef.hpp new file mode 100644 index 000000000..32e1946ab --- /dev/null +++ b/src/controllers/plugins/api/ChannelRef.hpp @@ -0,0 +1,275 @@ +#pragma once +#ifdef CHATTERINO_HAVE_PLUGINS +# include "common/Channel.hpp" +# include "controllers/plugins/LuaUtilities.hpp" +# include "controllers/plugins/PluginController.hpp" +# include "providers/twitch/TwitchChannel.hpp" + +# include + +namespace chatterino::lua::api { +// NOLINTBEGIN(readability-identifier-naming) + +/** + * This enum describes a platform for the purpose of searching for a channel. + * Currently only Twitch is supported because identifying IRC channels is tricky. + * @exposeenum c2.Platform + */ +enum class LPlatform { + Twitch, + //IRC, +}; + +/** + * @lua@class c2.Channel + */ +struct ChannelRef { + static void createMetatable(lua_State *L); + friend class chatterino::PluginController; + + /** + * @brief Get the content of the top object on Lua stack, usually first argument to function as a ChannelPtr. + * If the object given is not a userdatum or the pointer inside that + * userdatum doesn't point to a Channel, a lua error is thrown. + * + * @param expiredOk Should an expired return nullptr instead of erroring + */ + static ChannelPtr getOrError(lua_State *L, bool expiredOk = false); + + /** + * @brief Casts the result of getOrError to std::shared_ptr + * if that fails thows a lua error. + */ + static std::shared_ptr getTwitchOrError(lua_State *L); + +public: + /** + * Returns true if the channel this object points to is valid. + * If the object expired, returns false + * If given a non-Channel object, it errors. + * + * @lua@return boolean success + * @exposed c2.Channel:is_valid + */ + static int is_valid(lua_State *L); + + /** + * Gets the channel's name. This is the lowercase login name. + * + * @lua@return string name + * @exposed c2.Channel:get_name + */ + static int get_name(lua_State *L); + + /** + * Gets the channel's type + * + * @lua@return c2.ChannelType + * @exposed c2.Channel:get_type + */ + static int get_type(lua_State *L); + + /** + * Get the channel owner's display name. This may contain non-lowercase ascii characters. + * + * @lua@return string name + * @exposed c2.Channel:get_display_name + */ + static int get_display_name(lua_State *L); + + /** + * Sends a message to the target channel. + * Note that this does not execute client-commands. + * + * @lua@param message string + * @lua@param execute_commands boolean Should commands be run on the text? + * @exposed c2.Channel:send_message + */ + static int send_message(lua_State *L); + + /** + * Adds a system message client-side + * + * @lua@param message string + * @exposed c2.Channel:add_system_message + */ + static int add_system_message(lua_State *L); + + /** + * Returns true for twitch channels. + * Compares the channel Type. Note that enum values aren't guaranteed, just + * that they are equal to the exposed enum. + * + * @lua@return boolean + * @exposed c2.Channel:is_twitch_channel + */ + static int is_twitch_channel(lua_State *L); + + /** + * Twitch Channel specific functions + */ + + /** + * Returns a copy of the channel mode settings (subscriber only, r9k etc.) + * + * @lua@return RoomModes + * @exposed c2.Channel:get_room_modes + */ + static int get_room_modes(lua_State *L); + + /** + * Returns a copy of the stream status. + * + * @lua@return StreamStatus + * @exposed c2.Channel:get_stream_status + */ + static int get_stream_status(lua_State *L); + + /** + * Returns the Twitch user ID of the owner of the channel. + * + * @lua@return string + * @exposed c2.Channel:get_twitch_id + */ + static int get_twitch_id(lua_State *L); + + /** + * Returns true if the channel is a Twitch channel and the user owns it + * + * @lua@return boolean + * @exposed c2.Channel:is_broadcaster + */ + static int is_broadcaster(lua_State *L); + + /** + * Returns true if the channel is a Twitch channel and the user is a moderator in the channel + * Returns false for broadcaster. + * + * @lua@return boolean + * @exposed c2.Channel:is_mod + */ + static int is_mod(lua_State *L); + + /** + * Returns true if the channel is a Twitch channel and the user is a VIP in the channel + * Returns false for broadcaster. + * + * @lua@return boolean + * @exposed c2.Channel:is_vip + */ + static int is_vip(lua_State *L); + + /** + * Misc + */ + + /** + * @lua@return string + * @exposed c2.Channel:__tostring + */ + static int to_string(lua_State *L); + + /** + * Static functions + */ + + /** + * Finds a channel by name. + * + * Misc channels are marked as Twitch: + * - /whispers + * - /mentions + * - /watching + * - /live + * - /automod + * + * @lua@param name string Which channel are you looking for? + * @lua@param platform c2.Platform Where to search for the channel? + * @lua@return c2.Channel? + * @exposed c2.Channel.by_name + */ + static int get_by_name(lua_State *L); + + /** + * Finds a channel by the Twitch user ID of its owner. + * + * @lua@param id string ID of the owner of the channel. + * @lua@return c2.Channel? + * @exposed c2.Channel.by_twitch_id + */ + static int get_by_twitch_id(lua_State *L); +}; + +// This is a copy of the TwitchChannel::RoomModes structure, except it uses nicer optionals +/** + * @lua@class RoomModes + */ +struct LuaRoomModes { + /** + * @lua@field unique_chat boolean You might know this as r9kbeta or robot9000. + */ + bool unique_chat = false; + + /** + * @lua@field subscriber_only boolean + */ + bool subscriber_only = false; + + /** + * @lua@field emotes_only boolean Whether or not text is allowed in messages. Note that "emotes" here only means Twitch emotes, not Unicode emoji, nor 3rd party text-based emotes + */ + bool emotes_only = false; + + /** + * @lua@field follower_only number? Time in minutes you need to follow to chat or nil. + */ + std::optional follower_only; + /** + * @lua@field slow_mode number? Time in seconds you need to wait before sending messages or nil. + */ + std::optional slow_mode; +}; + +/** + * @lua@class StreamStatus + */ +struct LuaStreamStatus { + /** + * @lua@field live boolean + */ + bool live = false; + + /** + * @lua@field viewer_count number + */ + int viewer_count = 0; + + /** + * @lua@field uptime number Seconds since the stream started. + */ + int uptime = 0; + + /** + * @lua@field title string Stream title or last stream title + */ + QString title; + + /** + * @lua@field game_name string + */ + QString game_name; + + /** + * @lua@field game_id string + */ + QString game_id; +}; + +// NOLINTEND(readability-identifier-naming) +} // namespace chatterino::lua::api +namespace chatterino::lua { +StackIdx push(lua_State *L, const api::LuaRoomModes &modes); +StackIdx push(lua_State *L, const api::LuaStreamStatus &status); +StackIdx push(lua_State *L, ChannelPtr chn); +} // namespace chatterino::lua +#endif diff --git a/src/controllers/plugins/api/HTTPRequest.cpp b/src/controllers/plugins/api/HTTPRequest.cpp new file mode 100644 index 000000000..eba2773ad --- /dev/null +++ b/src/controllers/plugins/api/HTTPRequest.cpp @@ -0,0 +1,451 @@ +#ifdef CHATTERINO_HAVE_PLUGINS +# include "controllers/plugins/api/HTTPRequest.hpp" + +# include "Application.hpp" +# include "common/network/NetworkCommon.hpp" +# include "common/network/NetworkRequest.hpp" +# include "common/network/NetworkResult.hpp" +# include "controllers/plugins/api/HTTPResponse.hpp" +# include "controllers/plugins/LuaAPI.hpp" +# include "controllers/plugins/LuaUtilities.hpp" +# include "util/DebugCount.hpp" + +extern "C" { +# include +# include +} +# include +# include + +# include +# include + +namespace chatterino::lua::api { +// NOLINTBEGIN(*vararg) +// NOLINTNEXTLINE(*-avoid-c-arrays) +static const luaL_Reg HTTP_REQUEST_METHODS[] = { + {"on_success", &HTTPRequest::on_success_wrap}, + {"on_error", &HTTPRequest::on_error_wrap}, + {"finally", &HTTPRequest::finally_wrap}, + + {"execute", &HTTPRequest::execute_wrap}, + {"set_timeout", &HTTPRequest::set_timeout_wrap}, + {"set_payload", &HTTPRequest::set_payload_wrap}, + {"set_header", &HTTPRequest::set_header_wrap}, + // static + {"create", &HTTPRequest::create}, + {nullptr, nullptr}, +}; + +std::shared_ptr HTTPRequest::getOrError(lua_State *L, + StackIdx where) +{ + if (lua_gettop(L) < 1) + { + // The nullptr is there just to appease the compiler, luaL_error is no return + luaL_error(L, "Called c2.HTTPRequest method without a request object"); + return nullptr; + } + if (lua_isuserdata(L, where) == 0) + { + luaL_error( + L, + "Called c2.HTTPRequest method with a non-userdata 'self' argument"); + return nullptr; + } + // luaL_checkudata is no-return if check fails + auto *checked = luaL_checkudata(L, where, "c2.HTTPRequest"); + auto *data = + SharedPtrUserData::from( + checked); + if (data == nullptr) + { + luaL_error(L, "Called c2.HTTPRequest method with an invalid pointer"); + return nullptr; + } + lua_remove(L, where); + if (data->target == nullptr) + { + luaL_error( + L, "Internal error: SharedPtrUserData::target was null. This is a Chatterino bug!"); + return nullptr; + } + if (data->target->done) + { + luaL_error(L, "This c2.HTTPRequest has already been executed!"); + return nullptr; + } + return data->target; +} + +void HTTPRequest::createMetatable(lua_State *L) +{ + lua::StackGuard guard(L, 1); + + luaL_newmetatable(L, "c2.HTTPRequest"); + lua_pushstring(L, "__index"); + lua_pushvalue(L, -2); // clone metatable + lua_settable(L, -3); // metatable.__index = metatable + + // Generic ISharedResource stuff + lua_pushstring(L, "__gc"); + lua_pushcfunction(L, (&SharedPtrUserData::destroy)); + lua_settable(L, -3); // metatable.__gc = SharedPtrUserData<...>::destroy + + luaL_setfuncs(L, HTTP_REQUEST_METHODS, 0); +} + +int HTTPRequest::on_success_wrap(lua_State *L) +{ + lua::StackGuard guard(L, -2); + auto ptr = HTTPRequest::getOrError(L, 1); + return ptr->on_success(L); +} + +int HTTPRequest::on_success(lua_State *L) +{ + auto top = lua_gettop(L); + if (top != 1) + { + return luaL_error( + L, "HTTPRequest:on_success needs 1 argument (a callback " + "that takes an HTTPResult and doesn't return anything)"); + } + if (!lua_isfunction(L, top)) + { + return luaL_error( + L, "HTTPRequest:on_success needs 1 argument (a callback " + "that takes an HTTPResult and doesn't return anything)"); + } + auto shared = this->pushPrivate(L); + lua_pushvalue(L, -2); + lua_setfield(L, shared, "success"); // this deletes the function copy + lua_pop(L, 2); // delete the table and function original + return 0; +} + +int HTTPRequest::on_error_wrap(lua_State *L) +{ + lua::StackGuard guard(L, -2); + auto ptr = HTTPRequest::getOrError(L, 1); + return ptr->on_error(L); +} + +int HTTPRequest::on_error(lua_State *L) +{ + auto top = lua_gettop(L); + if (top != 1) + { + return luaL_error( + L, "HTTPRequest:on_error needs 1 argument (a callback " + "that takes an HTTPResult and doesn't return anything)"); + } + if (!lua_isfunction(L, top)) + { + return luaL_error( + L, "HTTPRequest:on_error needs 1 argument (a callback " + "that takes an HTTPResult and doesn't return anything)"); + } + auto shared = this->pushPrivate(L); + lua_pushvalue(L, -2); + lua_setfield(L, shared, "error"); // this deletes the function copy + lua_pop(L, 2); // delete the table and function original + return 0; +} + +int HTTPRequest::set_timeout_wrap(lua_State *L) +{ + lua::StackGuard guard(L, -2); + auto ptr = HTTPRequest::getOrError(L, 1); + return ptr->set_timeout(L); +} + +int HTTPRequest::set_timeout(lua_State *L) +{ + auto top = lua_gettop(L); + if (top != 1) + { + return luaL_error( + L, "HTTPRequest:set_timeout needs 1 argument (a number of " + "milliseconds after which the request will time out)"); + } + + int temporary = -1; + if (!lua::pop(L, &temporary)) + { + return luaL_error( + L, "HTTPRequest:set_timeout failed to get timeout, expected a " + "positive integer"); + } + if (temporary <= 0) + { + return luaL_error( + L, "HTTPRequest:set_timeout failed to get timeout, expected a " + "positive integer"); + } + this->timeout_ = temporary; + return 0; +} + +int HTTPRequest::finally_wrap(lua_State *L) +{ + lua::StackGuard guard(L, -2); + auto ptr = HTTPRequest::getOrError(L, 1); + return ptr->finally(L); +} + +int HTTPRequest::finally(lua_State *L) +{ + auto top = lua_gettop(L); + if (top != 1) + { + return luaL_error(L, "HTTPRequest:finally needs 1 argument (a callback " + "that takes nothing and doesn't return anything)"); + } + if (!lua_isfunction(L, top)) + { + return luaL_error(L, "HTTPRequest:finally needs 1 argument (a callback " + "that takes nothing and doesn't return anything)"); + } + auto shared = this->pushPrivate(L); + lua_pushvalue(L, -2); + lua_setfield(L, shared, "finally"); // this deletes the function copy + lua_pop(L, 2); // delete the table and function original + return 0; +} + +int HTTPRequest::set_payload_wrap(lua_State *L) +{ + lua::StackGuard guard(L, -2); + auto ptr = HTTPRequest::getOrError(L, 1); + return ptr->set_payload(L); +} + +int HTTPRequest::set_payload(lua_State *L) +{ + auto top = lua_gettop(L); + if (top != 1) + { + return luaL_error( + L, "HTTPRequest:set_payload needs 1 argument (a string payload)"); + } + + std::string temporary; + if (!lua::pop(L, &temporary)) + { + return luaL_error( + L, "HTTPRequest:set_payload failed to get payload, expected a " + "string"); + } + this->req_ = + std::move(this->req_).payload(QByteArray::fromStdString(temporary)); + return 0; +} + +int HTTPRequest::set_header_wrap(lua_State *L) +{ + lua::StackGuard guard(L, -3); + auto ptr = HTTPRequest::getOrError(L, 1); + return ptr->set_header(L); +} + +int HTTPRequest::set_header(lua_State *L) +{ + auto top = lua_gettop(L); + if (top != 2) + { + return luaL_error( + L, "HTTPRequest:set_header needs 2 arguments (a header name " + "and a value)"); + } + + std::string value; + if (!lua::pop(L, &value)) + { + return luaL_error( + L, "cannot get value (2nd argument of HTTPRequest:set_header)"); + } + std::string name; + if (!lua::pop(L, &name)) + { + return luaL_error( + L, "cannot get name (1st argument of HTTPRequest:set_header)"); + } + this->req_ = std::move(this->req_) + .header(QByteArray::fromStdString(name), + QByteArray::fromStdString(value)); + return 0; +} + +int HTTPRequest::create(lua_State *L) +{ + lua::StackGuard guard(L, -1); + if (lua_gettop(L) != 2) + { + return luaL_error( + L, "HTTPRequest.create needs exactly 2 arguments (method " + "and url)"); + } + QString url; + if (!lua::pop(L, &url)) + { + return luaL_error(L, + "cannot get url (2nd argument of HTTPRequest.create, " + "expected a string)"); + } + auto parsedurl = QUrl(url); + if (!parsedurl.isValid()) + { + return luaL_error( + L, "cannot parse url (2nd argument of HTTPRequest.create, " + "got invalid url in argument)"); + } + NetworkRequestType method{}; + if (!lua::pop(L, &method)) + { + return luaL_error( + L, "cannot get method (1st argument of HTTPRequest.create, " + "expected a string)"); + } + auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L); + if (!pl->hasHTTPPermissionFor(parsedurl)) + { + return luaL_error( + L, "Plugin does not have permission to send HTTP requests " + "to this URL"); + } + NetworkRequest r(parsedurl, method); + lua::push( + L, std::make_shared(ConstructorAccessTag{}, std::move(r))); + return 1; +} + +int HTTPRequest::execute_wrap(lua_State *L) +{ + auto ptr = HTTPRequest::getOrError(L, 1); + return ptr->execute(L); +} + +int HTTPRequest::execute(lua_State *L) +{ + auto shared = this->shared_from_this(); + this->done = true; + std::move(this->req_) + .onSuccess([shared, L](const NetworkResult &res) { + lua::StackGuard guard(L); + auto *thread = lua_newthread(L); + + auto priv = shared->pushPrivate(thread); + lua_getfield(thread, priv, "success"); + auto cb = lua_gettop(thread); + if (lua_isfunction(thread, cb)) + { + lua::push(thread, std::make_shared(res)); + // one arg, no return, no msgh + lua_pcall(thread, 1, 0, 0); + } + else + { + lua_pop(thread, 1); // remove callback + } + lua_closethread(thread, nullptr); + lua_pop(L, 1); // remove thread from L + }) + .onError([shared, L](const NetworkResult &res) { + lua::StackGuard guard(L); + auto *thread = lua_newthread(L); + + auto priv = shared->pushPrivate(thread); + lua_getfield(thread, priv, "error"); + auto cb = lua_gettop(thread); + if (lua_isfunction(thread, cb)) + { + lua::push(thread, std::make_shared(res)); + // one arg, no return, no msgh + lua_pcall(thread, 1, 0, 0); + } + else + { + lua_pop(thread, 1); // remove callback + } + lua_closethread(thread, nullptr); + lua_pop(L, 1); // remove thread from L + }) + .finally([shared, L]() { + lua::StackGuard guard(L); + auto *thread = lua_newthread(L); + + auto priv = shared->pushPrivate(thread); + lua_getfield(thread, priv, "finally"); + auto cb = lua_gettop(thread); + if (lua_isfunction(thread, cb)) + { + // no args, no return, no msgh + lua_pcall(thread, 0, 0, 0); + } + else + { + lua_pop(thread, 1); // remove callback + } + // remove our private data + lua_pushnil(thread); + lua_setfield(thread, LUA_REGISTRYINDEX, + shared->privateKey.toStdString().c_str()); + lua_closethread(thread, nullptr); + lua_pop(L, 1); // remove thread from L + + // we removed our private table, forget the key for it + shared->privateKey = QString(); + }) + .timeout(this->timeout_) + .execute(); + return 0; +} + +HTTPRequest::HTTPRequest(HTTPRequest::ConstructorAccessTag /*ignored*/, + NetworkRequest req) + : req_(std::move(req)) +{ + DebugCount::increase("lua::api::HTTPRequest"); +} + +HTTPRequest::~HTTPRequest() +{ + DebugCount::decrease("lua::api::HTTPRequest"); + // We might leak a Lua function or two here if the request isn't executed + // but that's better than accessing a possibly invalid lua_State pointer. +} + +StackIdx HTTPRequest::pushPrivate(lua_State *L) +{ + if (this->privateKey.isEmpty()) + { + this->privateKey = QString("HTTPRequestPrivate%1") + .arg(QRandomGenerator::system()->generate()); + pushEmptyTable(L, 4); + lua_setfield(L, LUA_REGISTRYINDEX, + this->privateKey.toStdString().c_str()); + } + lua_getfield(L, LUA_REGISTRYINDEX, this->privateKey.toStdString().c_str()); + return lua_gettop(L); +} + +// NOLINTEND(*vararg) +} // namespace chatterino::lua::api + +namespace chatterino::lua { + +StackIdx push(lua_State *L, std::shared_ptr request) +{ + using namespace chatterino::lua::api; + + SharedPtrUserData::create( + L, std::move(request)); + luaL_getmetatable(L, "c2.HTTPRequest"); + lua_setmetatable(L, -2); + return lua_gettop(L); +} +} // namespace chatterino::lua +#endif diff --git a/src/controllers/plugins/api/HTTPRequest.hpp b/src/controllers/plugins/api/HTTPRequest.hpp new file mode 100644 index 000000000..955a3cd2d --- /dev/null +++ b/src/controllers/plugins/api/HTTPRequest.hpp @@ -0,0 +1,155 @@ +#pragma once +#ifdef CHATTERINO_HAVE_PLUGINS +# include "common/network/NetworkRequest.hpp" +# include "controllers/plugins/LuaUtilities.hpp" +# include "controllers/plugins/PluginController.hpp" + +# include + +namespace chatterino::lua::api { +// NOLINTBEGIN(readability-identifier-naming) + +/** + * @lua@alias HTTPCallback fun(result: HTTPResponse): nil + */ + +/** + * @lua@class HTTPRequest + */ +class HTTPRequest : public std::enable_shared_from_this +{ + // This type is private to prevent the accidental construction of HTTPRequest without a shared pointer + struct ConstructorAccessTag { + }; + +public: + HTTPRequest(HTTPRequest::ConstructorAccessTag, NetworkRequest req); + HTTPRequest(HTTPRequest &&other) = default; + HTTPRequest &operator=(HTTPRequest &&) = default; + HTTPRequest &operator=(HTTPRequest &) = delete; + HTTPRequest(const HTTPRequest &other) = delete; + ~HTTPRequest(); + +private: + NetworkRequest req_; + + static void createMetatable(lua_State *L); + friend class chatterino::PluginController; + + /** + * @brief Get the content of the top object on Lua stack, usually the first argument as an HTTPRequest + * + * If the object given is not a userdatum or the pointer inside that + * userdatum doesn't point to a HTTPRequest, a lua error is thrown. + * + * This function always returns a non-null pointer. + */ + static std::shared_ptr getOrError(lua_State *L, + StackIdx where = -1); + /** + * Pushes the private table onto the lua stack. + * + * This might create it if it doesn't exist. + */ + StackIdx pushPrivate(lua_State *L); + + // This is the key in the registry the private table it held at (if it exists) + // This might be a null QString if the request has already been executed or + // the table wasn't created yet. + QString privateKey; + int timeout_ = 10'000; + bool done = false; + +public: + // These functions are wrapped so data can be accessed more easily. When a call from Lua comes in: + // - the static wrapper function is called + // - it calls getOrError + // - and then the wrapped method + + /** + * Sets the success callback + * + * @lua@param callback HTTPCallback Function to call when the HTTP request succeeds + * @exposed HTTPRequest:on_success + */ + static int on_success_wrap(lua_State *L); + int on_success(lua_State *L); + + /** + * Sets the failure callback + * + * @lua@param callback HTTPCallback Function to call when the HTTP request fails or returns a non-ok status + * @exposed HTTPRequest:on_error + */ + static int on_error_wrap(lua_State *L); + int on_error(lua_State *L); + + /** + * Sets the finally callback + * + * @lua@param callback fun(): nil Function to call when the HTTP request finishes + * @exposed HTTPRequest:finally + */ + static int finally_wrap(lua_State *L); + int finally(lua_State *L); + + /** + * Sets the timeout + * + * @lua@param timeout integer How long in milliseconds until the times out + * @exposed HTTPRequest:set_timeout + */ + static int set_timeout_wrap(lua_State *L); + int set_timeout(lua_State *L); + + /** + * Sets the request payload + * + * @lua@param data string + * @exposed HTTPRequest:set_payload + */ + static int set_payload_wrap(lua_State *L); + int set_payload(lua_State *L); + + /** + * Sets a header in the request + * + * @lua@param name string + * @lua@param value string + * @exposed HTTPRequest:set_header + */ + static int set_header_wrap(lua_State *L); + int set_header(lua_State *L); + + /** + * Executes the HTTP request + * + * @exposed HTTPRequest:execute + */ + static int execute_wrap(lua_State *L); + int execute(lua_State *L); + + /** + * Static functions + */ + + /** + * Creates a new HTTPRequest + * + * @lua@param method HTTPMethod Method to use + * @lua@param url string Where to send the request to + * + * @lua@return HTTPRequest + * @exposed HTTPRequest.create + */ + static int create(lua_State *L); +}; + +// NOLINTEND(readability-identifier-naming) +} // namespace chatterino::lua::api + +namespace chatterino::lua { +StackIdx push(lua_State *L, std::shared_ptr request); +} // namespace chatterino::lua + +#endif diff --git a/src/controllers/plugins/api/HTTPResponse.cpp b/src/controllers/plugins/api/HTTPResponse.cpp new file mode 100644 index 000000000..f6d6ea1df --- /dev/null +++ b/src/controllers/plugins/api/HTTPResponse.cpp @@ -0,0 +1,144 @@ +#ifdef CHATTERINO_HAVE_PLUGINS +# include "controllers/plugins/api/HTTPResponse.hpp" + +# include "common/network/NetworkResult.hpp" +# include "controllers/plugins/LuaAPI.hpp" +# include "util/DebugCount.hpp" + +extern "C" { +# include +} +# include + +namespace chatterino::lua::api { +// NOLINTBEGIN(*vararg) +// NOLINTNEXTLINE(*-avoid-c-arrays) +static const luaL_Reg HTTP_RESPONSE_METHODS[] = { + {"data", &HTTPResponse::data_wrap}, + {"status", &HTTPResponse::status_wrap}, + {"error", &HTTPResponse::error_wrap}, + {nullptr, nullptr}, +}; + +void HTTPResponse::createMetatable(lua_State *L) +{ + lua::StackGuard guard(L, 1); + + luaL_newmetatable(L, "c2.HTTPResponse"); + lua_pushstring(L, "__index"); + lua_pushvalue(L, -2); // clone metatable + lua_settable(L, -3); // metatable.__index = metatable + + // Generic ISharedResource stuff + lua_pushstring(L, "__gc"); + lua_pushcfunction(L, (&SharedPtrUserData::destroy)); + lua_settable(L, -3); // metatable.__gc = SharedPtrUserData<...>::destroy + + luaL_setfuncs(L, HTTP_RESPONSE_METHODS, 0); +} + +std::shared_ptr HTTPResponse::getOrError(lua_State *L, + StackIdx where) +{ + if (lua_gettop(L) < 1) + { + // The nullptr is there just to appease the compiler, luaL_error is no return + luaL_error(L, "Called c2.HTTPResponse method without a request object"); + return nullptr; + } + if (lua_isuserdata(L, where) == 0) + { + luaL_error(L, "Called c2.HTTPResponse method with a non-userdata " + "'self' argument"); + return nullptr; + } + // luaL_checkudata is no-return if check fails + auto *checked = luaL_checkudata(L, where, "c2.HTTPResponse"); + auto *data = + SharedPtrUserData::from( + checked); + if (data == nullptr) + { + luaL_error(L, "Called c2.HTTPResponse method with an invalid pointer"); + return nullptr; + } + lua_remove(L, where); + if (data->target == nullptr) + { + luaL_error( + L, + "Internal error: SharedPtrUserData::target was null. This is a Chatterino bug!"); + return nullptr; + } + return data->target; +} + +HTTPResponse::HTTPResponse(NetworkResult res) + : result_(std::move(res)) +{ + DebugCount::increase("lua::api::HTTPResponse"); +} +HTTPResponse::~HTTPResponse() +{ + DebugCount::decrease("lua::api::HTTPResponse"); +} + +int HTTPResponse::data_wrap(lua_State *L) +{ + lua::StackGuard guard(L, 0); // 1 in, 1 out + auto ptr = HTTPResponse::getOrError(L, 1); + return ptr->data(L); +} + +int HTTPResponse::data(lua_State *L) +{ + lua::push(L, this->result_.getData().toStdString()); + return 1; +} + +int HTTPResponse::status_wrap(lua_State *L) +{ + lua::StackGuard guard(L, 0); // 1 in, 1 out + auto ptr = HTTPResponse::getOrError(L, 1); + return ptr->status(L); +} + +int HTTPResponse::status(lua_State *L) +{ + lua::push(L, this->result_.status()); + return 1; +} + +int HTTPResponse::error_wrap(lua_State *L) +{ + lua::StackGuard guard(L, 0); // 1 in, 1 out + auto ptr = HTTPResponse::getOrError(L, 1); + return ptr->error(L); +} + +int HTTPResponse::error(lua_State *L) +{ + lua::push(L, this->result_.formatError()); + return 1; +} + +// NOLINTEND(*vararg) +} // namespace chatterino::lua::api + +namespace chatterino::lua { +StackIdx push(lua_State *L, std::shared_ptr request) +{ + using namespace chatterino::lua::api; + + // Prepare table + SharedPtrUserData::create( + L, std::move(request)); + luaL_getmetatable(L, "c2.HTTPResponse"); + lua_setmetatable(L, -2); + + return lua_gettop(L); +} +} // namespace chatterino::lua +#endif diff --git a/src/controllers/plugins/api/HTTPResponse.hpp b/src/controllers/plugins/api/HTTPResponse.hpp new file mode 100644 index 000000000..205aae01e --- /dev/null +++ b/src/controllers/plugins/api/HTTPResponse.hpp @@ -0,0 +1,80 @@ +#pragma once +#ifdef CHATTERINO_HAVE_PLUGINS +# include "common/network/NetworkResult.hpp" +# include "controllers/plugins/LuaUtilities.hpp" + +# include +extern "C" { +# include +} + +namespace chatterino { +class PluginController; +} // namespace chatterino + +namespace chatterino::lua::api { +// NOLINTBEGIN(readability-identifier-naming) + +/** + * @lua@class HTTPResponse + */ +class HTTPResponse : public std::enable_shared_from_this +{ + NetworkResult result_; + +public: + HTTPResponse(NetworkResult res); + HTTPResponse(HTTPResponse &&other) = default; + HTTPResponse &operator=(HTTPResponse &&) = default; + HTTPResponse &operator=(HTTPResponse &) = delete; + HTTPResponse(const HTTPResponse &other) = delete; + ~HTTPResponse(); + +private: + static void createMetatable(lua_State *L); + friend class chatterino::PluginController; + + /** + * @brief Get the content of the top object on Lua stack, usually the first argument as an HTTPResponse + * + * If the object given is not a userdatum or the pointer inside that + * userdatum doesn't point to a HTTPResponse, a lua error is thrown. + * + * This function always returns a non-null pointer. + */ + static std::shared_ptr getOrError(lua_State *L, + StackIdx where = -1); + +public: + /** + * Returns the data. This is not guaranteed to be encoded using any + * particular encoding scheme. It's just the bytes the server returned. + * + * @exposed HTTPResponse:data + */ + static int data_wrap(lua_State *L); + int data(lua_State *L); + + /** + * Returns the status code. + * + * @exposed HTTPResponse:status + */ + static int status_wrap(lua_State *L); + int status(lua_State *L); + + /** + * A somewhat human readable description of an error if such happened + * @exposed HTTPResponse:error + */ + + static int error_wrap(lua_State *L); + int error(lua_State *L); +}; + +// NOLINTEND(readability-identifier-naming) +} // namespace chatterino::lua::api +namespace chatterino::lua { +StackIdx push(lua_State *L, std::shared_ptr request); +} // namespace chatterino::lua +#endif diff --git a/src/controllers/plugins/api/IOWrapper.cpp b/src/controllers/plugins/api/IOWrapper.cpp new file mode 100644 index 000000000..f6a58a0bb --- /dev/null +++ b/src/controllers/plugins/api/IOWrapper.cpp @@ -0,0 +1,375 @@ +#ifdef CHATTERINO_HAVE_PLUGINS +# include "controllers/plugins/api/IOWrapper.hpp" + +# include "Application.hpp" +# include "controllers/plugins/LuaUtilities.hpp" +# include "controllers/plugins/PluginController.hpp" + +extern "C" { +# include +# include +} + +# include + +namespace chatterino::lua::api { + +// Note: Parsing and then serializing the mode ensures we understand it before +// passing it to Lua + +struct LuaFileMode { + char major = 'r'; // 'r'|'w'|'a' + bool update{}; // '+' + bool binary{}; // 'b' + QString error; + + LuaFileMode() = default; + + LuaFileMode(const QString &smode) + { + if (smode.isEmpty()) + { + this->error = "Empty mode given, use one matching /[rwa][+]?b?/."; + return; + } + auto major = smode.at(0); + if (major != 'r' && major != 'w' && major != 'a') + { + this->error = "Invalid mode, use one matching /[rwa][+]?b?/. " + "Parsing failed at 1st character."; + return; + } + this->major = major.toLatin1(); + if (smode.length() > 1) + { + auto plusOrB = smode.at(1); + if (plusOrB == '+') + { + this->update = true; + } + else if (plusOrB == 'b') + { + this->binary = true; + } + else + { + this->error = "Invalid mode, use one matching /[rwa][+]?b?/. " + "Parsing failed at 2nd character."; + return; + } + } + if (smode.length() > 2) + { + auto maybeB = smode.at(2); + if (maybeB == 'b') + { + this->binary = true; + } + else + { + this->error = "Invalid mode, use one matching /[rwa][+]?b?/. " + "Parsing failed at 3rd character."; + return; + } + } + } + + QString toString() const + { + assert(this->major == 'r' || this->major == 'w' || this->major == 'a'); + QString out; + out += this->major; + if (this->update) + { + out += '+'; + } + if (this->binary) + { + out += 'b'; + } + return out; + } +}; + +int ioError(lua_State *L, const QString &value, int errnoequiv) +{ + lua_pushnil(L); + lua::push(L, value); + lua::push(L, errnoequiv); + return 3; +} + +// NOLINTBEGIN(*vararg) +int io_open(lua_State *L) +{ + auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L); + if (pl == nullptr) + { + luaL_error(L, "internal error: no plugin"); + return 0; + } + LuaFileMode mode; + if (lua_gettop(L) == 2) + { + // we have a mode + QString smode; + if (!lua::pop(L, &smode)) + { + return luaL_error( + L, + "io.open mode (2nd argument) must be a string or not present"); + } + mode = LuaFileMode(smode); + if (!mode.error.isEmpty()) + { + return luaL_error(L, mode.error.toStdString().c_str()); + } + } + QString filename; + if (!lua::pop(L, &filename)) + { + return luaL_error(L, + "io.open filename (1st argument) must be a string"); + } + QFileInfo file(pl->dataDirectory().filePath(filename)); + auto abs = file.absoluteFilePath(); + qCDebug(chatterinoLua) << "[" << pl->id << ":" << pl->meta.name + << "] Plugin is opening file at " << abs + << " with mode " << mode.toString(); + bool ok = pl->hasFSPermissionFor( + mode.update || mode.major == 'w' || mode.major == 'a', abs); + if (!ok) + { + return ioError(L, + "Plugin does not have permissions to access given file.", + EACCES); + } + lua_getfield(L, LUA_REGISTRYINDEX, REG_REAL_IO_NAME); + lua_getfield(L, -1, "open"); + lua_remove(L, -2); // remove LUA_REGISTRYINDEX[REAL_IO_NAME] + lua::push(L, abs); + lua::push(L, mode.toString()); + lua_call(L, 2, 3); + return 3; +} + +int io_lines(lua_State *L) +{ + auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L); + if (pl == nullptr) + { + luaL_error(L, "internal error: no plugin"); + return 0; + } + if (lua_gettop(L) == 0) + { + // io.lines() case, just call realio.lines + lua_getfield(L, LUA_REGISTRYINDEX, REG_REAL_IO_NAME); + lua_getfield(L, -1, "lines"); + lua_remove(L, -2); // remove LUA_REGISTRYINDEX[REAL_IO_NAME] + lua_call(L, 0, 1); + return 1; + } + QString filename; + if (!lua::pop(L, &filename)) + { + return luaL_error( + L, + "io.lines filename (1st argument) must be a string or not present"); + } + QFileInfo file(pl->dataDirectory().filePath(filename)); + auto abs = file.absoluteFilePath(); + qCDebug(chatterinoLua) << "[" << pl->id << ":" << pl->meta.name + << "] Plugin is opening file at " << abs + << " for reading lines"; + bool ok = pl->hasFSPermissionFor(false, abs); + if (!ok) + { + return ioError(L, + "Plugin does not have permissions to access given file.", + EACCES); + } + // Our stack looks like this: + // - {...}[1] + // - {...}[2] + // ... + // We want: + // - REG[REG_REAL_IO_NAME].lines + // - absolute file path + // - {...}[1] + // - {...}[2] + // ... + + lua_getfield(L, LUA_REGISTRYINDEX, REG_REAL_IO_NAME); + lua_getfield(L, -1, "lines"); + lua_remove(L, -2); // remove LUA_REGISTRYINDEX[REAL_IO_NAME] + lua_insert(L, 1); // move function to start of stack + lua::push(L, abs); + lua_insert(L, 2); // move file name just after the function + lua_call(L, lua_gettop(L) - 1, LUA_MULTRET); + return lua_gettop(L); +} + +namespace { + + // This is the code for both io.input and io.output + int globalFileCommon(lua_State *L, bool output) + { + auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L); + if (pl == nullptr) + { + luaL_error(L, "internal error: no plugin"); + return 0; + } + // Three signature cases: + // io.input() + // io.input(file) + // io.input(name) + if (lua_gettop(L) == 0) + { + // We have no arguments, call realio.input() + lua_getfield(L, LUA_REGISTRYINDEX, REG_REAL_IO_NAME); + if (output) + { + lua_getfield(L, -1, "output"); + } + else + { + lua_getfield(L, -1, "input"); + } + lua_remove(L, -2); // remove LUA_REGISTRYINDEX[REAL_IO_NAME] + lua_call(L, 0, 1); + return 1; + } + if (lua_gettop(L) != 1) + { + return luaL_error(L, "Too many arguments given to io.input()."); + } + // Now check if we have a file or name + auto *p = luaL_testudata(L, 1, LUA_FILEHANDLE); + if (p == nullptr) + { + // this is not a file handle, send it to open + luaL_getsubtable(L, LUA_REGISTRYINDEX, REG_C2_IO_NAME); + lua_getfield(L, -1, "open"); + lua_remove(L, -2); // remove io + + lua_pushvalue(L, 1); // dupe arg + if (output) + { + lua_pushstring(L, "w"); + } + else + { + lua_pushstring(L, "r"); + } + lua_call(L, 2, 1); // call ourio.open(arg1, 'r'|'w') + // if this isn't a string ourio.open errors + + // this leaves us with: + // 1. arg + // 2. new_file + lua_remove(L, 1); // remove arg, replacing it with new_file + } + + // file handle, pass it off to realio.input + lua_getfield(L, LUA_REGISTRYINDEX, REG_REAL_IO_NAME); + if (output) + { + lua_getfield(L, -1, "output"); + } + else + { + lua_getfield(L, -1, "input"); + } + lua_remove(L, -2); // remove LUA_REGISTRYINDEX[REAL_IO_NAME] + lua_pushvalue(L, 1); // duplicate arg + lua_call(L, 1, 1); + return 1; + } + +} // namespace + +int io_input(lua_State *L) +{ + return globalFileCommon(L, false); +} + +int io_output(lua_State *L) +{ + return globalFileCommon(L, true); +} + +int io_close(lua_State *L) +{ + if (lua_gettop(L) > 1) + { + return luaL_error( + L, "Too many arguments for io.close. Expected one or zero."); + } + if (lua_gettop(L) == 0) + { + lua_getfield(L, LUA_REGISTRYINDEX, "_IO_output"); + } + lua_getfield(L, -1, "close"); + lua_pushvalue(L, -2); + lua_call(L, 1, 0); + return 0; +} + +int io_flush(lua_State *L) +{ + if (lua_gettop(L) > 1) + { + return luaL_error( + L, "Too many arguments for io.flush. Expected one or zero."); + } + lua_getfield(L, LUA_REGISTRYINDEX, "_IO_output"); + lua_getfield(L, -1, "flush"); + lua_pushvalue(L, -2); + lua_call(L, 1, 0); + return 0; +} + +int io_read(lua_State *L) +{ + if (lua_gettop(L) > 1) + { + return luaL_error( + L, "Too many arguments for io.read. Expected one or zero."); + } + lua_getfield(L, LUA_REGISTRYINDEX, "_IO_input"); + lua_getfield(L, -1, "read"); + lua_insert(L, 1); + lua_insert(L, 2); + lua_call(L, lua_gettop(L) - 1, 1); + return 1; +} + +int io_write(lua_State *L) +{ + lua_getfield(L, LUA_REGISTRYINDEX, "_IO_output"); + lua_getfield(L, -1, "write"); + lua_insert(L, 1); + lua_insert(L, 2); + // (input) + // (input).read + // args + lua_call(L, lua_gettop(L) - 1, 1); + return 1; +} + +int io_popen(lua_State *L) +{ + return luaL_error(L, "io.popen: This function is a stub!"); +} + +int io_tmpfile(lua_State *L) +{ + return luaL_error(L, "io.tmpfile: This function is a stub!"); +} + +// NOLINTEND(*vararg) + +} // namespace chatterino::lua::api +#endif diff --git a/src/controllers/plugins/api/IOWrapper.hpp b/src/controllers/plugins/api/IOWrapper.hpp new file mode 100644 index 000000000..24ee2801e --- /dev/null +++ b/src/controllers/plugins/api/IOWrapper.hpp @@ -0,0 +1,98 @@ +#pragma once +#ifdef CHATTERINO_HAVE_PLUGINS + +struct lua_State; + +namespace chatterino::lua::api { +// NOLINTBEGIN(readability-identifier-naming) +// These functions are exposed as `_G.io`, they are wrappers for native Lua functionality. + +const char *const REG_REAL_IO_NAME = "real_lua_io_lib"; +const char *const REG_C2_IO_NAME = "c2io"; + +/** + * Opens a file. + * If given a relative path, it will be relative to + * c2datadir/Plugins/pluginDir/data/ + * See https://www.lua.org/manual/5.4/manual.html#pdf-io.open + * + * @lua@param filename string + * @lua@param mode nil|"r"|"w"|"a"|"r+"|"w+"|"a+" + * @exposed io.open + */ +int io_open(lua_State *L); + +/** + * Equivalent to io.input():lines("l") or a specific iterator over given file + * If given a relative path, it will be relative to + * c2datadir/Plugins/pluginDir/data/ + * See https://www.lua.org/manual/5.4/manual.html#pdf-io.lines + * + * @lua@param filename nil|string + * @lua@param ... + * @exposed io.lines + */ +int io_lines(lua_State *L); + +/** + * Opens a file and sets it as default input or if given no arguments returns the default input. + * See https://www.lua.org/manual/5.4/manual.html#pdf-io.input + * + * @lua@param fileorname nil|string|FILE* + * @lua@return nil|FILE* + * @exposed io.input + */ +int io_input(lua_State *L); + +/** + * Opens a file and sets it as default output or if given no arguments returns the default output + * See https://www.lua.org/manual/5.4/manual.html#pdf-io.output + * + * @lua@param fileorname nil|string|FILE* + * @lua@return nil|FILE* + * @exposed io.output + */ +int io_output(lua_State *L); + +/** + * Closes given file or io.output() if not given. + * See https://www.lua.org/manual/5.4/manual.html#pdf-io.close + * + * @lua@param nil|FILE* + * @exposed io.close + */ +int io_close(lua_State *L); + +/** + * Flushes the buffer for given file or io.output() if not given. + * See https://www.lua.org/manual/5.4/manual.html#pdf-io.flush + * + * @lua@param nil|FILE* + * @exposed io.flush + */ +int io_flush(lua_State *L); + +/** + * Reads some data from the default input file + * See https://www.lua.org/manual/5.4/manual.html#pdf-io.read + * + * @lua@param nil|string + * @exposed io.read + */ +int io_read(lua_State *L); + +/** + * Writes some data to the default output file + * See https://www.lua.org/manual/5.4/manual.html#pdf-io.write + * + * @lua@param nil|string + * @exposed io.write + */ +int io_write(lua_State *L); + +int io_popen(lua_State *L); +int io_tmpfile(lua_State *L); + +// NOLINTEND(readability-identifier-naming) +} // namespace chatterino::lua::api +#endif diff --git a/src/controllers/sound/ISoundController.hpp b/src/controllers/sound/ISoundController.hpp new file mode 100644 index 000000000..10e8c6c73 --- /dev/null +++ b/src/controllers/sound/ISoundController.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include + +namespace chatterino { + +enum class SoundBackend { + Miniaudio, + Null, +}; + +/** + * @brief Handles sound loading & playback + **/ +class ISoundController +{ +public: + ISoundController() = default; + virtual ~ISoundController() = default; + ISoundController(const ISoundController &) = delete; + ISoundController(ISoundController &&) = delete; + ISoundController &operator=(const ISoundController &) = delete; + ISoundController &operator=(ISoundController &&) = delete; + + // Play a sound from the given url + // If the url points to something that isn't a local file, it will play + // the default sound initialized in the initialize method + // + // This function should not block + virtual void play(const QUrl &sound) = 0; +}; + +} // namespace chatterino diff --git a/src/controllers/sound/MiniaudioBackend.cpp b/src/controllers/sound/MiniaudioBackend.cpp new file mode 100644 index 000000000..3a88bbf30 --- /dev/null +++ b/src/controllers/sound/MiniaudioBackend.cpp @@ -0,0 +1,295 @@ +#include "controllers/sound/MiniaudioBackend.hpp" + +#include "Application.hpp" +#include "common/QLogging.hpp" +#include "debug/Benchmark.hpp" +#include "singletons/Paths.hpp" +#include "singletons/Settings.hpp" +#include "singletons/WindowManager.hpp" +#include "util/RenameThread.hpp" +#include "widgets/Window.hpp" + +#include + +#define MINIAUDIO_IMPLEMENTATION +#include +#include + +#include +#include + +namespace { + +using namespace chatterino; + +// The duration after which a sound is played we should try to stop the sound engine, hopefully +// returning the handle to idle letting the computer or monitors sleep +constexpr const auto STOP_AFTER_DURATION = std::chrono::seconds(30); + +void miniaudioLogCallback(void *userData, ma_uint32 level, const char *pMessage) +{ + (void)userData; + + QString message{pMessage}; + + switch (level) + { + case MA_LOG_LEVEL_DEBUG: { + qCDebug(chatterinoSound).noquote() + << "ma debug: " << message.trimmed(); + } + break; + case MA_LOG_LEVEL_INFO: { + qCDebug(chatterinoSound).noquote() + << "ma info: " << message.trimmed(); + } + break; + case MA_LOG_LEVEL_WARNING: { + qCWarning(chatterinoSound).noquote() + << "ma warning:" << message.trimmed(); + } + break; + case MA_LOG_LEVEL_ERROR: { + qCWarning(chatterinoSound).noquote() + << "ma error: " << message.trimmed(); + } + break; + default: { + qCWarning(chatterinoSound).noquote() + << "ma unknown:" << message.trimmed(); + } + break; + } +} + +} // namespace + +namespace chatterino { + +// NUM_SOUNDS specifies how many simultaneous default ping sounds & decoders to create +constexpr const auto NUM_SOUNDS = 4; + +MiniaudioBackend::MiniaudioBackend() + : context(std::make_unique()) + , engine(std::make_unique()) + , workGuard(boost::asio::make_work_guard(this->ioContext)) + , sleepTimer(this->ioContext) +{ + qCInfo(chatterinoSound) << "Initializing miniaudio sound backend"; + + boost::asio::post(this->ioContext, [this] { + ma_result result{}; + + // We are leaking this log object on purpose + auto *logger = new ma_log; + + result = ma_log_init(nullptr, logger); + if (result != MA_SUCCESS) + { + qCWarning(chatterinoSound) + << "Error initializing logger:" << result; + return; + } + + result = ma_log_register_callback( + logger, ma_log_callback_init(miniaudioLogCallback, nullptr)); + if (result != MA_SUCCESS) + { + qCWarning(chatterinoSound) + << "Error registering logger callback:" << result; + return; + } + + auto contextConfig = ma_context_config_init(); + contextConfig.pLog = logger; + + /// Initialize context + result = + ma_context_init(nullptr, 0, &contextConfig, this->context.get()); + if (result != MA_SUCCESS) + { + qCWarning(chatterinoSound) + << "Error initializing context:" << result; + return; + } + + /// Load default sound + QFile defaultPingFile(":/sounds/ping2.wav"); + if (!defaultPingFile.open(QIODevice::ReadOnly)) + { + qCWarning(chatterinoSound) << "Error loading default ping sound"; + return; + } + this->defaultPingData = defaultPingFile.readAll(); + + /// Initialize engine + auto engineConfig = ma_engine_config_init(); + engineConfig.pContext = this->context.get(); + engineConfig.noAutoStart = MA_TRUE; + + result = ma_engine_init(&engineConfig, this->engine.get()); + if (result != MA_SUCCESS) + { + qCWarning(chatterinoSound) + << "Error initializing engine:" << result; + return; + } + + /// Initialize default ping sounds + { + // TODO: Can we optimize this? + BenchmarkGuard b("init sounds"); + + ma_uint32 soundFlags = 0; + // Decode the sound during loading instead of during playback + soundFlags |= MA_SOUND_FLAG_DECODE; + // Disable pitch control (we don't use it, so this saves some performance) + soundFlags |= MA_SOUND_FLAG_NO_PITCH; + // Disable spatialization control, this brings the volume up to "normal levels" + soundFlags |= MA_SOUND_FLAG_NO_SPATIALIZATION; + + auto decoderConfig = + ma_decoder_config_init(ma_format_f32, 0, 48000); + // This must match the encoding format of our default ping sound + decoderConfig.encodingFormat = ma_encoding_format_wav; + + for (auto i = 0; i < NUM_SOUNDS; ++i) + { + auto dec = std::make_unique(); + auto snd = std::make_unique(); + + result = ma_decoder_init_memory( + (void *)this->defaultPingData.data(), + this->defaultPingData.size() * sizeof(char), &decoderConfig, + dec.get()); + if (result != MA_SUCCESS) + { + qCWarning(chatterinoSound) << "Error initializing default " + "ping decoder from memory:" + << result; + return; + } + + result = ma_sound_init_from_data_source(this->engine.get(), + dec.get(), soundFlags, + nullptr, snd.get()); + if (result != MA_SUCCESS) + { + qCWarning(chatterinoSound) + << "Error initializing default sound from data source:" + << result; + return; + } + + this->defaultPingDecoders.emplace_back(std::move(dec)); + this->defaultPingSounds.emplace_back(std::move(snd)); + } + } + + qCInfo(chatterinoSound) << "miniaudio sound system initialized"; + + this->initialized = true; + }); + + this->audioThread = std::make_unique([this] { + this->ioContext.run(); + }); + renameThread(*this->audioThread, "C2Miniaudio"); +} + +MiniaudioBackend::~MiniaudioBackend() +{ + // NOTE: This destructor is never called because the `runGui` function calls _exit before that happens + // I have manually called the destructor prior to _exit being called to ensure this logic is sound + + boost::asio::post(this->ioContext, [this] { + for (const auto &snd : this->defaultPingSounds) + { + ma_sound_uninit(snd.get()); + } + for (const auto &dec : this->defaultPingDecoders) + { + ma_decoder_uninit(dec.get()); + } + + ma_engine_uninit(this->engine.get()); + ma_context_uninit(this->context.get()); + + this->workGuard.reset(); + }); + + if (this->audioThread->joinable()) + { + this->audioThread->join(); + } + else + { + qCWarning(chatterinoSound) << "Audio thread not joinable"; + } +} + +void MiniaudioBackend::play(const QUrl &sound) +{ + boost::asio::post(this->ioContext, [this, sound] { + static size_t i = 0; + + this->tgPlay.guard(); + + if (!this->initialized) + { + qCWarning(chatterinoSound) << "Can't play sound, sound controller " + "didn't initialize correctly"; + return; + } + + auto result = ma_engine_start(this->engine.get()); + if (result != MA_SUCCESS) + { + qCWarning(chatterinoSound) << "Error starting engine " << result; + return; + } + + if (sound.isLocalFile()) + { + auto soundPath = sound.toLocalFile(); + result = ma_engine_play_sound(this->engine.get(), + qPrintable(soundPath), nullptr); + if (result != MA_SUCCESS) + { + qCWarning(chatterinoSound) << "Failed to play sound" << sound + << soundPath << ":" << result; + } + + return; + } + + // Play default sound, loaded from our resources in the constructor + auto &snd = this->defaultPingSounds[++i % NUM_SOUNDS]; + ma_sound_seek_to_pcm_frame(snd.get(), 0); + result = ma_sound_start(snd.get()); + if (result != MA_SUCCESS) + { + qCWarning(chatterinoSound) + << "Failed to play default ping" << result; + } + + this->sleepTimer.expires_from_now(STOP_AFTER_DURATION); + this->sleepTimer.async_wait([this](const auto &ec) { + if (ec) + { + // Timer was most likely cancelled + return; + } + + auto result = ma_engine_stop(this->engine.get()); + if (result != MA_SUCCESS) + { + qCWarning(chatterinoSound) + << "Error stopping miniaudio engine " << result; + return; + } + }); + }); +} + +} // namespace chatterino diff --git a/src/controllers/sound/MiniaudioBackend.hpp b/src/controllers/sound/MiniaudioBackend.hpp new file mode 100644 index 000000000..b8f359c3c --- /dev/null +++ b/src/controllers/sound/MiniaudioBackend.hpp @@ -0,0 +1,69 @@ +#pragma once + +#include "controllers/sound/ISoundController.hpp" +#include "util/ThreadGuard.hpp" + +#include +#include +#include +#include + +#include +#include + +struct ma_engine; +struct ma_device; +struct ma_resource_manager; +struct ma_context; +struct ma_sound; +struct ma_decoder; + +namespace chatterino { + +/** + * @brief Handles sound loading & playback + **/ +class MiniaudioBackend : public ISoundController +{ +public: + MiniaudioBackend(); + ~MiniaudioBackend() override; + + // Play a sound from the given url + // If the url points to something that isn't a local file, it will play + // the default sound initialized in the initialize method + void play(const QUrl &sound) final; + +private: + // Used for selecting & initializing an appropriate sound backend + std::unique_ptr context; + // The engine is a high-level API for playing sounds from paths in a simple & efficient-enough manner + std::unique_ptr engine; + + // Stores the data of our default ping sounds + QByteArray defaultPingData; + // Stores N decoders for simultaneous default ping playback. + // We can't use the engine API for this as this requires direct access to a custom data_source + std::vector> defaultPingDecoders; + // Stores N sounds for simultaneous default ping playback + // We can't use the engine API for this as this requires direct access to a custom data_source + std::vector> defaultPingSounds; + + // Thread guard for the play method + // Ensures play is only ever called from the same thread + ThreadGuard tgPlay; + + std::chrono::system_clock::time_point lastSoundPlay; + + boost::asio::io_context ioContext{1}; + boost::asio::executor_work_guard + workGuard; + std::unique_ptr audioThread; + boost::asio::steady_timer sleepTimer; + + bool initialized{false}; + + friend class Application; +}; + +} // namespace chatterino diff --git a/src/controllers/sound/NullBackend.cpp b/src/controllers/sound/NullBackend.cpp new file mode 100644 index 000000000..7f1797e2e --- /dev/null +++ b/src/controllers/sound/NullBackend.cpp @@ -0,0 +1,18 @@ +#include "controllers/sound/NullBackend.hpp" + +#include "common/QLogging.hpp" + +namespace chatterino { + +NullBackend::NullBackend() +{ + qCInfo(chatterinoSound) << "Initializing null sound backend"; +} + +void NullBackend::play(const QUrl &sound) +{ + // Do nothing + qCDebug(chatterinoSound) << "null backend asked to play" << sound; +} + +} // namespace chatterino diff --git a/src/controllers/sound/NullBackend.hpp b/src/controllers/sound/NullBackend.hpp new file mode 100644 index 000000000..421deff07 --- /dev/null +++ b/src/controllers/sound/NullBackend.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include "controllers/sound/ISoundController.hpp" + +namespace chatterino { + +/** + * @brief This sound backend does nothing + **/ +class NullBackend final : public ISoundController +{ +public: + NullBackend(); + ~NullBackend() override = default; + NullBackend(const NullBackend &) = delete; + NullBackend(NullBackend &&) = delete; + NullBackend &operator=(const NullBackend &) = delete; + NullBackend &operator=(NullBackend &&) = delete; + + // Play a sound from the given url + // If the url points to something that isn't a local file, it will play + // the default sound initialized in the initialize method + void play(const QUrl &sound) final; +}; + +} // namespace chatterino diff --git a/src/controllers/twitch/LiveController.cpp b/src/controllers/twitch/LiveController.cpp new file mode 100644 index 000000000..d574bf867 --- /dev/null +++ b/src/controllers/twitch/LiveController.cpp @@ -0,0 +1,192 @@ +#include "controllers/twitch/LiveController.hpp" + +#include "common/QLogging.hpp" +#include "providers/twitch/api/Helix.hpp" +#include "providers/twitch/TwitchChannel.hpp" +#include "util/Helpers.hpp" + +#include + +namespace { + +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +const auto &LOG = chatterinoTwitchLiveController; + +} // namespace + +namespace chatterino { + +TwitchLiveController::TwitchLiveController() +{ + QObject::connect(&this->refreshTimer, &QTimer::timeout, [this] { + this->request(); + }); + this->refreshTimer.start(TwitchLiveController::REFRESH_INTERVAL); + + QObject::connect(&this->immediateRequestTimer, &QTimer::timeout, [this] { + QStringList channelIDs; + + { + std::unique_lock immediateRequestsLock( + this->immediateRequestsMutex); + for (const auto &channelID : this->immediateRequests) + { + channelIDs.append(channelID); + } + this->immediateRequests.clear(); + } + + if (channelIDs.isEmpty()) + { + return; + } + + this->request(channelIDs); + }); + this->immediateRequestTimer.start( + TwitchLiveController::IMMEDIATE_REQUEST_INTERVAL); +} + +void TwitchLiveController::add(const std::shared_ptr &newChannel) +{ + assert(newChannel != nullptr); + + const auto channelID = newChannel->roomId(); + assert(!channelID.isEmpty()); + + { + std::unique_lock lock(this->channelsMutex); + this->channels[channelID] = {.ptr = newChannel, .wasChecked = false}; + } + + { + std::unique_lock immediateRequestsLock(this->immediateRequestsMutex); + this->immediateRequests.emplace(channelID); + } +} + +void TwitchLiveController::request(std::optional optChannelIDs) +{ + QStringList channelIDs; + + if (optChannelIDs) + { + channelIDs = *optChannelIDs; + } + else + { + std::shared_lock lock(this->channelsMutex); + + for (const auto &channelList : this->channels) + { + channelIDs.append(channelList.first); + } + } + + if (channelIDs.isEmpty()) + { + return; + } + + auto batches = + splitListIntoBatches(channelIDs, TwitchLiveController::BATCH_SIZE); + + qCDebug(LOG) << "Make" << batches.size() << "requests"; + + for (const auto &batch : batches) + { + // TODO: Explore making this concurrent + getHelix()->fetchStreams( + batch, {}, + [this, batch{batch}](const auto &streams) { + std::unordered_map> results; + + for (const auto &channelID : batch) + { + results[channelID] = std::nullopt; + } + + for (const auto &stream : streams) + { + results[stream.userId] = stream; + } + + QStringList deadChannels; + + { + std::shared_lock lock(this->channelsMutex); + for (const auto &result : results) + { + auto it = this->channels.find(result.first); + if (it != channels.end()) + { + if (auto channel = it->second.ptr.lock(); channel) + { + channel->updateStreamStatus( + result.second, !it->second.wasChecked); + it->second.wasChecked = true; + } + else + { + deadChannels.append(result.first); + } + } + } + } + + if (!deadChannels.isEmpty()) + { + std::unique_lock lock(this->channelsMutex); + for (const auto &deadChannel : deadChannels) + { + this->channels.erase(deadChannel); + } + } + }, + [] { + qCWarning(LOG) << "Failed stream check request"; + }, + [] {}); + + // TODO: Explore making this concurrent + getHelix()->fetchChannels( + batch, + [this, batch{batch}](const auto &helixChannels) { + QStringList deadChannels; + + { + std::shared_lock lock(this->channelsMutex); + for (const auto &helixChannel : helixChannels) + { + auto it = this->channels.find(helixChannel.userId); + if (it != this->channels.end()) + { + if (auto channel = it->second.ptr.lock(); channel) + { + channel->updateStreamTitle(helixChannel.title); + channel->updateDisplayName(helixChannel.name); + } + else + { + deadChannels.append(helixChannel.userId); + } + } + } + } + + if (!deadChannels.isEmpty()) + { + std::unique_lock lock(this->channelsMutex); + for (const auto &deadChannel : deadChannels) + { + this->channels.erase(deadChannel); + } + } + }, + [] { + qCWarning(LOG) << "Failed stream check request"; + }); + } +} + +} // namespace chatterino diff --git a/src/controllers/twitch/LiveController.hpp b/src/controllers/twitch/LiveController.hpp new file mode 100644 index 000000000..767004061 --- /dev/null +++ b/src/controllers/twitch/LiveController.hpp @@ -0,0 +1,93 @@ +#pragma once + +#include "util/QStringHash.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace chatterino { + +class TwitchChannel; + +class ITwitchLiveController +{ +public: + virtual ~ITwitchLiveController() = default; + + virtual void add(const std::shared_ptr &newChannel) = 0; +}; + +class TwitchLiveController : public ITwitchLiveController +{ +public: + // Controls how often all channels have their stream status refreshed + static constexpr std::chrono::seconds REFRESH_INTERVAL{30}; + + // Controls how quickly new channels have their stream status loaded + static constexpr std::chrono::seconds IMMEDIATE_REQUEST_INTERVAL{1}; + + /** + * How many channels to include in a single request + * + * Should not be more than 100 + **/ + static constexpr int BATCH_SIZE{100}; + + TwitchLiveController(); + + // Add a Twitch channel to be queried for live status + // A request is made within a few seconds if this is the first time this channel is added + void add(const std::shared_ptr &newChannel) override; + +private: + struct ChannelEntry { + std::weak_ptr ptr; + bool wasChecked = false; + }; + + /** + * Run batched Helix Channels & Stream requests for channels + * + * If a list of channel IDs is passed to request, we only make a request for those channels + * + * If no list of channels is passed to request (the default behaviour), we make requests for all channels + * in the `channels` map. + **/ + void request(std::optional optChannelIDs = std::nullopt); + + /** + * List of channel IDs pointing to their Twitch Channel + * + * These channels will have their stream status updated every REFRESH_INTERVAL seconds + **/ + std::unordered_map channels; + std::shared_mutex channelsMutex; + + /** + * List of channels that need an immediate live status update + * + * These channels will have their stream status updated after at most IMMEDIATE_REQUEST_INTERVAL seconds + **/ + std::unordered_set immediateRequests; + std::mutex immediateRequestsMutex; + + /** + * Timer responsible for refreshing `channels` + **/ + QTimer refreshTimer; + + /** + * Timer responsible for refreshing `immediateRequests` + **/ + QTimer immediateRequestTimer; +}; + +} // namespace chatterino diff --git a/src/controllers/userdata/UserData.hpp b/src/controllers/userdata/UserData.hpp new file mode 100644 index 000000000..37eb79aa8 --- /dev/null +++ b/src/controllers/userdata/UserData.hpp @@ -0,0 +1,72 @@ +#pragma once + +#include "util/RapidjsonHelpers.hpp" +#include "util/RapidJsonSerializeQString.hpp" + +#include +#include +#include + +#include + +namespace chatterino { + +// UserData defines a set of data that is defined for a unique user +// It can contain things like optional replacement color for the user, a unique alias +// or a user note that should be displayed with the user +// Replacement fields should be optional, where none denotes that the field should not be updated for the user +struct UserData { + std::optional color{std::nullopt}; + + // TODO: User note? +}; + +} // namespace chatterino + +namespace pajlada { + +template <> +struct Serialize { + static rapidjson::Value get(const chatterino::UserData &value, + rapidjson::Document::AllocatorType &a) + { + rapidjson::Value obj; + obj.SetObject(); + if (value.color) + { + const auto &color = *value.color; + chatterino::rj::set(obj, "color", + color.name().toUtf8().toStdString(), a); + } + return obj; + } +}; + +template <> +struct Deserialize { + static chatterino::UserData get(const rapidjson::Value &value, + bool *error = nullptr) + { + if (!value.IsObject()) + { + PAJLADA_REPORT_ERROR(error) + return chatterino::UserData{}; + } + + chatterino::UserData user; + + QString colorString; + if (chatterino::rj::getSafe(value, "color", colorString)) + { + QColor color(colorString); + if (color.isValid()) + { + user.color = color; + } + } + + return user; + } +}; + +} // namespace pajlada diff --git a/src/controllers/userdata/UserDataController.cpp b/src/controllers/userdata/UserDataController.cpp new file mode 100644 index 000000000..dc79f16ae --- /dev/null +++ b/src/controllers/userdata/UserDataController.cpp @@ -0,0 +1,94 @@ +#include "controllers/userdata/UserDataController.hpp" + +#include "singletons/Paths.hpp" +#include "util/CombinePath.hpp" +#include "util/Helpers.hpp" + +namespace { + +using namespace chatterino; + +std::shared_ptr initSettingsInstance( + const Paths &paths) +{ + auto sm = std::make_shared(); + + auto path = combinePath(paths.settingsDirectory, "user-data.json"); + + sm->setPath(path.toUtf8().toStdString()); + + sm->setBackupEnabled(true); + sm->setBackupSlots(9); + sm->saveMethod = + pajlada::Settings::SettingManager::SaveMethod::SaveAllTheTime; + + return sm; +} + +} // namespace + +namespace chatterino { + +UserDataController::UserDataController(const Paths &paths) + : sm(initSettingsInstance(paths)) + , setting("/users", this->sm) +{ + this->sm->load(); + this->users = this->setting.getValue(); +} + +std::optional UserDataController::getUser(const QString &userID) const +{ + std::shared_lock lock(this->usersMutex); + auto it = this->users.find(userID); + + if (it == this->users.end()) + { + return std::nullopt; + } + + return it->second; +} + +std::unordered_map UserDataController::getUsers() const +{ + std::shared_lock lock(this->usersMutex); + return this->users; +} + +void UserDataController::setUserColor(const QString &userID, + const QString &colorString) +{ + auto c = this->getUsers(); + auto it = c.find(userID); + std::optional finalColor = + makeConditionedOptional(!colorString.isEmpty(), QColor(colorString)); + if (it == c.end()) + { + if (!finalColor) + { + // Early out - user is not configured and will not get a new color + return; + } + + UserData user; + user.color = finalColor; + c.insert({userID, user}); + } + else + { + it->second.color = finalColor; + } + + this->update(std::move(c)); +} + +void UserDataController::update( + std::unordered_map &&newUsers) +{ + std::unique_lock lock(this->usersMutex); + this->users = std::move(newUsers); + this->setting.setValue(this->users); +} + +} // namespace chatterino diff --git a/src/controllers/userdata/UserDataController.hpp b/src/controllers/userdata/UserDataController.hpp new file mode 100644 index 000000000..49edbde75 --- /dev/null +++ b/src/controllers/userdata/UserDataController.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include "controllers/userdata/UserData.hpp" +#include "util/QStringHash.hpp" +#include "util/RapidjsonHelpers.hpp" +#include "util/RapidJsonSerializeQString.hpp" +#include "util/serialize/Container.hpp" + +#include +#include +#include + +#include +#include +#include + +namespace chatterino { + +class Paths; + +class IUserDataController +{ +public: + virtual ~IUserDataController() = default; + + virtual std::optional getUser(const QString &userID) const = 0; + + virtual void setUserColor(const QString &userID, + const QString &colorString) = 0; +}; + +class UserDataController : public IUserDataController +{ +public: + explicit UserDataController(const Paths &paths); + + // Get extra data about a user + // If the user does not have any extra data, return none + std::optional getUser(const QString &userID) const override; + + // Update or insert extra data for the user's color override + void setUserColor(const QString &userID, + const QString &colorString) override; + +private: + void update(std::unordered_map &&newUsers); + + std::unordered_map getUsers() const; + + // Stores a real-time list of users & their customizations + std::unordered_map users; + mutable std::shared_mutex usersMutex; + + std::shared_ptr sm; + pajlada::Settings::Setting> setting; +}; + +} // namespace chatterino diff --git a/src/debug/AssertInGuiThread.hpp b/src/debug/AssertInGuiThread.hpp index 6dc144963..4cdf6606a 100644 --- a/src/debug/AssertInGuiThread.hpp +++ b/src/debug/AssertInGuiThread.hpp @@ -2,16 +2,17 @@ #include #include + #include namespace chatterino { -static bool isGuiThread() +inline bool isGuiThread() { return QCoreApplication::instance()->thread() == QThread::currentThread(); } -static void assertInGuiThread() +inline void assertInGuiThread() { #ifdef _DEBUG assert(isGuiThread()); diff --git a/src/debug/Benchmark.cpp b/src/debug/Benchmark.cpp index 58c6cea42..0aca0a61d 100644 --- a/src/debug/Benchmark.cpp +++ b/src/debug/Benchmark.cpp @@ -1,4 +1,5 @@ -#include "Benchmark.hpp" +#include "debug/Benchmark.hpp" + #include "common/QLogging.hpp" namespace chatterino { diff --git a/src/debug/Benchmark.hpp b/src/debug/Benchmark.hpp index cb7660b51..e9b058ba7 100644 --- a/src/debug/Benchmark.hpp +++ b/src/debug/Benchmark.hpp @@ -2,15 +2,21 @@ #include #include -#include namespace chatterino { -class BenchmarkGuard : boost::noncopyable +class BenchmarkGuard { public: BenchmarkGuard(const QString &_name); ~BenchmarkGuard(); + + BenchmarkGuard(const BenchmarkGuard &) = delete; + BenchmarkGuard &operator=(const BenchmarkGuard &) = delete; + + BenchmarkGuard(BenchmarkGuard &&) = delete; + BenchmarkGuard &operator=(BenchmarkGuard &&) = delete; + qreal getElapsedMs(); private: diff --git a/src/main.cpp b/src/main.cpp index 44e75495b..4ece0deb8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,21 +1,27 @@ -#include -#include -#include -#include -#include - #include "BrowserExtension.hpp" -#include "RunGui.hpp" #include "common/Args.hpp" +#include "common/Env.hpp" #include "common/Modes.hpp" #include "common/QLogging.hpp" #include "common/Version.hpp" #include "providers/IvrApi.hpp" +#include "providers/NetworkConfigurationProvider.hpp" #include "providers/twitch/api/Helix.hpp" +#include "RunGui.hpp" +#include "singletons/CrashHandler.hpp" #include "singletons/Paths.hpp" #include "singletons/Settings.hpp" +#include "singletons/Updates.hpp" #include "util/AttachToConsole.hpp" -#include "util/IncognitoBrowser.hpp" +#include "util/IpcQueue.hpp" + +#include +#include +#include +#include +#include + +#include using namespace chatterino; @@ -25,13 +31,13 @@ int main(int argc, char **argv) QCoreApplication::setApplicationName("chatterino"); QCoreApplication::setApplicationVersion(CHATTERINO_VERSION); - QCoreApplication::setOrganizationDomain("https://www.chatterino.com"); + QCoreApplication::setOrganizationDomain("chatterino.com"); - Paths *paths{}; + std::unique_ptr paths; try { - paths = new Paths; + paths = std::make_unique(); } catch (std::runtime_error &error) { @@ -53,15 +59,20 @@ int main(int argc, char **argv) box.exec(); return 1; } + ipc::initPaths(paths.get()); - initArgs(a); + const Args args(a, *paths); + +#ifdef CHATTERINO_WITH_CRASHPAD + const auto crashpadHandler = installCrashHandler(args, *paths); +#endif // run in gui mode or browser extension host mode - if (getArgs().shouldRunBrowserExtensionHost) + if (args.shouldRunBrowserExtensionHost) { runBrowserExtensionHost(); } - else if (getArgs().printVersion) + else if (args.printVersion) { attachToConsole(); @@ -75,17 +86,40 @@ int main(int argc, char **argv) } else { - if (getArgs().verbose) + if (args.verbose) { attachToConsole(); } + qCInfo(chatterinoApp).noquote() + << "Chatterino Qt SSL library build version:" + << QSslSocket::sslLibraryBuildVersionString(); + qCInfo(chatterinoApp).noquote() + << "Chatterino Qt SSL library version:" + << QSslSocket::sslLibraryVersionString(); +#if QT_VERSION >= QT_VERSION_CHECK(6, 1, 0) + qCInfo(chatterinoApp).noquote() + << "Chatterino Qt SSL active backend:" + << QSslSocket::activeBackend() << "of" + << QSslSocket::availableBackends().join(", "); +# if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) + qCInfo(chatterinoApp) << "Chatterino Qt SSL active backend features:" + << QSslSocket::supportedFeatures(); +# endif + qCInfo(chatterinoApp) << "Chatterino Qt SSL active backend protocols:" + << QSslSocket::supportedProtocols(); +#endif + + Settings settings(args, paths->settingsDirectory); + + Updates updates(*paths, settings); + + NetworkConfigurationProvider::applyFromEnv(Env::get()); + IvrApi::initialize(); Helix::initialize(); - Settings settings(paths->settingsDirectory); - - runGui(a, *paths, settings); + runGui(a, *paths, settings, args, updates); } return 0; } diff --git a/src/messages/Emote.cpp b/src/messages/Emote.cpp index 7c0f5e821..f19fc3027 100644 --- a/src/messages/Emote.cpp +++ b/src/messages/Emote.cpp @@ -1,4 +1,4 @@ -#include "Emote.hpp" +#include "messages/Emote.hpp" #include @@ -20,7 +20,9 @@ EmotePtr cachedOrMakeEmotePtr(Emote &&emote, const EmoteMap &cache) // reuse old shared_ptr if nothing changed auto it = cache.find(emote.name); if (it != cache.end() && *it->second == emote) + { return it->second; + } return std::make_shared(std::move(emote)); } @@ -46,4 +48,23 @@ EmotePtr cachedOrMakeEmotePtr( } } +EmoteMap::const_iterator EmoteMap::findEmote(const QString &emoteNameHint, + const QString &emoteID) const +{ + auto it = this->end(); + if (!emoteNameHint.isEmpty()) + { + it = this->find(EmoteName{emoteNameHint}); + } + + if (it == this->end() || it->second->id.string != emoteID) + { + it = std::find_if(this->begin(), this->end(), + [emoteID](const auto entry) { + return entry.second->id.string == emoteID; + }); + } + return it; +} + } // namespace chatterino diff --git a/src/messages/Emote.hpp b/src/messages/Emote.hpp index d7c19d238..57e4a8e68 100644 --- a/src/messages/Emote.hpp +++ b/src/messages/Emote.hpp @@ -1,10 +1,12 @@ #pragma once -#include "messages/Image.hpp" +#include "common/Aliases.hpp" #include "messages/ImageSet.hpp" #include #include +#include +#include #include namespace chatterino { @@ -14,6 +16,14 @@ struct Emote { ImageSet images; Tooltip tooltip; Url homePage; + bool zeroWidth{}; + EmoteId id; + EmoteAuthor author; + /** + * If this emote is aliased, this contains + * the original (base) name of the emote. + */ + std::optional baseName; // FOURTF: no solution yet, to be refactored later const QString &getCopyString() const @@ -29,10 +39,21 @@ using EmotePtr = std::shared_ptr; class EmoteMap : public std::unordered_map { +public: + /** + * Finds an emote by it's id with a hint to it's name. + * + * 1. Searches by name for the emote, checking if the ids match (fast-path). + * 2. Searches through the map for an emote with the `emoteID` (slow-path). + * + * @param emoteNameHint A hint to the name of the searched emote, + * may be empty. + * @param emoteID The emote id to search for. + * @return An iterator to the found emote (possibly this->end()). + */ + EmoteMap::const_iterator findEmote(const QString &emoteNameHint, + const QString &emoteID) const; }; -using EmoteIdMap = std::unordered_map; -using WeakEmoteMap = std::unordered_map>; -using WeakEmoteIdMap = std::unordered_map>; static const std::shared_ptr EMPTY_EMOTE_MAP = std::make_shared< const EmoteMap>(); // NOLINT(cert-err58-cpp) -- assume this doesn't throw an exception diff --git a/src/messages/Image.cpp b/src/messages/Image.cpp index fda0637f6..59be19957 100644 --- a/src/messages/Image.cpp +++ b/src/messages/Image.cpp @@ -1,55 +1,58 @@ #include "messages/Image.hpp" +#include "Application.hpp" +#include "common/Common.hpp" +#include "common/network/NetworkRequest.hpp" +#include "common/network/NetworkResult.hpp" +#include "common/QLogging.hpp" +#include "debug/AssertInGuiThread.hpp" +#include "debug/Benchmark.hpp" +#include "singletons/Emotes.hpp" +#include "singletons/helper/GifTimer.hpp" +#include "singletons/WindowManager.hpp" +#include "util/DebugCount.hpp" +#include "util/PostToThread.hpp" + +#include #include #include #include #include #include #include -#include -#include -#include "Application.hpp" -#include "common/Common.hpp" -#include "common/NetworkRequest.hpp" -#include "common/QLogging.hpp" -#include "debug/AssertInGuiThread.hpp" -#include "debug/Benchmark.hpp" -#ifndef CHATTERINO_TEST -# include "singletons/Emotes.hpp" -#endif -#include "singletons/WindowManager.hpp" -#include "singletons/helper/GifTimer.hpp" -#include "util/DebugCount.hpp" -#include "util/PostToThread.hpp" +#include -#include +// Duration between each check of every Image instance +const auto IMAGE_POOL_CLEANUP_INTERVAL = std::chrono::minutes(1); +// Duration since last usage of Image pixmap before expiration of frames +const auto IMAGE_POOL_IMAGE_LIFETIME = std::chrono::minutes(10); -namespace chatterino { -namespace detail { - // Frames - Frames::Frames() +namespace chatterino::detail { + +Frames::Frames() +{ + DebugCount::increase("images"); +} + +Frames::Frames(QList &&frames) + : items_(std::move(frames)) +{ + assertInGuiThread(); + DebugCount::increase("images"); + if (!this->empty()) { - DebugCount::increase("images"); + DebugCount::increase("loaded images"); } - Frames::Frames(const QVector> &frames) - : items_(frames) + if (this->animated()) { - assertInGuiThread(); - DebugCount::increase("images"); + DebugCount::increase("animated images"); - if (this->animated()) - { - DebugCount::increase("animated images"); - -#ifndef CHATTERINO_TEST - this->gifTimerConnection_ = - getApp()->emotes->gifTimer.signal.connect([this] { - this->advance(); - }); -#endif - } + this->gifTimerConnection_ = + getApp()->getEmotes()->getGIFTimer().signal.connect([this] { + this->advance(); + }); auto totalLength = std::accumulate(this->items_.begin(), this->items_.end(), 0UL, @@ -63,186 +66,215 @@ namespace detail { } else { -#ifndef CHATTERINO_TEST this->durationOffset_ = std::min( - int(getApp()->emotes->gifTimer.position() % totalLength), + int(getApp()->getEmotes()->getGIFTimer().position() % + totalLength), 60000); -#endif } this->processOffset(); } - Frames::~Frames() - { - assertInGuiThread(); - DebugCount::decrease("images"); + DebugCount::increase("image bytes", this->memoryUsage()); + DebugCount::increase("image bytes (ever loaded)", this->memoryUsage()); +} - if (this->animated()) +Frames::~Frames() +{ + assertInGuiThread(); + DebugCount::decrease("images"); + if (!this->empty()) + { + DebugCount::decrease("loaded images"); + } + + if (this->animated()) + { + DebugCount::decrease("animated images"); + } + DebugCount::decrease("image bytes", this->memoryUsage()); + DebugCount::increase("image bytes (ever unloaded)", this->memoryUsage()); + + this->gifTimerConnection_.disconnect(); +} + +int64_t Frames::memoryUsage() const +{ + int64_t usage = 0; + for (const auto &frame : this->items_) + { + auto sz = frame.image.size(); + auto area = sz.width() * sz.height(); + auto memory = area * frame.image.depth() / 8; + + usage += memory; + } + return usage; +} + +void Frames::advance() +{ + this->durationOffset_ += GIF_FRAME_LENGTH; + this->processOffset(); +} + +void Frames::processOffset() +{ + if (this->items_.isEmpty()) + { + return; + } + + while (true) + { + this->index_ %= this->items_.size(); + + if (this->durationOffset_ > this->items_[this->index_].duration) { - DebugCount::decrease("animated images"); + this->durationOffset_ -= this->items_[this->index_].duration; + this->index_ = (this->index_ + 1) % this->items_.size(); } + else + { + break; + } + } +} - this->gifTimerConnection_.disconnect(); +void Frames::clear() +{ + assertInGuiThread(); + if (!this->empty()) + { + DebugCount::decrease("loaded images"); + } + DebugCount::decrease("image bytes", this->memoryUsage()); + DebugCount::increase("image bytes (ever unloaded)", this->memoryUsage()); + + this->items_.clear(); + this->index_ = 0; + this->durationOffset_ = 0; + this->gifTimerConnection_.disconnect(); +} + +bool Frames::empty() const +{ + return this->items_.empty(); +} + +bool Frames::animated() const +{ + return this->items_.size() > 1; +} + +std::optional Frames::current() const +{ + if (this->items_.empty()) + { + return std::nullopt; } - void Frames::advance() + return this->items_[this->index_].image; +} + +std::optional Frames::first() const +{ + if (this->items_.empty()) { - this->durationOffset_ += gifFrameLength; - this->processOffset(); + return std::nullopt; } - void Frames::processOffset() + return this->items_.front().image; +} + +QList readFrames(QImageReader &reader, const Url &url) +{ + QList frames; + frames.reserve(reader.imageCount()); + + for (int index = 0; index < reader.imageCount(); ++index) { - if (this->items_.isEmpty()) + auto pixmap = QPixmap::fromImageReader(&reader); + if (!pixmap.isNull()) + { + // It seems that browsers have special logic for fast animations. + // This implements Chrome and Firefox's behavior which uses + // a duration of 100 ms for any frames that specify a duration of <= 10 ms. + // See http://webkit.org/b/36082 for more information. + // https://github.com/SevenTV/chatterino7/issues/46#issuecomment-1010595231 + int duration = reader.nextImageDelay(); + if (duration <= 10) + { + duration = 100; + } + duration = std::max(20, duration); + frames.append(Frame{ + .image = std::move(pixmap), + .duration = duration, + }); + } + } + + if (frames.empty()) + { + qCDebug(chatterinoImage) << "Error while reading image" << url.string + << ": '" << reader.errorString() << "'"; + } + + return frames; +} + +void assignFrames(std::weak_ptr weak, QList parsed) +{ + static bool isPushQueued; + + auto cb = [parsed = std::move(parsed), weak = std::move(weak)]() mutable { + auto shared = weak.lock(); + if (!shared) { return; } + shared->frames_ = std::make_unique(std::move(parsed)); - while (true) + // Avoid too many layouts in one event-loop iteration + // + // This callback is called for every image, so there might be multiple + // callbacks queued on the event-loop in this iteration, but we only + // want to generate one invalidation. + if (!isPushQueued) { - this->index_ %= this->items_.size(); - - if (this->durationOffset_ > this->items_[this->index_].duration) - { - this->durationOffset_ -= this->items_[this->index_].duration; - this->index_ = (this->index_ + 1) % this->items_.size(); - } - else - { - break; - } + isPushQueued = true; + postToThread([] { + isPushQueued = false; + getApp()->getWindows()->forceLayoutChannelViews(); + }); } - } + }; - bool Frames::animated() const - { - return this->items_.size() > 1; - } + postToGuiThread(cb); +} - boost::optional Frames::current() const - { - if (this->items_.size() == 0) - return boost::none; - return this->items_[this->index_].image; - } +} // namespace chatterino::detail - boost::optional Frames::first() const - { - if (this->items_.size() == 0) - return boost::none; - return this->items_.front().image; - } - - // functions - QVector> readFrames(QImageReader &reader, const Url &url) - { - QVector> frames; - - QImage image; - for (int index = 0; index < reader.imageCount(); ++index) - { - if (reader.read(&image)) - { - QPixmap::fromImage(image); - // It seems that browsers have special logic for fast animations. - // This implements Chrome and Firefox's behavior which uses - // a duration of 100 ms for any frames that specify a duration of <= 10 ms. - // See http://webkit.org/b/36082 for more information. - // https://github.com/SevenTV/chatterino7/issues/46#issuecomment-1010595231 - int duration = reader.nextImageDelay(); - if (duration <= 10) - duration = 100; - duration = std::max(20, duration); - frames.push_back(Frame{image, duration}); - } - } - - if (frames.size() == 0) - { - qCDebug(chatterinoImage) - << "Error while reading image" << url.string << ": '" - << reader.errorString() << "'"; - } - - return frames; - } - - // parsed - template - void assignDelayed( - std::queue>>> &queued, - std::mutex &mutex, std::atomic_bool &loadedEventQueued) - { - std::lock_guard lock(mutex); - int i = 0; - - while (!queued.empty()) - { - queued.front().first(queued.front().second); - queued.pop(); - - if (++i > 50) - { - QTimer::singleShot(3, [&] { - assignDelayed(queued, mutex, loadedEventQueued); - }); - return; - } - } - -#ifndef CHATTERINO_TEST - getApp()->windows->forceLayoutChannelViews(); -#endif - loadedEventQueued = false; - } - - template - auto makeConvertCallback(const QVector> &parsed, - Assign assign) - { - return [parsed, assign] { - // convert to pixmap - auto frames = QVector>(); - std::transform(parsed.begin(), parsed.end(), - std::back_inserter(frames), [](auto &frame) { - return Frame{ - QPixmap::fromImage(frame.image), - frame.duration}; - }); - - // put into stack - static std::queue>>> - queued; - static std::mutex mutex; - - std::lock_guard lock(mutex); - queued.emplace(assign, frames); - - static std::atomic_bool loadedEventQueued{false}; - - if (!loadedEventQueued) - { - loadedEventQueued = true; - - QTimer::singleShot(100, [=] { - assignDelayed(queued, mutex, loadedEventQueued); - }); - } - }; - } -} // namespace detail +namespace chatterino { // IMAGE2 Image::~Image() { - if (this->empty_) +#ifndef DISABLE_IMAGE_EXPIRATION_POOL + ImageExpirationPool::instance().removeImagePtr(this); +#endif + + if (this->empty_ && !this->frames_) { // No data in this image, don't bother trying to release it // The reason we do this check is that we keep a few (or one) static empty image around that are deconstructed at the end of the programs lifecycle, and we want to prevent the isGuiThread call to be called after the QApplication has been exited return; } - // run destructor of Frames in gui thread + // Ensure the destructor for our frames is called in the GUI thread + // If the Image destructor is called outside of the GUI thread, move the + // ownership of the frames to the GUI thread, otherwise the frames will be + // destructed as part as we go out of scope if (!isGuiThread()) { postToThread([frames = this->frames_.release()]() { @@ -251,7 +283,7 @@ Image::~Image() } } -ImagePtr Image::fromUrl(const Url &url, qreal scale) +ImagePtr Image::fromUrl(const Url &url, qreal scale, QSize expectedSize) { static std::unordered_map> cache; static std::mutex mutex; @@ -262,19 +294,41 @@ ImagePtr Image::fromUrl(const Url &url, qreal scale) if (!shared) { - cache[url] = shared = ImagePtr(new Image(url, scale)); + cache[url] = shared = ImagePtr(new Image(url, scale, expectedSize)); } return shared; } -ImagePtr Image::fromPixmap(const QPixmap &pixmap, qreal scale) +ImagePtr Image::fromResourcePixmap(const QPixmap &pixmap, qreal scale) { - auto result = ImagePtr(new Image(scale)); + using key_t = std::pair; + static std::unordered_map, boost::hash> + cache; + static std::mutex mutex; - result->setPixmap(pixmap); + std::lock_guard lock(mutex); - return result; + auto it = cache.find({&pixmap, scale}); + if (it != cache.end()) + { + auto shared = it->second.lock(); + if (shared) + { + return shared; + } + + cache.erase(it); + } + + auto newImage = ImagePtr(new Image(scale)); + + newImage->setPixmap(pixmap); + + // store in cache + cache.insert({{&pixmap, scale}, std::weak_ptr(newImage)}); + + return newImage; } ImagePtr Image::getEmpty() @@ -283,14 +337,21 @@ ImagePtr Image::getEmpty() return empty; } +ImagePtr getEmptyImagePtr() +{ + return Image::getEmpty(); +} + Image::Image() : empty_(true) { } -Image::Image(const Url &url, qreal scale) +Image::Image(const Url &url, qreal scale, QSize expectedSize) : url_(url) , scale_(scale) + , expectedSize_(expectedSize.isValid() ? expectedSize + : (QSize(16, 16) * scale)) , shouldLoad_(true) , frames_(std::make_unique()) { @@ -306,7 +367,7 @@ void Image::setPixmap(const QPixmap &pixmap) { auto setFrames = [shared = this->shared_from_this(), pixmap]() { shared->frames_ = std::make_unique( - QVector>{detail::Frame{pixmap, 1}}); + QList{detail::Frame{pixmap, 1}}); }; if (isGuiThread()) @@ -328,13 +389,18 @@ bool Image::loaded() const { assertInGuiThread(); - return bool(this->frames_->current()); + return this->frames_->current().has_value(); } -boost::optional Image::pixmapOrLoad() const +std::optional Image::pixmapOrLoad() const { assertInGuiThread(); + // Mark the image as just used. + // Any time this Image is painted, this method is invoked. + // See src/messages/layouts/MessageLayoutElement.cpp ImageLayoutElement::paint, for example. + this->lastUsed_ = std::chrono::steady_clock::now(); + this->load(); return this->frames_->current(); @@ -346,8 +412,12 @@ void Image::load() const if (this->shouldLoad_) { - const_cast(this)->shouldLoad_ = false; - const_cast(this)->actuallyLoad(); + Image *this2 = const_cast(this); + this2->shouldLoad_ = false; + this2->actuallyLoad(); +#ifndef DISABLE_IMAGE_EXPIRATION_POOL + ImageExpirationPool::instance().addImagePtr(this2->shared_from_this()); +#endif } } @@ -373,9 +443,12 @@ int Image::width() const assertInGuiThread(); if (auto pixmap = this->frames_->first()) - return int(pixmap->width() * this->scale_); - else - return 16; + { + return static_cast(pixmap->width() * this->scale_); + } + + // No frames loaded, use the expected size + return static_cast(this->expectedSize_.width() * this->scale_); } int Image::height() const @@ -383,39 +456,44 @@ int Image::height() const assertInGuiThread(); if (auto pixmap = this->frames_->first()) - return int(pixmap->height() * this->scale_); - else - return 16; + { + return static_cast(pixmap->height() * this->scale_); + } + + // No frames loaded, use the expected size + return static_cast(this->expectedSize_.height() * this->scale_); } void Image::actuallyLoad() { + auto weak = weakOf(this); NetworkRequest(this->url().string) .concurrent() .cache() - .onSuccess([weak = weakOf(this)](auto result) -> Outcome { + .onSuccess([weak](auto result) { auto shared = weak.lock(); if (!shared) - return Failure; + { + return; + } - auto data = result.getData(); - - // const cast since we are only reading from it - QBuffer buffer(const_cast(&data)); - buffer.open(QIODevice::ReadOnly); + QBuffer buffer; + buffer.setData(result.getData()); QImageReader reader(&buffer); if (!reader.canRead()) { qCDebug(chatterinoImage) << "Error: image cant be read " << shared->url().string; - return Failure; + shared->empty_ = true; + return; } const auto size = reader.size(); if (size.isEmpty()) { - return Failure; + shared->empty_ = true; + return; } // returns 1 for non-animated formats @@ -424,7 +502,8 @@ void Image::actuallyLoad() qCDebug(chatterinoImage) << "Error: image has less than 1 frame " << shared->url().string << ": " << reader.errorString(); - return Failure; + shared->empty_ = true; + return; } // use "double" to prevent int overflows @@ -434,22 +513,20 @@ void Image::actuallyLoad() { qCDebug(chatterinoImage) << "image too large in RAM"; - return Failure; + shared->empty_ = true; + return; } auto parsed = detail::readFrames(reader, shared->url()); - postToThread(makeConvertCallback(parsed, [weak](auto frames) { - if (auto shared = weak.lock()) - shared->frames_ = std::make_unique(frames); - })); - - return Success; + assignFrames(shared, parsed); }) - .onError([weak = weakOf(this)](auto /*result*/) { + .onError([weak](auto /*result*/) { auto shared = weak.lock(); if (!shared) + { return false; + } // fourtf: is this the right thing to do? shared->empty_ = true; @@ -459,21 +536,126 @@ void Image::actuallyLoad() .execute(); } -bool Image::operator==(const Image &other) const +void Image::expireFrames() { - if (this->isEmpty() && other.isEmpty()) - return true; - if (!this->url_.string.isEmpty() && this->url_ == other.url_) - return true; - if (this->frames_->first() == other.frames_->first()) - return true; - - return false; + assertInGuiThread(); + this->frames_->clear(); + this->shouldLoad_ = true; // Mark as needing load again } -bool Image::operator!=(const Image &other) const +#ifndef DISABLE_IMAGE_EXPIRATION_POOL + +ImageExpirationPool::ImageExpirationPool() + : freeTimer_(new QTimer) { - return !this->operator==(other); + QObject::connect(this->freeTimer_, &QTimer::timeout, [this] { + if (isGuiThread()) + { + this->freeOld(); + } + else + { + postToThread([this] { + this->freeOld(); + }); + } + }); + + this->freeTimer_->start( + std::chrono::duration_cast( + IMAGE_POOL_CLEANUP_INTERVAL)); + + // configure all debug counts used by images + DebugCount::configure("image bytes", DebugCount::Flag::DataSize); + DebugCount::configure("image bytes (ever loaded)", + DebugCount::Flag::DataSize); + DebugCount::configure("image bytes (ever unloaded)", + DebugCount::Flag::DataSize); } +ImageExpirationPool &ImageExpirationPool::instance() +{ + static auto *instance = new ImageExpirationPool; + return *instance; +} + +void ImageExpirationPool::addImagePtr(ImagePtr imgPtr) +{ + std::lock_guard lock(this->mutex_); + this->allImages_.emplace(imgPtr.get(), std::weak_ptr(imgPtr)); +} + +void ImageExpirationPool::removeImagePtr(Image *rawPtr) +{ + std::lock_guard lock(this->mutex_); + this->allImages_.erase(rawPtr); +} + +void ImageExpirationPool::freeAll() +{ + { + std::lock_guard lock(this->mutex_); + for (auto it = this->allImages_.begin(); it != this->allImages_.end();) + { + auto img = it->second.lock(); + img->expireFrames(); + it = this->allImages_.erase(it); + } + } + this->freeOld(); +} + +void ImageExpirationPool::freeOld() +{ + std::lock_guard lock(this->mutex_); + + size_t numExpired = 0; + size_t eligible = 0; + + auto now = std::chrono::steady_clock::now(); + for (auto it = this->allImages_.begin(); it != this->allImages_.end();) + { + auto img = it->second.lock(); + if (!img) + { + // This can only really happen from a race condition because ~Image + // should remove itself from the ImageExpirationPool automatically. + it = this->allImages_.erase(it); + continue; + } + + if (img->frames_->empty()) + { + // No frame data, nothing to do + ++it; + continue; + } + + ++eligible; + + // Check if image has expired and, if so, expire its frame data + auto diff = now - img->lastUsed_; + if (diff > IMAGE_POOL_IMAGE_LIFETIME) + { + ++numExpired; + img->expireFrames(); + // erase without mutex locking issue + it = this->allImages_.erase(it); + continue; + } + + ++it; + } + +# ifndef NDEBUG + qCDebug(chatterinoImage) << "freed frame data for" << numExpired << "/" + << eligible << "eligible images"; +# endif + DebugCount::set("last image gc: expired", numExpired); + DebugCount::set("last image gc: eligible", eligible); + DebugCount::set("last image gc: left after gc", this->allImages_.size()); +} + +#endif + } // namespace chatterino diff --git a/src/messages/Image.hpp b/src/messages/Image.hpp index a4ad674fe..31351ab78 100644 --- a/src/messages/Image.hpp +++ b/src/messages/Image.hpp @@ -1,53 +1,76 @@ #pragma once +#include "common/Aliases.hpp" + +#include +#include +#include #include #include #include -#include +#include + #include -#include -#include -#include +#include +#include #include #include -#include - -#include "common/Aliases.hpp" -#include "common/Common.hpp" +#include namespace chatterino { -namespace detail { - template - struct Frame { - Image image; - int duration; - }; - class Frames : boost::noncopyable - { - public: - Frames(); - Frames(const QVector> &frames); - ~Frames(); - bool animated() const; - void advance(); - boost::optional current() const; - boost::optional first() const; +class Image; - private: - void processOffset(); - QVector> items_; - int index_{0}; - int durationOffset_{0}; - pajlada::Signals::Connection gifTimerConnection_; - }; -} // namespace detail +} // namespace chatterino + +namespace chatterino::detail { + +struct Frame { + QPixmap image; + int duration; +}; + +class Frames +{ +public: + Frames(); + Frames(QList &&frames); + ~Frames(); + + Frames(const Frames &) = delete; + Frames &operator=(const Frames &) = delete; + + Frames(Frames &&) = delete; + Frames &operator=(Frames &&) = delete; + + void clear(); + bool empty() const; + bool animated() const; + void advance(); + std::optional current() const; + std::optional first() const; + +private: + int64_t memoryUsage() const; + void processOffset(); + QList items_; + QList::size_type index_{0}; + int durationOffset_{0}; + pajlada::Signals::Connection gifTimerConnection_; +}; + +QList readFrames(QImageReader &reader, const Url &url); +void assignFrames(std::weak_ptr weak, QList parsed); + +} // namespace chatterino::detail + +namespace chatterino { class Image; using ImagePtr = std::shared_ptr; /// This class is thread safe. -class Image : public std::enable_shared_from_this, boost::noncopyable +class Image : public std::enable_shared_from_this { public: // Maximum amount of RAM used by the image in bytes. @@ -55,14 +78,21 @@ public: ~Image(); - static ImagePtr fromUrl(const Url &url, qreal scale = 1); - static ImagePtr fromPixmap(const QPixmap &pixmap, qreal scale = 1); + Image(const Image &) = delete; + Image &operator=(const Image &) = delete; + + Image(Image &&) = delete; + Image &operator=(Image &&) = delete; + + static ImagePtr fromUrl(const Url &url, qreal scale = 1, + QSize expectedSize = {}); + static ImagePtr fromResourcePixmap(const QPixmap &pixmap, qreal scale = 1); static ImagePtr getEmpty(); const Url &url() const; bool loaded() const; // either returns the current pixmap, or triggers loading it (lazy loading) - boost::optional pixmapOrLoad() const; + std::optional pixmapOrLoad() const; void load() const; qreal scale() const; bool isEmpty() const; @@ -70,23 +100,74 @@ public: int height() const; bool animated() const; - bool operator==(const Image &image) const; - bool operator!=(const Image &image) const; + bool operator==(const Image &image) = delete; + bool operator!=(const Image &image) = delete; private: Image(); - Image(const Url &url, qreal scale); + Image(const Url &url, qreal scale, QSize expectedSize); Image(qreal scale); void setPixmap(const QPixmap &pixmap); void actuallyLoad(); + void expireFrames(); const Url url_{}; const qreal scale_{1}; + /// @brief The expected size of this image once its loaded. + /// + /// This doesn't represent the actual size (it can be different) - it's + /// just an estimation and provided to avoid (large) layout shifts when + /// loading images. + const QSize expectedSize_{16, 16}; std::atomic_bool empty_{false}; - // gui thread only bool shouldLoad_{false}; - std::unique_ptr frames_{}; + + mutable std::chrono::time_point lastUsed_; + + // gui thread only + std::unique_ptr frames_; + + friend class ImageExpirationPool; + friend void detail::assignFrames(std::weak_ptr, + QList); }; + +// forward-declarable function that calls Image::getEmpty() under the hood. +ImagePtr getEmptyImagePtr(); + +#ifndef DISABLE_IMAGE_EXPIRATION_POOL + +class ImageExpirationPool +{ +public: + ImageExpirationPool(); + static ImageExpirationPool &instance(); + + void addImagePtr(ImagePtr imgPtr); + void removeImagePtr(Image *rawPtr); + + /** + * @brief Frees frame data for all images that ImagePool deems to have expired. + * + * Expiration is based on last accessed time of the Image, stored in Image::lastUsed_. + * Must be ran in the GUI thread. + */ + void freeOld(); + + /* + * Debug function that unloads all images in the pool. This is intended to + * test for possible memory leaks from tracked images. + */ + void freeAll(); + + // Timer to periodically run freeOld() + QTimer *freeTimer_; + std::map> allImages_; + std::mutex mutex_; +}; + +#endif + } // namespace chatterino diff --git a/src/messages/ImageSet.cpp b/src/messages/ImageSet.cpp index 1dccf10e9..7e25e3f86 100644 --- a/src/messages/ImageSet.cpp +++ b/src/messages/ImageSet.cpp @@ -1,5 +1,6 @@ #include "messages/ImageSet.hpp" +#include "messages/Image.hpp" #include "singletons/Settings.hpp" namespace chatterino { @@ -60,16 +61,18 @@ const ImagePtr &ImageSet::getImage3() const const std::shared_ptr &getImagePriv(const ImageSet &set, float scale) { -#ifndef CHATTERINO_TEST scale *= getSettings()->emoteScale; -#endif int quality = 1; if (scale > 2.001f) + { quality = 3; + } else if (scale > 1.001f) + { quality = 2; + } if (!set.getImage3()->isEmpty() && quality == 3) { @@ -93,17 +96,27 @@ const ImagePtr &ImageSet::getImageOrLoaded(float scale) const // prefer other image if selected image is not loaded yet if (result->loaded()) + { return result; + } else if (this->imageX3_ && !this->imageX3_->isEmpty() && this->imageX3_->loaded()) + { return this->imageX3_; + } else if (this->imageX2_ && !this->imageX2_->isEmpty() && this->imageX2_->loaded()) + { return this->imageX2_; + } else if (this->imageX1_->loaded()) + { return this->imageX1_; + } else + { return result; + } } const ImagePtr &ImageSet::getImage(float scale) const diff --git a/src/messages/ImageSet.hpp b/src/messages/ImageSet.hpp index 4e484eedb..49c94eed0 100644 --- a/src/messages/ImageSet.hpp +++ b/src/messages/ImageSet.hpp @@ -1,15 +1,22 @@ #pragma once -#include "messages/Image.hpp" +#include "common/Aliases.hpp" + +#include namespace chatterino { +class Image; +using ImagePtr = std::shared_ptr; +ImagePtr getEmptyImagePtr(); + class ImageSet { public: ImageSet(); - ImageSet(const ImagePtr &image1, const ImagePtr &image2 = Image::getEmpty(), - const ImagePtr &image3 = Image::getEmpty()); + ImageSet(const ImagePtr &image1, + const ImagePtr &image2 = getEmptyImagePtr(), + const ImagePtr &image3 = getEmptyImagePtr()); ImageSet(const Url &image1, const Url &image2 = {}, const Url &image3 = {}); void setImage1(const ImagePtr &image); diff --git a/src/messages/LimitedQueue.hpp b/src/messages/LimitedQueue.hpp index 8c419e184..e06e5a0f2 100644 --- a/src/messages/LimitedQueue.hpp +++ b/src/messages/LimitedQueue.hpp @@ -3,10 +3,10 @@ #include "messages/LimitedQueueSnapshot.hpp" #include -#include #include #include +#include #include #include @@ -24,14 +24,6 @@ public: private: /// Property Accessors - /** - * @brief Return the limit of the internal buffer - */ - [[nodiscard]] size_t limit() const - { - return this->limit_; - } - /** * @brief Return the amount of space left in the buffer * @@ -43,6 +35,14 @@ private: } public: + /** + * @brief Return the limit of the queue + */ + [[nodiscard]] size_t limit() const + { + return this->limit_; + } + /** * @brief Return true if the buffer is empty */ @@ -62,13 +62,13 @@ public: * @param[in] index the index of the item to fetch * @return the item at the index if it's populated, or none if it's not */ - [[nodiscard]] boost::optional get(size_t index) const + [[nodiscard]] std::optional get(size_t index) const { std::shared_lock lock(this->mutex_); if (index >= this->buffer_.size()) { - return boost::none; + return std::nullopt; } return this->buffer_[index]; @@ -79,13 +79,13 @@ public: * * @return the item at the front of the queue if it's populated, or none the queue is empty */ - [[nodiscard]] boost::optional first() const + [[nodiscard]] std::optional first() const { std::shared_lock lock(this->mutex_); if (this->buffer_.empty()) { - return boost::none; + return std::nullopt; } return this->buffer_.front(); @@ -96,13 +96,13 @@ public: * * @return the item at the back of the queue if it's populated, or none the queue is empty */ - [[nodiscard]] boost::optional last() const + [[nodiscard]] std::optional last() const { std::shared_lock lock(this->mutex_); if (this->buffer_.empty()) { - return boost::none; + return std::nullopt; } return this->buffer_.back(); @@ -293,14 +293,14 @@ public: * * The contents of the LimitedQueue are iterated over from front to back * until the first element that satisfies `pred(item)`. If no item - * satisfies the predicate, or if the queue is empty, then boost::none + * satisfies the predicate, or if the queue is empty, then std::nullopt * is returned. * * @param[in] pred predicate that will be applied to items - * @return the first item found or boost::none + * @return the first item found or std::nullopt */ template - [[nodiscard]] boost::optional find(Predicate pred) const + [[nodiscard]] std::optional find(Predicate pred) const { std::shared_lock lock(this->mutex_); @@ -312,7 +312,7 @@ public: } } - return boost::none; + return std::nullopt; } /** @@ -320,14 +320,14 @@ public: * * The contents of the LimitedQueue are iterated over from back to front * until the first element that satisfies `pred(item)`. If no item - * satisfies the predicate, or if the queue is empty, then boost::none + * satisfies the predicate, or if the queue is empty, then std::nullopt * is returned. * * @param[in] pred predicate that will be applied to items - * @return the first item found or boost::none + * @return the first item found or std::nullopt */ template - [[nodiscard]] boost::optional rfind(Predicate pred) const + [[nodiscard]] std::optional rfind(Predicate pred) const { std::shared_lock lock(this->mutex_); @@ -339,7 +339,7 @@ public: } } - return boost::none; + return std::nullopt; } private: diff --git a/src/messages/Link.hpp b/src/messages/Link.hpp index f6a48a7d3..2692ace69 100644 --- a/src/messages/Link.hpp +++ b/src/messages/Link.hpp @@ -25,6 +25,7 @@ public: CopyToClipboard, ReplyToMessage, ViewThread, + JumpToMessage, }; Link(); diff --git a/src/messages/Message.cpp b/src/messages/Message.cpp index da5c0a801..8840c6919 100644 --- a/src/messages/Message.cpp +++ b/src/messages/Message.cpp @@ -1,13 +1,10 @@ #include "messages/Message.hpp" -#include "Application.hpp" -#include "MessageElement.hpp" -#include "providers/twitch/PubSubActions.hpp" -#include "singletons/Theme.hpp" +#include "providers/colors/ColorProvider.hpp" +#include "providers/twitch/TwitchBadge.hpp" +#include "singletons/Settings.hpp" #include "util/DebugCount.hpp" -#include "util/IrcHelpers.hpp" - -using SBHighlight = chatterino::ScrollbarHighlight; +#include "widgets/helper/ScrollbarHighlight.hpp" namespace chatterino { @@ -22,38 +19,65 @@ Message::~Message() DebugCount::decrease("messages"); } -SBHighlight Message::getScrollBarHighlight() const +ScrollbarHighlight Message::getScrollBarHighlight() const { if (this->flags.has(MessageFlag::Highlighted) || this->flags.has(MessageFlag::HighlightedWhisper)) { - return SBHighlight(this->highlightColor); + return { + this->highlightColor, + }; } - else if (this->flags.has(MessageFlag::Subscription) && - getSettings()->enableSubHighlight) + + if (this->flags.has(MessageFlag::Subscription) && + getSettings()->enableSubHighlight) { - return SBHighlight( - ColorProvider::instance().color(ColorType::Subscription)); + return { + ColorProvider::instance().color(ColorType::Subscription), + }; } - else if (this->flags.has(MessageFlag::RedeemedHighlight) || - this->flags.has(MessageFlag::RedeemedChannelPointReward)) + + if (this->flags.has(MessageFlag::RedeemedHighlight) || + this->flags.has(MessageFlag::RedeemedChannelPointReward)) { - return SBHighlight( + return { ColorProvider::instance().color(ColorType::RedeemedHighlight), - SBHighlight::Default, true); + ScrollbarHighlight::Default, + true, + }; } - else if (this->flags.has(MessageFlag::FirstMessage)) + + if (this->flags.has(MessageFlag::ElevatedMessage)) { - return SBHighlight( - ColorProvider::instance().color(ColorType::FirstMessageHighlight), - SBHighlight::Default, false, true); + return { + ColorProvider::instance().color( + ColorType::ElevatedMessageHighlight), + ScrollbarHighlight::Default, + false, + false, + true, + }; } - return SBHighlight(); + + if (this->flags.has(MessageFlag::FirstMessage)) + { + return { + ColorProvider::instance().color(ColorType::FirstMessageHighlight), + ScrollbarHighlight::Default, + false, + true, + }; + } + + if (this->flags.has(MessageFlag::AutoModOffendingMessage) || + this->flags.has(MessageFlag::AutoModOffendingMessageHeader)) + { + return { + ColorProvider::instance().color(ColorType::AutomodHighlight), + }; + } + + return {}; } -// Static -namespace { - -} // namespace - } // namespace chatterino diff --git a/src/messages/Message.hpp b/src/messages/Message.hpp index cc9f36503..898f12217 100644 --- a/src/messages/Message.hpp +++ b/src/messages/Message.hpp @@ -1,54 +1,35 @@ #pragma once -#include "common/FlagsEnum.hpp" -#include "providers/twitch/TwitchBadge.hpp" +#include "messages/MessageFlag.hpp" +#include "providers/twitch/ChannelPointReward.hpp" #include "util/QStringHash.hpp" -#include "widgets/helper/ScrollbarHighlight.hpp" +#include #include -#include + #include #include +#include #include namespace chatterino { class MessageElement; class MessageThread; +class Badge; +class ScrollbarHighlight; -enum class MessageFlag : uint32_t { - None = 0, - System = (1 << 0), - Timeout = (1 << 1), - Highlighted = (1 << 2), - DoNotTriggerNotification = (1 << 3), // disable notification sound - Centered = (1 << 4), - Disabled = (1 << 5), - DisableCompactEmotes = (1 << 6), - Collapsed = (1 << 7), - ConnectedMessage = (1 << 8), - DisconnectedMessage = (1 << 9), - Untimeout = (1 << 10), - PubSub = (1 << 11), - Subscription = (1 << 12), - DoNotLog = (1 << 13), - AutoMod = (1 << 14), - RecentMessage = (1 << 15), - Whisper = (1 << 16), - HighlightedWhisper = (1 << 17), - Debug = (1 << 18), - Similar = (1 << 19), - RedeemedHighlight = (1 << 20), - RedeemedChannelPointReward = (1 << 21), - ShowInMentions = (1 << 22), - FirstMessage = (1 << 23), - ReplyMessage = (1 << 24), -}; -using MessageFlags = FlagsEnum; - -struct Message : boost::noncopyable { +struct Message; +using MessagePtr = std::shared_ptr; +struct Message { Message(); ~Message(); + Message(const Message &) = delete; + Message &operator=(const Message &) = delete; + + Message(Message &&) = delete; + Message &operator=(Message &&) = delete; + // Making this a mutable means that we can update a messages flags, // while still keeping Message constant. This means that a message's flag // can be updated without the renderer being made aware, which might be bad. @@ -74,12 +55,13 @@ struct Message : boost::noncopyable { // the reply thread will be cleaned up by the TwitchChannel. // The root of the thread does not have replyThread set. std::shared_ptr replyThread; + MessagePtr replyParent; uint32_t count = 1; std::vector> elements; ScrollbarHighlight getScrollBarHighlight() const; + + std::shared_ptr reward = nullptr; }; -using MessagePtr = std::shared_ptr; - } // namespace chatterino diff --git a/src/messages/MessageBuilder.cpp b/src/messages/MessageBuilder.cpp index d76b8b4a1..d3750ea63 100644 --- a/src/messages/MessageBuilder.cpp +++ b/src/messages/MessageBuilder.cpp @@ -1,20 +1,573 @@ -#include "MessageBuilder.hpp" +#include "messages/MessageBuilder.hpp" #include "Application.hpp" #include "common/LinkParser.hpp" +#include "common/Literals.hpp" +#include "common/QLogging.hpp" #include "controllers/accounts/AccountController.hpp" +#include "controllers/highlights/HighlightController.hpp" +#include "controllers/ignores/IgnoreController.hpp" +#include "controllers/ignores/IgnorePhrase.hpp" +#include "controllers/userdata/UserDataController.hpp" +#include "messages/Emote.hpp" #include "messages/Image.hpp" #include "messages/Message.hpp" +#include "messages/MessageColor.hpp" #include "messages/MessageElement.hpp" -#include "providers/LinkResolver.hpp" +#include "messages/MessageThread.hpp" +#include "providers/bttv/BttvEmotes.hpp" +#include "providers/chatterino/ChatterinoBadges.hpp" +#include "providers/colors/ColorProvider.hpp" +#include "providers/ffz/FfzBadges.hpp" +#include "providers/ffz/FfzEmotes.hpp" +#include "providers/links/LinkResolver.hpp" +#include "providers/seventv/SeventvBadges.hpp" +#include "providers/seventv/SeventvEmotes.hpp" +#include "providers/twitch/api/Helix.hpp" +#include "providers/twitch/ChannelPointReward.hpp" #include "providers/twitch/PubSubActions.hpp" +#include "providers/twitch/TwitchAccount.hpp" +#include "providers/twitch/TwitchBadge.hpp" +#include "providers/twitch/TwitchBadges.hpp" +#include "providers/twitch/TwitchChannel.hpp" +#include "providers/twitch/TwitchIrcServer.hpp" #include "singletons/Emotes.hpp" #include "singletons/Resources.hpp" +#include "singletons/Settings.hpp" +#include "singletons/StreamerMode.hpp" #include "singletons/Theme.hpp" +#include "singletons/WindowManager.hpp" #include "util/FormatTime.hpp" -#include "util/Qt.hpp" +#include "util/Helpers.hpp" +#include "util/IrcHelpers.hpp" +#include "util/QStringHash.hpp" +#include "widgets/Window.hpp" +#include +#include +#include #include +#include +#include + +#include +#include + +using namespace chatterino::literals; + +namespace { + +using namespace chatterino; +using namespace std::chrono_literals; + +const QColor AUTOMOD_USER_COLOR{"blue"}; + +const QString regexHelpString("(\\w+)[.,!?;:]*?$"); + +// matches a mention with punctuation at the end, like "@username," or "@username!!!" where capture group would return "username" +const QRegularExpression mentionRegex("^@" + regexHelpString); + +// if findAllUsernames setting is enabled, matches strings like in the examples above, but without @ symbol at the beginning +const QRegularExpression allUsernamesMentionRegex("^" + regexHelpString); + +const QSet zeroWidthEmotes{ + "SoSnowy", "IceCold", "SantaHat", "TopHat", + "ReinDeer", "CandyCane", "cvMask", "cvHazmat", +}; + +struct HypeChatPaidLevel { + std::chrono::seconds duration; + uint8_t numeric; +}; + +const std::unordered_map HYPE_CHAT_PAID_LEVEL{ + {u"ONE"_s, {30s, 1}}, {u"TWO"_s, {2min + 30s, 2}}, + {u"THREE"_s, {5min, 3}}, {u"FOUR"_s, {10min, 4}}, + {u"FIVE"_s, {30min, 5}}, {u"SIX"_s, {1h, 6}}, + {u"SEVEN"_s, {2h, 7}}, {u"EIGHT"_s, {3h, 8}}, + {u"NINE"_s, {4h, 9}}, {u"TEN"_s, {5h, 10}}, +}; + +QString formatUpdatedEmoteList(const QString &platform, + const std::vector &emoteNames, + bool isAdd, bool isFirstWord) +{ + QString text = ""; + if (isAdd) + { + text += isFirstWord ? "Added" : "added"; + } + else + { + text += isFirstWord ? "Removed" : "removed"; + } + + if (emoteNames.size() == 1) + { + text += QString(" %1 emote ").arg(platform); + } + else + { + text += QString(" %1 %2 emotes ").arg(emoteNames.size()).arg(platform); + } + + auto i = 0; + for (const auto &emoteName : emoteNames) + { + i++; + if (i > 1) + { + text += i == emoteNames.size() ? " and " : ", "; + } + text += emoteName; + } + + text += "."; + + return text; +} + +/** + * Gets the default sound url if the user set one, + * or the chatterino default ping sound if no url is set. + */ +QUrl getFallbackHighlightSound() +{ + QString path = getSettings()->pathHighlightSound; + bool fileExists = + !path.isEmpty() && QFileInfo::exists(path) && QFileInfo(path).isFile(); + + if (fileExists) + { + return QUrl::fromLocalFile(path); + } + + return QUrl("qrc:/sounds/ping2.wav"); +} + +void actuallyTriggerHighlights(const QString &channelName, bool playSound, + const std::optional &customSoundUrl, + bool windowAlert) +{ + if (getApp()->getStreamerMode()->isEnabled() && + getSettings()->streamerModeMuteMentions) + { + // We are in streamer mode with muting mention sounds enabled. Do nothing. + return; + } + + if (getSettings()->isMutedChannel(channelName)) + { + // Do nothing. Pings are muted in this channel. + return; + } + + const bool hasFocus = (QApplication::focusWidget() != nullptr); + const bool resolveFocus = + !hasFocus || getSettings()->highlightAlwaysPlaySound; + + if (playSound && resolveFocus) + { + // TODO(C++23): optional or_else + QUrl soundUrl; + if (customSoundUrl) + { + soundUrl = *customSoundUrl; + } + else + { + soundUrl = getFallbackHighlightSound(); + } + getApp()->getSound()->play(soundUrl); + } + + if (windowAlert) + { + getApp()->getWindows()->sendAlert(); + } +} + +QString stylizeUsername(const QString &username, const Message &message) +{ + const QString &localizedName = message.localizedName; + bool hasLocalizedName = !localizedName.isEmpty(); + + // The full string that will be rendered in the chat widget + QString usernameText; + + switch (getSettings()->usernameDisplayMode.getValue()) + { + case UsernameDisplayMode::Username: { + usernameText = username; + } + break; + + case UsernameDisplayMode::LocalizedName: { + if (hasLocalizedName) + { + usernameText = localizedName; + } + else + { + usernameText = username; + } + } + break; + + default: + case UsernameDisplayMode::UsernameAndLocalizedName: { + if (hasLocalizedName) + { + usernameText = username + "(" + localizedName + ")"; + } + else + { + usernameText = username; + } + } + break; + } + + if (auto nicknameText = getSettings()->matchNickname(usernameText)) + { + usernameText = *nicknameText; + } + + return usernameText; +} + +void appendTwitchEmoteOccurrences(const QString &emote, + std::vector &vec, + const std::vector &correctPositions, + const QString &originalMessage, + int messageOffset) +{ + auto *app = getApp(); + if (!emote.contains(':')) + { + return; + } + + auto parameters = emote.split(':'); + + if (parameters.length() < 2) + { + return; + } + + auto id = EmoteId{parameters.at(0)}; + + auto occurrences = parameters.at(1).split(','); + + for (const QString &occurrence : occurrences) + { + auto coords = occurrence.split('-'); + + if (coords.length() < 2) + { + return; + } + + auto from = coords.at(0).toUInt() - messageOffset; + auto to = coords.at(1).toUInt() - messageOffset; + auto maxPositions = correctPositions.size(); + if (from > to || to >= maxPositions) + { + // Emote coords are out of range + qCDebug(chatterinoTwitch) + << "Emote coords" << from << "-" << to << "are out of range (" + << maxPositions << ")"; + return; + } + + auto start = correctPositions[from]; + auto end = correctPositions[to]; + if (start > end || start < 0 || end > originalMessage.length()) + { + // Emote coords are out of range from the modified character positions + qCDebug(chatterinoTwitch) << "Emote coords" << from << "-" << to + << "are out of range after offsets (" + << originalMessage.length() << ")"; + return; + } + + auto name = EmoteName{originalMessage.mid(start, end - start + 1)}; + TwitchEmoteOccurrence emoteOccurrence{ + start, + end, + app->getEmotes()->getTwitchEmotes()->getOrCreateEmote(id, name), + name, + }; + if (emoteOccurrence.ptr == nullptr) + { + qCDebug(chatterinoTwitch) + << "nullptr" << emoteOccurrence.name.string; + } + vec.push_back(std::move(emoteOccurrence)); + } +} + +std::optional getTwitchBadge(const Badge &badge, + const TwitchChannel *twitchChannel) +{ + if (auto channelBadge = + twitchChannel->twitchBadge(badge.key_, badge.value_)) + { + return channelBadge; + } + + if (auto globalBadge = + getApp()->getTwitchBadges()->badge(badge.key_, badge.value_)) + { + return globalBadge; + } + + return std::nullopt; +} + +void appendBadges(MessageBuilder *builder, const std::vector &badges, + const std::unordered_map &badgeInfos, + const TwitchChannel *twitchChannel) +{ + if (twitchChannel == nullptr) + { + return; + } + + for (const auto &badge : badges) + { + auto badgeEmote = getTwitchBadge(badge, twitchChannel); + if (!badgeEmote) + { + continue; + } + auto tooltip = (*badgeEmote)->tooltip.string; + + if (badge.key_ == "bits") + { + const auto &cheerAmount = badge.value_; + tooltip = QString("Twitch cheer %0").arg(cheerAmount); + } + else if (badge.key_ == "moderator" && + getSettings()->useCustomFfzModeratorBadges) + { + if (auto customModBadge = twitchChannel->ffzCustomModBadge()) + { + builder + ->emplace( + *customModBadge, + MessageElementFlag::BadgeChannelAuthority) + ->setTooltip((*customModBadge)->tooltip.string); + // early out, since we have to add a custom badge element here + continue; + } + } + else if (badge.key_ == "vip" && getSettings()->useCustomFfzVipBadges) + { + if (auto customVipBadge = twitchChannel->ffzCustomVipBadge()) + { + builder + ->emplace( + *customVipBadge, + MessageElementFlag::BadgeChannelAuthority) + ->setTooltip((*customVipBadge)->tooltip.string); + // early out, since we have to add a custom badge element here + continue; + } + } + else if (badge.flag_ == MessageElementFlag::BadgeSubscription) + { + auto badgeInfoIt = badgeInfos.find(badge.key_); + if (badgeInfoIt != badgeInfos.end()) + { + // badge.value_ is 4 chars long if user is subbed on higher tier + // (tier + amount of months with leading zero if less than 100) + // e.g. 3054 - tier 3 4,5-year sub. 2108 - tier 2 9-year sub + const auto &subTier = + badge.value_.length() > 3 ? badge.value_.at(0) : '1'; + const auto &subMonths = badgeInfoIt->second; + tooltip += + QString(" (%1%2 months)") + .arg(subTier != '1' ? QString("Tier %1, ").arg(subTier) + : "") + .arg(subMonths); + } + } + else if (badge.flag_ == MessageElementFlag::BadgePredictions) + { + auto badgeInfoIt = badgeInfos.find(badge.key_); + if (badgeInfoIt != badgeInfos.end()) + { + auto infoValue = badgeInfoIt->second; + auto predictionText = + infoValue + .replace(R"(\s)", " ") // standard IRC escapes + .replace(R"(\:)", ";") + .replace(R"(\\)", R"(\)") + .replace("⸝", ","); // twitch's comma escape + // Careful, the first character is RIGHT LOW PARAPHRASE BRACKET or U+2E1D, which just looks like a comma + + tooltip = QString("Predicted %1").arg(predictionText); + } + } + + builder->emplace(*badgeEmote, badge.flag_) + ->setTooltip(tooltip); + } + + builder->message().badges = badges; + builder->message().badgeInfos = badgeInfos; +} + +/** + * Computes (only) the replacement of @a match in @a source. + * The parts before and after the match in @a source are ignored. + * + * Occurrences of \b{\\1}, \b{\\2}, ..., in @a replacement are replaced + * with the string captured by the corresponding capturing group. + * This function should only be used if the regex contains capturing groups. + * + * Since Qt doesn't provide a way of replacing a single match with some replacement + * while supporting both capturing groups and lookahead/-behind in the regex, + * this is included here. It's essentially the implementation of + * QString::replace(const QRegularExpression &, const QString &). + * @see https://github.com/qt/qtbase/blob/97bb0ecfe628b5bb78e798563212adf02129c6f6/src/corelib/text/qstring.cpp#L4594-L4703 + */ +QString makeRegexReplacement(QStringView source, + const QRegularExpression ®ex, + const QRegularExpressionMatch &match, + const QString &replacement) +{ + using SizeType = QString::size_type; + struct QStringCapture { + SizeType pos; + SizeType len; + int captureNumber; + }; + + qsizetype numCaptures = regex.captureCount(); + + // 1. build the backreferences list, holding where the backreferences + // are in the replacement string + QVarLengthArray backReferences; + + SizeType replacementLength = replacement.size(); + for (SizeType i = 0; i < replacementLength - 1; i++) + { + if (replacement[i] != u'\\') + { + continue; + } + + int no = replacement[i + 1].digitValue(); + if (no <= 0 || no > numCaptures) + { + continue; + } + + QStringCapture backReference{.pos = i, .len = 2}; + + if (i < replacementLength - 2) + { + int secondDigit = replacement[i + 2].digitValue(); + if (secondDigit != -1 && ((no * 10) + secondDigit) <= numCaptures) + { + no = (no * 10) + secondDigit; + ++backReference.len; + } + } + + backReference.captureNumber = no; + backReferences.append(backReference); + } + + // 2. iterate on the matches. + // For every match, copy the replacement string in chunks + // with the proper replacements for the backreferences + + // length of the new string, with all the replacements + SizeType newLength = 0; + QVarLengthArray chunks; + QStringView replacementView{replacement}; + + // Initially: empty, as we only care about the replacement + SizeType len = 0; + SizeType lastEnd = 0; + for (const QStringCapture &backReference : std::as_const(backReferences)) + { + // part of "replacement" before the backreference + len = backReference.pos - lastEnd; + if (len > 0) + { + chunks << replacementView.mid(lastEnd, len); + newLength += len; + } + + // backreference itself + len = match.capturedLength(backReference.captureNumber); + if (len > 0) + { + chunks << source.mid( + match.capturedStart(backReference.captureNumber), len); + newLength += len; + } + + lastEnd = backReference.pos + backReference.len; + } + + // add the last part of the replacement string + len = replacementView.size() - lastEnd; + if (len > 0) + { + chunks << replacementView.mid(lastEnd, len); + newLength += len; + } + + // 3. assemble the chunks together + QString dst; + dst.reserve(newLength); + for (const QStringView &chunk : std::as_const(chunks)) + { + dst += chunk; + } + return dst; +} + +bool doesWordContainATwitchEmote( + int cursor, const QString &word, + const std::vector &twitchEmotes, + std::vector::const_iterator ¤tTwitchEmoteIt) +{ + if (currentTwitchEmoteIt == twitchEmotes.end()) + { + // No emote to add! + return false; + } + + const auto ¤tTwitchEmote = *currentTwitchEmoteIt; + + auto wordEnd = cursor + word.length(); + + // Check if this emote fits within the word boundaries + if (currentTwitchEmote.start < cursor || currentTwitchEmote.end > wordEnd) + { + // this emote does not fit xd + return false; + } + + return true; +} + +EmotePtr makeAutoModBadge() +{ + return std::make_shared(Emote{ + EmoteName{}, + ImageSet{Image::fromResourcePixmap(getResources().twitch.automod)}, + Tooltip{"AutoMod"}, + Url{"https://dashboard.twitch.tv/settings/moderation/automod"}}); +} + +} // namespace namespace chatterino { @@ -28,153 +581,38 @@ MessagePtr makeSystemMessage(const QString &text, const QTime &time) return MessageBuilder(systemMessage, text, time).release(); } -EmotePtr makeAutoModBadge() -{ - return std::make_shared(Emote{ - EmoteName{}, ImageSet{Image::fromPixmap(getResources().twitch.automod)}, - Tooltip{"AutoMod"}, - Url{"https://dashboard.twitch.tv/settings/moderation/automod"}}); -} - -MessagePtr makeAutomodInfoMessage(const AutomodInfoAction &action) -{ - auto builder = MessageBuilder(); - QString text("AutoMod: "); - - builder.emplace(); - builder.message().flags.set(MessageFlag::PubSub); - - // AutoMod shield badge - builder.emplace(makeAutoModBadge(), - MessageElementFlag::BadgeChannelAuthority); - // AutoMod "username" - builder.emplace("AutoMod:", MessageElementFlag::BoldUsername, - MessageColor(QColor("blue")), - FontStyle::ChatMediumBold); - builder.emplace( - "AutoMod:", MessageElementFlag::NonBoldUsername, - MessageColor(QColor("blue"))); - switch (action.type) - { - case AutomodInfoAction::OnHold: { - QString info("Hey! Your message is being checked " - "by mods and has not been sent."); - text += info; - builder.emplace(info, MessageElementFlag::Text, - MessageColor::Text); - } - break; - case AutomodInfoAction::Denied: { - QString info("Mods have removed your message."); - text += info; - builder.emplace(info, MessageElementFlag::Text, - MessageColor::Text); - } - break; - case AutomodInfoAction::Approved: { - QString info("Mods have accepted your message."); - text += info; - builder.emplace(info, MessageElementFlag::Text, - MessageColor::Text); - } - break; - } - - builder.message().flags.set(MessageFlag::AutoMod); - builder.message().messageText = text; - builder.message().searchText = text; - - auto message = builder.release(); - - return message; -} - -std::pair makeAutomodMessage( - const AutomodAction &action) -{ - MessageBuilder builder, builder2; - - // - // Builder for AutoMod message with explanation - builder.message().loginName = "automod"; - builder.message().flags.set(MessageFlag::PubSub); - builder.message().flags.set(MessageFlag::Timeout); - builder.message().flags.set(MessageFlag::AutoMod); - - // AutoMod shield badge - builder.emplace(makeAutoModBadge(), - MessageElementFlag::BadgeChannelAuthority); - // AutoMod "username" - builder.emplace("AutoMod:", MessageElementFlag::BoldUsername, - MessageColor(QColor("blue")), - FontStyle::ChatMediumBold); - builder.emplace( - "AutoMod:", MessageElementFlag::NonBoldUsername, - MessageColor(QColor("blue"))); - // AutoMod header message - builder.emplace( - ("Held a message for reason: " + action.reason + - ". Allow will post it in chat. "), - MessageElementFlag::Text, MessageColor::Text); - // Allow link button - builder - .emplace("Allow", MessageElementFlag::Text, - MessageColor(QColor("green")), - FontStyle::ChatMediumBold) - ->setLink({Link::AutoModAllow, action.msgID}); - // Deny link button - builder - .emplace(" Deny", MessageElementFlag::Text, - MessageColor(QColor("red")), - FontStyle::ChatMediumBold) - ->setLink({Link::AutoModDeny, action.msgID}); - // ID of message caught by AutoMod - // builder.emplace(action.msgID, MessageElementFlag::Text, - // MessageColor::Text); - auto text1 = - QString("AutoMod: Held a message for reason: %1. Allow will post " - "it in chat. Allow Deny") - .arg(action.reason); - builder.message().messageText = text1; - builder.message().searchText = text1; - - auto message1 = builder.release(); - - // - // Builder for offender's message - builder2.emplace(); - builder2.emplace(); - builder2.message().loginName = action.target.login; - builder2.message().flags.set(MessageFlag::PubSub); - builder2.message().flags.set(MessageFlag::Timeout); - builder2.message().flags.set(MessageFlag::AutoMod); - - // sender username - builder2 - .emplace( - action.target.displayName + ":", MessageElementFlag::BoldUsername, - MessageColor(action.target.color), FontStyle::ChatMediumBold) - ->setLink({Link::UserInfo, action.target.login}); - builder2 - .emplace(action.target.displayName + ":", - MessageElementFlag::NonBoldUsername, - MessageColor(action.target.color)) - ->setLink({Link::UserInfo, action.target.login}); - // sender's message caught by AutoMod - builder2.emplace(action.message, MessageElementFlag::Text, - MessageColor::Text); - auto text2 = - QString("%1: %2").arg(action.target.displayName, action.message); - builder2.message().messageText = text2; - builder2.message().searchText = text2; - - auto message2 = builder2.release(); - - return std::make_pair(message1, message2); -} - MessageBuilder::MessageBuilder() : message_(std::make_shared()) + , ircMessage(nullptr) +{ +} + +MessageBuilder::MessageBuilder(Channel *_channel, + const Communi::IrcPrivateMessage *_ircMessage, + const MessageParseArgs &_args) + : twitchChannel(dynamic_cast(_channel)) + , message_(std::make_shared()) + , channel(_channel) + , ircMessage(_ircMessage) + , args(_args) + , tags(this->ircMessage->tags()) + , originalMessage_(_ircMessage->content()) + , action_(_ircMessage->isAction()) +{ +} + +MessageBuilder::MessageBuilder(Channel *_channel, + const Communi::IrcMessage *_ircMessage, + const MessageParseArgs &_args, QString content, + bool isAction) + : twitchChannel(dynamic_cast(_channel)) + , message_(std::make_shared()) + , channel(_channel) + , ircMessage(_ircMessage) + , args(_args) + , tags(this->ircMessage->tags()) + , originalMessage_(content) + , action_(isAction) { } @@ -190,10 +628,10 @@ MessageBuilder::MessageBuilder(SystemMessageTag, const QString &text, text.split(QRegularExpression("\\s"), Qt::SkipEmptyParts); for (const auto &word : textFragments) { - const auto linkString = this->matchLink(word); - if (!linkString.isEmpty()) + auto link = linkparser::parse(word); + if (link) { - this->addLink(word, linkString); + this->addLink(*link, word); continue; } @@ -295,11 +733,10 @@ MessageBuilder::MessageBuilder(TimeoutMessageTag, const QString &username, this->message().searchText = fullText; } -// XXX: This does not belong in the MessageBuilder, this should be part of the TwitchMessageBuilder MessageBuilder::MessageBuilder(const BanAction &action, uint32_t count) : MessageBuilder() { - auto current = getApp()->accounts->twitch.getCurrent(); + auto current = getApp()->getAccounts()->twitch.getCurrent(); this->emplace(); this->message().flags.set(MessageFlag::System); @@ -350,7 +787,8 @@ MessageBuilder::MessageBuilder(const BanAction &action, uint32_t count) this->emplaceSystemTextAndUpdate("banned", text); if (action.reason.isEmpty()) { - this->emplaceSystemTextAndUpdate(action.target.login, text) + this->emplaceSystemTextAndUpdate(action.target.login + ".", + text) ->setLink({Link::UserInfo, action.target.login}); } else @@ -410,13 +848,72 @@ MessageBuilder::MessageBuilder(const UnbanAction &action) ->setLink({Link::UserInfo, action.source.login}); this->emplaceSystemTextAndUpdate( action.wasBan() ? "unbanned" : "untimedout", text); - this->emplaceSystemTextAndUpdate(action.target.login, text) + this->emplaceSystemTextAndUpdate(action.target.login + ".", text) ->setLink({Link::UserInfo, action.target.login}); this->message().messageText = text; this->message().searchText = text; } +MessageBuilder::MessageBuilder(const WarnAction &action) + : MessageBuilder() +{ + this->emplace(); + this->message().flags.set(MessageFlag::System); + + QString text; + + // TODO: Use MentionElement here, once WarnAction includes username/displayname + this->emplaceSystemTextAndUpdate("A moderator", text) + ->setLink({Link::UserInfo, "id:" + action.source.id}); + this->emplaceSystemTextAndUpdate("warned", text); + this->emplaceSystemTextAndUpdate( + action.target.login + (action.reasons.isEmpty() ? "." : ":"), text) + ->setLink({Link::UserInfo, action.target.login}); + + if (!action.reasons.isEmpty()) + { + this->emplaceSystemTextAndUpdate(action.reasons.join(", "), text); + } + + this->message().messageText = text; + this->message().searchText = text; +} + +MessageBuilder::MessageBuilder(const RaidAction &action) + : MessageBuilder() +{ + this->emplace(); + this->message().flags.set(MessageFlag::System); + + QString text; + + this->emplaceSystemTextAndUpdate(action.source.login, text) + ->setLink({Link::UserInfo, "id:" + action.source.id}); + this->emplaceSystemTextAndUpdate("initiated a raid to", text); + this->emplaceSystemTextAndUpdate(action.target + ".", text) + ->setLink({Link::UserInfo, action.target}); + + this->message().messageText = text; + this->message().searchText = text; +} + +MessageBuilder::MessageBuilder(const UnraidAction &action) + : MessageBuilder() +{ + this->emplace(); + this->message().flags.set(MessageFlag::System); + + QString text; + + this->emplaceSystemTextAndUpdate(action.source.login, text) + ->setLink({Link::UserInfo, "id:" + action.source.id}); + this->emplaceSystemTextAndUpdate("canceled the raid.", text); + + this->message().messageText = text; + this->message().searchText = text; +} + MessageBuilder::MessageBuilder(const AutomodUserAction &action) : MessageBuilder() { @@ -463,6 +960,203 @@ MessageBuilder::MessageBuilder(const AutomodUserAction &action) MessageColor::System); } +MessageBuilder::MessageBuilder(LiveUpdatesAddEmoteMessageTag /*unused*/, + const QString &platform, const QString &actor, + const std::vector &emoteNames) + : MessageBuilder() +{ + auto text = + formatUpdatedEmoteList(platform, emoteNames, true, actor.isEmpty()); + + this->emplace(); + if (!actor.isEmpty()) + { + this->emplace(actor, MessageElementFlag::Username, + MessageColor::System) + ->setLink({Link::UserInfo, actor}); + } + this->emplace(text, MessageElementFlag::Text, + MessageColor::System); + + QString finalText; + if (actor.isEmpty()) + { + finalText = text; + } + else + { + finalText = QString("%1 %2").arg(actor, text); + } + + this->message().loginName = actor; + this->message().messageText = finalText; + this->message().searchText = finalText; + + this->message().flags.set(MessageFlag::System); + this->message().flags.set(MessageFlag::LiveUpdatesAdd); + this->message().flags.set(MessageFlag::DoNotTriggerNotification); +} + +MessageBuilder::MessageBuilder(LiveUpdatesRemoveEmoteMessageTag /*unused*/, + const QString &platform, const QString &actor, + const std::vector &emoteNames) + : MessageBuilder() +{ + auto text = + formatUpdatedEmoteList(platform, emoteNames, false, actor.isEmpty()); + + this->emplace(); + if (!actor.isEmpty()) + { + this->emplace(actor, MessageElementFlag::Username, + MessageColor::System) + ->setLink({Link::UserInfo, actor}); + } + this->emplace(text, MessageElementFlag::Text, + MessageColor::System); + + QString finalText; + if (actor.isEmpty()) + { + finalText = text; + } + else + { + finalText = QString("%1 %2").arg(actor, text); + } + + this->message().loginName = actor; + this->message().messageText = finalText; + this->message().searchText = finalText; + + this->message().flags.set(MessageFlag::System); + this->message().flags.set(MessageFlag::LiveUpdatesRemove); + this->message().flags.set(MessageFlag::DoNotTriggerNotification); +} + +MessageBuilder::MessageBuilder(LiveUpdatesUpdateEmoteMessageTag /*unused*/, + const QString &platform, const QString &actor, + const QString &emoteName, + const QString &oldEmoteName) + : MessageBuilder() +{ + QString text; + if (actor.isEmpty()) + { + text = "Renamed"; + } + else + { + text = "renamed"; + } + text += + QString(" %1 emote %2 to %3.").arg(platform, oldEmoteName, emoteName); + + this->emplace(); + if (!actor.isEmpty()) + { + this->emplace(actor, MessageElementFlag::Username, + MessageColor::System) + ->setLink({Link::UserInfo, actor}); + } + this->emplace(text, MessageElementFlag::Text, + MessageColor::System); + + QString finalText; + if (actor.isEmpty()) + { + finalText = text; + } + else + { + finalText = QString("%1 %2").arg(actor, text); + } + + this->message().loginName = actor; + this->message().messageText = finalText; + this->message().searchText = finalText; + + this->message().flags.set(MessageFlag::System); + this->message().flags.set(MessageFlag::LiveUpdatesUpdate); + this->message().flags.set(MessageFlag::DoNotTriggerNotification); +} + +MessageBuilder::MessageBuilder(LiveUpdatesUpdateEmoteSetMessageTag /*unused*/, + const QString &platform, const QString &actor, + const QString &emoteSetName) + : MessageBuilder() +{ + auto text = QString("switched the active %1 Emote Set to \"%2\".") + .arg(platform, emoteSetName); + + this->emplace(); + this->emplace(actor, MessageElementFlag::Username, + MessageColor::System) + ->setLink({Link::UserInfo, actor}); + this->emplace(text, MessageElementFlag::Text, + MessageColor::System); + + auto finalText = QString("%1 %2").arg(actor, text); + + this->message().loginName = actor; + this->message().messageText = finalText; + this->message().searchText = finalText; + + this->message().flags.set(MessageFlag::System); + this->message().flags.set(MessageFlag::LiveUpdatesUpdate); + this->message().flags.set(MessageFlag::DoNotTriggerNotification); +} + +MessageBuilder::MessageBuilder(ImageUploaderResultTag /*unused*/, + const QString &imageLink, + const QString &deletionLink, + size_t imagesStillQueued, size_t secondsLeft) + : MessageBuilder() +{ + this->message().flags.set(MessageFlag::System); + this->message().flags.set(MessageFlag::DoNotTriggerNotification); + + this->emplace(); + + using MEF = MessageElementFlag; + auto addText = [this](QString text, + MessageColor color = + MessageColor::System) -> TextElement * { + this->message().searchText += text; + this->message().messageText += text; + return this->emplace(text, MEF::Text, color); + }; + + addText("Your image has been uploaded to"); + + // ASSUMPTION: the user gave this uploader configuration to the program + // therefore they trust that the host is not wrong/malicious. This doesn't obey getSettings()->lowercaseDomains. + // This also ensures that the LinkResolver doesn't get these links. + addText(imageLink, MessageColor::Link) + ->setLink({Link::Url, imageLink}) + ->setTrailingSpace(!deletionLink.isEmpty()); + + if (!deletionLink.isEmpty()) + { + addText("(Deletion link:"); + addText(deletionLink, MessageColor::Link) + ->setLink({Link::Url, deletionLink}) + ->setTrailingSpace(false); + addText(")")->setTrailingSpace(false); + } + addText("."); + + if (imagesStillQueued == 0) + { + return; + } + + addText(QString("%1 left. Please wait until all of them are uploaded. " + "About %2 seconds left.") + .arg(imagesStillQueued) + .arg(secondsLeft)); +} + Message *MessageBuilder::operator->() { return this->message_.get(); @@ -490,92 +1184,1381 @@ void MessageBuilder::append(std::unique_ptr element) this->message().elements.push_back(std::move(element)); } -QString MessageBuilder::matchLink(const QString &string) +void MessageBuilder::addLink(const linkparser::Parsed &parsedLink, + const QString &source) { - LinkParser linkParser(string); - - static QRegularExpression httpRegex( - "\\bhttps?://", QRegularExpression::CaseInsensitiveOption); - static QRegularExpression ftpRegex( - "\\bftps?://", QRegularExpression::CaseInsensitiveOption); - static QRegularExpression spotifyRegex( - "\\bspotify:", QRegularExpression::CaseInsensitiveOption); - - if (!linkParser.hasMatch()) - { - return QString(); - } - - QString captured = linkParser.getCaptured(); - - if (!captured.contains(httpRegex) && !captured.contains(ftpRegex) && - !captured.contains(spotifyRegex)) - { - captured.insert(0, "http://"); - } - - return captured; -} - -void MessageBuilder::addLink(const QString &origLink, - const QString &matchedLink) -{ - static QRegularExpression domainRegex( - R"(^(?:(?:ftp|http)s?:\/\/)?([^\/]+)(?:\/.*)?$)", - QRegularExpression::CaseInsensitiveOption); - QString lowercaseLinkString; - auto match = domainRegex.match(origLink); - if (match.isValid()) + QString origLink = parsedLink.link.toString(); + QString fullUrl; + + if (parsedLink.protocol.isNull()) { - lowercaseLinkString = origLink.mid(0, match.capturedStart(1)) + - match.captured(1).toLower() + - origLink.mid(match.capturedEnd(1)); + fullUrl = QStringLiteral("http://") + origLink; } else { - lowercaseLinkString = origLink; + lowercaseLinkString += parsedLink.protocol; + fullUrl = origLink; } - auto linkElement = Link(Link::Url, matchedLink); + + lowercaseLinkString += parsedLink.host.toString().toLower(); + lowercaseLinkString += parsedLink.rest; auto textColor = MessageColor(MessageColor::Link); - auto linkMELowercase = - this->emplace(lowercaseLinkString, - MessageElementFlag::LowercaseLink, textColor) - ->setLink(linkElement); - auto linkMEOriginal = - this->emplace(origLink, MessageElementFlag::OriginalLink, - textColor) - ->setLink(linkElement); - LinkResolver::getLinkInfo( - matchedLink, nullptr, - [weakMessage = this->weakOf(), linkMELowercase, linkMEOriginal, - matchedLink](QString tooltipText, Link originalLink, - ImagePtr thumbnail) { - auto shared = weakMessage.lock(); - if (!shared) + if (parsedLink.hasPrefix(source)) + { + this->emplace(parsedLink.prefix(source).toString(), + MessageElementFlag::Text, this->textColor_) + ->setTrailingSpace(false); + } + auto *el = this->emplace( + LinkElement::Parsed{.lowercase = lowercaseLinkString, + .original = origLink}, + fullUrl, MessageElementFlag::Text, textColor); + if (parsedLink.hasSuffix(source)) + { + el->setTrailingSpace(false); + this->emplace(parsedLink.suffix(source).toString(), + MessageElementFlag::Text, this->textColor_); + } + + getApp()->getLinkResolver()->resolve(el->linkInfo()); +} + +bool MessageBuilder::isIgnored() const +{ + return isIgnoredMessage({ + /*.message = */ this->originalMessage_, + /*.twitchUserID = */ this->tags.value("user-id").toString(), + /*.isMod = */ this->channel->isMod(), + /*.isBroadcaster = */ this->channel->isBroadcaster(), + }); +} + +bool MessageBuilder::isIgnoredReply() const +{ + return isIgnoredMessage({ + /*.message = */ this->originalMessage_, + /*.twitchUserID = */ + this->tags.value("reply-parent-user-id").toString(), + /*.isMod = */ this->channel->isMod(), + /*.isBroadcaster = */ this->channel->isBroadcaster(), + }); +} + +void MessageBuilder::triggerHighlights() +{ + if (this->historicalMessage_) + { + // Do nothing. Highlights should not be triggered on historical messages. + return; + } + + actuallyTriggerHighlights(this->channel->getName(), this->highlightSound_, + this->highlightSoundCustomUrl_, + this->highlightAlert_); +} + +MessagePtr MessageBuilder::build() +{ + assert(this->ircMessage != nullptr); + assert(this->channel != nullptr); + + // PARSE + this->userId_ = this->ircMessage->tag("user-id").toString(); + + this->parse(); + + if (this->userName == this->channel->getName()) + { + this->senderIsBroadcaster = true; + } + + this->message().channelName = this->channel->getName(); + + this->parseMessageID(); + + this->parseRoomID(); + + // If it is a reward it has to be appended first + if (this->args.channelPointRewardId != "") + { + assert(this->twitchChannel != nullptr); + const auto &reward = this->twitchChannel->channelPointReward( + this->args.channelPointRewardId); + if (reward) + { + this->appendChannelPointRewardMessage( + *reward, this->channel->isMod(), + this->channel->isBroadcaster()); + } + } + + this->appendChannelName(); + + if (this->tags.contains("rm-deleted")) + { + this->message().flags.set(MessageFlag::Disabled); + } + + this->historicalMessage_ = this->tags.contains("historical"); + + if (this->tags.contains("msg-id") && + this->tags["msg-id"].toString().split(';').contains( + "highlighted-message")) + { + this->message().flags.set(MessageFlag::RedeemedHighlight); + } + + if (this->tags.contains("first-msg") && + this->tags["first-msg"].toString() == "1") + { + this->message().flags.set(MessageFlag::FirstMessage); + } + + if (this->tags.contains("pinned-chat-paid-amount")) + { + this->message().flags.set(MessageFlag::ElevatedMessage); + } + + if (this->tags.contains("bits")) + { + this->message().flags.set(MessageFlag::CheerMessage); + } + + // reply threads + this->parseThread(); + + // timestamp + this->message().serverReceivedTime = calculateMessageTime(this->ircMessage); + this->emplace(this->message().serverReceivedTime.time()); + + if (this->shouldAddModerationElements()) + { + this->emplace(); + } + + this->appendTwitchBadges(); + + this->appendChatterinoBadges(); + this->appendFfzBadges(); + this->appendSeventvBadges(); + + this->appendUsername(); + + // QString bits; + auto iterator = this->tags.find("bits"); + if (iterator != this->tags.end()) + { + this->hasBits_ = true; + this->bitsLeft = iterator.value().toInt(); + this->bits = iterator.value().toString(); + } + + // Twitch emotes + auto twitchEmotes = MessageBuilder::parseTwitchEmotes( + this->tags, this->originalMessage_, this->messageOffset_); + + // This runs through all ignored phrases and runs its replacements on this->originalMessage_ + MessageBuilder::processIgnorePhrases( + *getSettings()->ignoredMessages.readOnly(), this->originalMessage_, + twitchEmotes); + + std::sort(twitchEmotes.begin(), twitchEmotes.end(), + [](const auto &a, const auto &b) { + return a.start < b.start; + }); + twitchEmotes.erase(std::unique(twitchEmotes.begin(), twitchEmotes.end(), + [](const auto &first, const auto &second) { + return first.start == second.start; + }), + twitchEmotes.end()); + + // words + QStringList splits = this->originalMessage_.split(' '); + + this->addWords(splits, twitchEmotes); + + QString stylizedUsername = stylizeUsername(this->userName, this->message()); + + this->message().messageText = this->originalMessage_; + this->message().searchText = + stylizedUsername + " " + this->message().localizedName + " " + + this->userName + ": " + this->originalMessage_ + " " + + this->message().searchText; + + // highlights + this->parseHighlights(); + + // highlighting incoming whispers if requested per setting + if (this->args.isReceivedWhisper && getSettings()->highlightInlineWhispers) + { + this->message().flags.set(MessageFlag::HighlightedWhisper, true); + this->message().highlightColor = + ColorProvider::instance().color(ColorType::Whisper); + } + + if (this->thread_) + { + auto &img = getResources().buttons.replyThreadDark; + this->emplace( + Image::fromResourcePixmap(img, 0.15), 2, Qt::gray, + MessageElementFlag::ReplyButton) + ->setLink({Link::ViewThread, this->thread_->rootId()}); + } + else + { + auto &img = getResources().buttons.replyDark; + this->emplace( + Image::fromResourcePixmap(img, 0.15), 2, Qt::gray, + MessageElementFlag::ReplyButton) + ->setLink({Link::ReplyToMessage, this->message().id}); + } + + return this->release(); +} + +void MessageBuilder::setThread(std::shared_ptr thread) +{ + this->thread_ = std::move(thread); +} + +void MessageBuilder::setParent(MessagePtr parent) +{ + this->parent_ = std::move(parent); +} + +void MessageBuilder::setMessageOffset(int offset) +{ + this->messageOffset_ = offset; +} + +void MessageBuilder::appendChannelPointRewardMessage( + const ChannelPointReward &reward, bool isMod, bool isBroadcaster) +{ + if (isIgnoredMessage({ + /*.message = */ "", + /*.twitchUserID = */ reward.user.id, + /*.isMod = */ isMod, + /*.isBroadcaster = */ isBroadcaster, + })) + { + return; + } + + this->emplace(); + QString redeemed = "Redeemed"; + QStringList textList; + if (!reward.isUserInputRequired) + { + this->emplace( + reward.user.login, MessageElementFlag::ChannelPointReward, + MessageColor::Text, FontStyle::ChatMediumBold) + ->setLink({Link::UserInfo, reward.user.login}); + redeemed = "redeemed"; + textList.append(reward.user.login); + } + this->emplace(redeemed, + MessageElementFlag::ChannelPointReward); + if (reward.id == "CELEBRATION") + { + const auto emotePtr = + getApp()->getEmotes()->getTwitchEmotes()->getOrCreateEmote( + EmoteId{reward.emoteId}, EmoteName{reward.emoteName}); + this->emplace(emotePtr, + MessageElementFlag::ChannelPointReward, + MessageColor::Text); + } + this->emplace(reward.title, + MessageElementFlag::ChannelPointReward, + MessageColor::Text, FontStyle::ChatMediumBold); + this->emplace( + reward.image, MessageElementFlag::ChannelPointRewardImage); + this->emplace(QString::number(reward.cost), + MessageElementFlag::ChannelPointReward, + MessageColor::Text, FontStyle::ChatMediumBold); + if (reward.isBits) + { + this->emplace( + "bits", MessageElementFlag::ChannelPointReward, MessageColor::Text, + FontStyle::ChatMediumBold); + } + if (reward.isUserInputRequired) + { + this->emplace(MessageElementFlag::ChannelPointReward); + } + + this->message().flags.set(MessageFlag::RedeemedChannelPointReward); + + textList.append({redeemed, reward.title, QString::number(reward.cost)}); + this->message().messageText = textList.join(" "); + this->message().searchText = textList.join(" "); + this->message().loginName = reward.user.login; + + this->message().reward = std::make_shared(reward); +} + +MessagePtr MessageBuilder::makeChannelPointRewardMessage( + const ChannelPointReward &reward, bool isMod, bool isBroadcaster) +{ + MessageBuilder builder; + + builder.appendChannelPointRewardMessage(reward, isMod, isBroadcaster); + + return builder.release(); +} + +MessagePtr MessageBuilder::makeLiveMessage(const QString &channelName, + const QString &channelID, + MessageFlags extraFlags) +{ + MessageBuilder builder; + + builder.emplace(); + builder + .emplace(channelName, MessageElementFlag::Username, + MessageColor::Text, FontStyle::ChatMediumBold) + ->setLink({Link::UserInfo, channelName}); + builder.emplace("is live!", MessageElementFlag::Text, + MessageColor::Text); + auto text = QString("%1 is live!").arg(channelName); + builder.message().messageText = text; + builder.message().searchText = text; + builder.message().id = channelID; + + if (!extraFlags.isEmpty()) + { + builder.message().flags.set(extraFlags); + } + + return builder.release(); +} + +MessagePtr MessageBuilder::makeOfflineSystemMessage(const QString &channelName, + const QString &channelID) +{ + MessageBuilder builder; + builder.emplace(); + builder.message().flags.set(MessageFlag::System); + builder.message().flags.set(MessageFlag::DoNotTriggerNotification); + builder + .emplace(channelName, MessageElementFlag::Username, + MessageColor::System, FontStyle::ChatMediumBold) + ->setLink({Link::UserInfo, channelName}); + builder.emplace("is now offline.", MessageElementFlag::Text, + MessageColor::System); + auto text = QString("%1 is now offline.").arg(channelName); + builder.message().messageText = text; + builder.message().searchText = text; + builder.message().id = channelID; + + return builder.release(); +} + +MessagePtr MessageBuilder::makeHostingSystemMessage(const QString &channelName, + bool hostOn) +{ + MessageBuilder builder; + QString text; + builder.emplace(); + builder.message().flags.set(MessageFlag::System); + builder.message().flags.set(MessageFlag::DoNotTriggerNotification); + if (hostOn) + { + builder.emplace("Now hosting", MessageElementFlag::Text, + MessageColor::System); + builder + .emplace( + channelName + ".", MessageElementFlag::Username, + MessageColor::System, FontStyle::ChatMediumBold) + ->setLink({Link::UserInfo, channelName}); + text = QString("Now hosting %1.").arg(channelName); + } + else + { + builder + .emplace(channelName, MessageElementFlag::Username, + MessageColor::System, + FontStyle::ChatMediumBold) + ->setLink({Link::UserInfo, channelName}); + builder.emplace("has gone offline. Exiting host mode.", + MessageElementFlag::Text, + MessageColor::System); + text = + QString("%1 has gone offline. Exiting host mode.").arg(channelName); + } + builder.message().messageText = text; + builder.message().searchText = text; + return builder.release(); +} + +MessagePtr MessageBuilder::makeDeletionMessageFromIRC( + const MessagePtr &originalMessage) +{ + MessageBuilder builder; + + builder.emplace(); + builder.message().flags.set(MessageFlag::System); + builder.message().flags.set(MessageFlag::DoNotTriggerNotification); + builder.message().flags.set(MessageFlag::Timeout); + // TODO(mm2pl): If or when jumping to a single message gets implemented a link, + // add a link to the originalMessage + builder.emplace("A message from", MessageElementFlag::Text, + MessageColor::System); + builder + .emplace(originalMessage->displayName, + MessageElementFlag::Username, + MessageColor::System, FontStyle::ChatMediumBold) + ->setLink({Link::UserInfo, originalMessage->loginName}); + builder.emplace("was deleted:", MessageElementFlag::Text, + MessageColor::System); + if (originalMessage->messageText.length() > 50) + { + builder + .emplace(originalMessage->messageText.left(50) + "…", + MessageElementFlag::Text, MessageColor::Text) + ->setLink({Link::JumpToMessage, originalMessage->id}); + } + else + { + builder + .emplace(originalMessage->messageText, + MessageElementFlag::Text, MessageColor::Text) + ->setLink({Link::JumpToMessage, originalMessage->id}); + } + builder.message().timeoutUser = "msg:" + originalMessage->id; + + return builder.release(); +} + +MessagePtr MessageBuilder::makeDeletionMessageFromPubSub( + const DeleteAction &action) +{ + MessageBuilder builder; + + builder.emplace(); + builder.message().flags.set(MessageFlag::System); + builder.message().flags.set(MessageFlag::DoNotTriggerNotification); + builder.message().flags.set(MessageFlag::Timeout); + + builder + .emplace(action.source.login, MessageElementFlag::Username, + MessageColor::System, FontStyle::ChatMediumBold) + ->setLink({Link::UserInfo, action.source.login}); + // TODO(mm2pl): If or when jumping to a single message gets implemented a link, + // add a link to the originalMessage + builder.emplace( + "deleted message from", MessageElementFlag::Text, MessageColor::System); + builder + .emplace(action.target.login, MessageElementFlag::Username, + MessageColor::System, FontStyle::ChatMediumBold) + ->setLink({Link::UserInfo, action.target.login}); + builder.emplace("saying:", MessageElementFlag::Text, + MessageColor::System); + if (action.messageText.length() > 50) + { + builder + .emplace(action.messageText.left(50) + "…", + MessageElementFlag::Text, MessageColor::Text) + ->setLink({Link::JumpToMessage, action.messageId}); + } + else + { + builder + .emplace(action.messageText, MessageElementFlag::Text, + MessageColor::Text) + ->setLink({Link::JumpToMessage, action.messageId}); + } + builder.message().timeoutUser = "msg:" + action.messageId; + builder.message().flags.set(MessageFlag::PubSub); + + return builder.release(); +} + +MessagePtr MessageBuilder::makeListOfUsersMessage(QString prefix, + QStringList users, + Channel *channel, + MessageFlags extraFlags) +{ + MessageBuilder builder; + + QString text = prefix + users.join(", "); + + builder.message().messageText = text; + builder.message().searchText = text; + + builder.emplace(); + builder.message().flags.set(MessageFlag::System); + builder.message().flags.set(MessageFlag::DoNotTriggerNotification); + builder.emplace(prefix, MessageElementFlag::Text, + MessageColor::System); + bool isFirst = true; + auto *tc = dynamic_cast(channel); + for (const QString &username : users) + { + if (!isFirst) + { + // this is used to add the ", " after each but the last entry + builder.emplace(",", MessageElementFlag::Text, + MessageColor::System); + } + isFirst = false; + + MessageColor color = MessageColor::System; + + if (tc) + { + if (auto userColor = tc->getUserColor(username); + userColor.isValid()) { - return; + color = MessageColor(userColor); } - if (!tooltipText.isEmpty()) + } + + // TODO: Ensure we make use of display name / username(login name) correctly here + builder + .emplace(username, username, MessageColor::System, + color) + ->setTrailingSpace(false); + } + + if (!extraFlags.isEmpty()) + { + builder.message().flags.set(extraFlags); + } + + return builder.release(); +} + +MessagePtr MessageBuilder::makeListOfUsersMessage( + QString prefix, const std::vector &users, Channel *channel, + MessageFlags extraFlags) +{ + MessageBuilder builder; + + QString text = prefix; + + builder.emplace(); + builder.message().flags.set(MessageFlag::System); + builder.message().flags.set(MessageFlag::DoNotTriggerNotification); + builder.emplace(prefix, MessageElementFlag::Text, + MessageColor::System); + bool isFirst = true; + auto *tc = dynamic_cast(channel); + for (const auto &user : users) + { + if (!isFirst) + { + // this is used to add the ", " after each but the last entry + builder.emplace(",", MessageElementFlag::Text, + MessageColor::System); + text += QString(", %1").arg(user.userName); + } + else + { + text += user.userName; + } + isFirst = false; + + MessageColor color = MessageColor::System; + + if (tc) + { + if (auto userColor = tc->getUserColor(user.userLogin); + userColor.isValid()) { - linkMELowercase->setTooltip(tooltipText); - linkMEOriginal->setTooltip(tooltipText); + color = MessageColor(userColor); } - if (originalLink.value != matchedLink && - !originalLink.value.isEmpty()) + } + + builder + .emplace(user.userName, user.userLogin, + MessageColor::System, color) + ->setTrailingSpace(false); + } + + builder.message().messageText = text; + builder.message().searchText = text; + + if (!extraFlags.isEmpty()) + { + builder.message().flags.set(extraFlags); + } + + return builder.release(); +} + +MessagePtr MessageBuilder::buildHypeChatMessage( + Communi::IrcPrivateMessage *message) +{ + auto levelID = message->tag(u"pinned-chat-paid-level"_s).toString(); + auto currency = message->tag(u"pinned-chat-paid-currency"_s).toString(); + bool okAmount = false; + auto amount = message->tag(u"pinned-chat-paid-amount"_s).toInt(&okAmount); + bool okExponent = false; + auto exponent = + message->tag(u"pinned-chat-paid-exponent"_s).toInt(&okExponent); + if (!okAmount || !okExponent || currency.isEmpty()) + { + return {}; + } + // additionally, there's `pinned-chat-paid-is-system-message` which isn't used by Chatterino. + + QString subtitle; + auto levelIt = HYPE_CHAT_PAID_LEVEL.find(levelID); + if (levelIt != HYPE_CHAT_PAID_LEVEL.end()) + { + const auto &level = levelIt->second; + subtitle = u"Level %1 Hype Chat (%2) "_s.arg(level.numeric) + .arg(formatTime(level.duration)); + } + else + { + subtitle = u"Hype Chat "_s; + } + + // actualAmount = amount * 10^(-exponent) + double actualAmount = std::pow(10.0, double(-exponent)) * double(amount); + subtitle += QLocale::system().toCurrencyString(actualAmount, currency); + + MessageBuilder builder(systemMessage, parseTagString(subtitle), + calculateMessageTime(message).time()); + builder->flags.set(MessageFlag::ElevatedMessage); + return builder.release(); +} + +std::pair MessageBuilder::makeAutomodMessage( + const AutomodAction &action, const QString &channelName) +{ + MessageBuilder builder, builder2; + + // + // Builder for AutoMod message with explanation + builder.message().loginName = "automod"; + builder.message().channelName = channelName; + builder.message().flags.set(MessageFlag::PubSub); + builder.message().flags.set(MessageFlag::Timeout); + builder.message().flags.set(MessageFlag::AutoMod); + builder.message().flags.set(MessageFlag::AutoModOffendingMessageHeader); + + // AutoMod shield badge + builder.emplace(makeAutoModBadge(), + MessageElementFlag::BadgeChannelAuthority); + // AutoMod "username" + builder2.emplace("AutoMod:", MessageElementFlag::Text, + AUTOMOD_USER_COLOR, + FontStyle::ChatMediumBold); + // AutoMod header message + builder.emplace( + ("Held a message for reason: " + action.reason + + ". Allow will post it in chat. "), + MessageElementFlag::Text, MessageColor::Text); + // Allow link button + builder + .emplace("Allow", MessageElementFlag::Text, + MessageColor(QColor("green")), + FontStyle::ChatMediumBold) + ->setLink({Link::AutoModAllow, action.msgID}); + // Deny link button + builder + .emplace(" Deny", MessageElementFlag::Text, + MessageColor(QColor("red")), + FontStyle::ChatMediumBold) + ->setLink({Link::AutoModDeny, action.msgID}); + // ID of message caught by AutoMod + // builder.emplace(action.msgID, MessageElementFlag::Text, + // MessageColor::Text); + auto text1 = + QString("AutoMod: Held a message for reason: %1. Allow will post " + "it in chat. Allow Deny") + .arg(action.reason); + builder.message().messageText = text1; + builder.message().searchText = text1; + + auto message1 = builder.release(); + + // + // Builder for offender's message + builder2.message().channelName = channelName; + builder2 + .emplace("#" + channelName, + MessageElementFlag::ChannelName, + MessageColor::System) + ->setLink({Link::JumpToChannel, channelName}); + builder2.emplace(); + builder2.emplace(); + builder2.message().loginName = action.target.login; + builder2.message().flags.set(MessageFlag::PubSub); + builder2.message().flags.set(MessageFlag::Timeout); + builder2.message().flags.set(MessageFlag::AutoMod); + builder2.message().flags.set(MessageFlag::AutoModOffendingMessage); + + // sender username + builder2.emplace(action.target.displayName + ":", + action.target.login, MessageColor::Text, + action.target.color); + // sender's message caught by AutoMod + builder2.emplace(action.message, MessageElementFlag::Text, + MessageColor::Text); + auto text2 = + QString("%1: %2").arg(action.target.displayName, action.message); + builder2.message().messageText = text2; + builder2.message().searchText = text2; + + auto message2 = builder2.release(); + + // Normally highlights would be checked & triggered during the builder parse steps + // and when the message is added to the channel + // We do this a bit weird since the message comes in from PubSub and not the normal message route + auto [highlighted, highlightResult] = getApp()->getHighlights()->check( + {}, {}, action.target.login, action.message, message2->flags); + if (highlighted) + { + actuallyTriggerHighlights(channelName, highlightResult.playSound, + highlightResult.customSoundUrl, + highlightResult.alert); + } + + return std::make_pair(message1, message2); +} + +MessagePtr MessageBuilder::makeAutomodInfoMessage( + const AutomodInfoAction &action) +{ + auto builder = MessageBuilder(); + QString text("AutoMod: "); + + builder.emplace(); + builder.message().flags.set(MessageFlag::PubSub); + + // AutoMod shield badge + builder.emplace(makeAutoModBadge(), + MessageElementFlag::BadgeChannelAuthority); + // AutoMod "username" + builder.emplace("AutoMod:", MessageElementFlag::Text, + AUTOMOD_USER_COLOR, FontStyle::ChatMediumBold); + switch (action.type) + { + case AutomodInfoAction::OnHold: { + QString info("Hey! Your message is being checked " + "by mods and has not been sent."); + text += info; + builder.emplace(info, MessageElementFlag::Text, + MessageColor::Text); + } + break; + case AutomodInfoAction::Denied: { + QString info("Mods have removed your message."); + text += info; + builder.emplace(info, MessageElementFlag::Text, + MessageColor::Text); + } + break; + case AutomodInfoAction::Approved: { + QString info("Mods have accepted your message."); + text += info; + builder.emplace(info, MessageElementFlag::Text, + MessageColor::Text); + } + break; + } + + builder.message().flags.set(MessageFlag::AutoMod); + builder.message().messageText = text; + builder.message().searchText = text; + + auto message = builder.release(); + + return message; +} + +std::pair MessageBuilder::makeLowTrustUserMessage( + const PubSubLowTrustUsersMessage &action, const QString &channelName, + const TwitchChannel *twitchChannel) +{ + MessageBuilder builder, builder2; + + // Builder for low trust user message with explanation + builder.message().channelName = channelName; + builder.message().flags.set(MessageFlag::PubSub); + builder.message().flags.set(MessageFlag::LowTrustUsers); + + // AutoMod shield badge + builder.emplace(makeAutoModBadge(), + MessageElementFlag::BadgeChannelAuthority); + + // Suspicious user header message + QString prefix = "Suspicious User:"; + builder.emplace(prefix, MessageElementFlag::Text, + MessageColor(QColor("blue")), + FontStyle::ChatMediumBold); + + QString headerMessage; + if (action.treatment == PubSubLowTrustUsersMessage::Treatment::Restricted) + { + headerMessage = "Restricted"; + builder2.message().flags.set(MessageFlag::RestrictedMessage); + } + else + { + headerMessage = "Monitored"; + builder2.message().flags.set(MessageFlag::MonitoredMessage); + } + + if (action.restrictionTypes.has( + PubSubLowTrustUsersMessage::RestrictionType::ManuallyAdded)) + { + headerMessage += " by " + action.updatedByUserLogin; + } + + headerMessage += " at " + action.updatedAt; + + if (action.restrictionTypes.has( + PubSubLowTrustUsersMessage::RestrictionType::DetectedBanEvader)) + { + QString evader; + if (action.evasionEvaluation == + PubSubLowTrustUsersMessage::EvasionEvaluation::LikelyEvader) + { + evader = "likely"; + } + else + { + evader = "possible"; + } + + headerMessage += ". Detected as " + evader + " ban evader"; + } + + if (action.restrictionTypes.has( + PubSubLowTrustUsersMessage::RestrictionType::BannedInSharedChannel)) + { + headerMessage += ". Banned in " + + QString::number(action.sharedBanChannelIDs.size()) + + " shared channels"; + } + + builder.emplace(headerMessage, MessageElementFlag::Text, + MessageColor::Text); + builder.message().messageText = prefix + " " + headerMessage; + builder.message().searchText = prefix + " " + headerMessage; + + auto message1 = builder.release(); + + // + // Builder for offender's message + builder2.message().channelName = channelName; + builder2 + .emplace("#" + channelName, + MessageElementFlag::ChannelName, + MessageColor::System) + ->setLink({Link::JumpToChannel, channelName}); + builder2.emplace(); + builder2.emplace(); + builder2.message().loginName = action.suspiciousUserLogin; + builder2.message().flags.set(MessageFlag::PubSub); + builder2.message().flags.set(MessageFlag::LowTrustUsers); + + // sender badges + appendBadges(&builder2, action.senderBadges, {}, twitchChannel); + + // sender username + builder2.emplace( + action.suspiciousUserDisplayName + ":", action.suspiciousUserLogin, + MessageColor::Text, action.suspiciousUserColor); + + // sender's message caught by AutoMod + for (const auto &fragment : action.fragments) + { + if (fragment.emoteID.isEmpty()) + { + builder2.emplace( + fragment.text, MessageElementFlag::Text, MessageColor::Text); + } + else + { + const auto emotePtr = + getApp()->getEmotes()->getTwitchEmotes()->getOrCreateEmote( + EmoteId{fragment.emoteID}, EmoteName{fragment.text}); + builder2.emplace( + emotePtr, MessageElementFlag::TwitchEmote, MessageColor::Text); + } + } + + auto text = + QString("%1: %2").arg(action.suspiciousUserDisplayName, action.text); + builder2.message().messageText = text; + builder2.message().searchText = text; + + auto message2 = builder2.release(); + + return std::make_pair(message1, message2); +} + +MessagePtr MessageBuilder::makeLowTrustUpdateMessage( + const PubSubLowTrustUsersMessage &action) +{ + /** + * Known issues: + * - Non-Twitch badges are not shown + * - Non-Twitch emotes are not shown + */ + + MessageBuilder builder; + builder.emplace(); + builder.message().flags.set(MessageFlag::System); + builder.message().flags.set(MessageFlag::PubSub); + builder.message().flags.set(MessageFlag::DoNotTriggerNotification); + + builder + .emplace(action.updatedByUserDisplayName, + MessageElementFlag::Username, + MessageColor::System, FontStyle::ChatMediumBold) + ->setLink({Link::UserInfo, action.updatedByUserLogin}); + + assert(action.treatment != PubSubLowTrustUsersMessage::Treatment::INVALID); + switch (action.treatment) + { + case PubSubLowTrustUsersMessage::Treatment::NoTreatment: { + builder.emplace("removed", MessageElementFlag::Text, + MessageColor::System); + builder + .emplace(action.suspiciousUserDisplayName, + MessageElementFlag::Username, + MessageColor::System, + FontStyle::ChatMediumBold) + ->setLink({Link::UserInfo, action.suspiciousUserLogin}); + builder.emplace("from the suspicious user list.", + MessageElementFlag::Text, + MessageColor::System); + } + break; + + case PubSubLowTrustUsersMessage::Treatment::ActiveMonitoring: { + builder.emplace("added", MessageElementFlag::Text, + MessageColor::System); + builder + .emplace(action.suspiciousUserDisplayName, + MessageElementFlag::Username, + MessageColor::System, + FontStyle::ChatMediumBold) + ->setLink({Link::UserInfo, action.suspiciousUserLogin}); + builder.emplace("as a monitored suspicious chatter.", + MessageElementFlag::Text, + MessageColor::System); + } + break; + + case PubSubLowTrustUsersMessage::Treatment::Restricted: { + builder.emplace("added", MessageElementFlag::Text, + MessageColor::System); + builder + .emplace(action.suspiciousUserDisplayName, + MessageElementFlag::Username, + MessageColor::System, + FontStyle::ChatMediumBold) + ->setLink({Link::UserInfo, action.suspiciousUserLogin}); + builder.emplace("as a restricted suspicious chatter.", + MessageElementFlag::Text, + MessageColor::System); + } + break; + + default: + qCDebug(chatterinoTwitch) << "Unexpected suspicious treatment: " + << action.treatmentString; + break; + } + + return builder.release(); +} + +std::unordered_map MessageBuilder::parseBadgeInfoTag( + const QVariantMap &tags) +{ + std::unordered_map infoMap; + + auto infoIt = tags.constFind("badge-info"); + if (infoIt == tags.end()) + { + return infoMap; + } + + auto info = infoIt.value().toString().split(',', Qt::SkipEmptyParts); + + for (const QString &badge : info) + { + infoMap.emplace(slashKeyValue(badge)); + } + + return infoMap; +} + +std::vector MessageBuilder::parseBadgeTag(const QVariantMap &tags) +{ + std::vector b; + + auto badgesIt = tags.constFind("badges"); + if (badgesIt == tags.end()) + { + return b; + } + + auto badges = badgesIt.value().toString().split(',', Qt::SkipEmptyParts); + + for (const QString &badge : badges) + { + if (!badge.contains('/')) + { + continue; + } + + auto pair = slashKeyValue(badge); + b.emplace_back(Badge{pair.first, pair.second}); + } + + return b; +} + +std::vector MessageBuilder::parseTwitchEmotes( + const QVariantMap &tags, const QString &originalMessage, int messageOffset) +{ + // Twitch emotes + std::vector twitchEmotes; + + auto emotesTag = tags.find("emotes"); + + if (emotesTag == tags.end()) + { + return twitchEmotes; + } + + QStringList emoteString = emotesTag.value().toString().split('/'); + std::vector correctPositions; + for (int i = 0; i < originalMessage.size(); ++i) + { + if (!originalMessage.at(i).isLowSurrogate()) + { + correctPositions.push_back(i); + } + } + for (const QString &emote : emoteString) + { + appendTwitchEmoteOccurrences(emote, twitchEmotes, correctPositions, + originalMessage, messageOffset); + } + + return twitchEmotes; +} + +void MessageBuilder::processIgnorePhrases( + const std::vector &phrases, QString &originalMessage, + std::vector &twitchEmotes) +{ + using SizeType = QString::size_type; + + auto removeEmotesInRange = [&twitchEmotes](SizeType pos, SizeType len) { + // all emotes outside the range come before `it` + // all emotes in the range start at `it` + auto it = std::partition( + twitchEmotes.begin(), twitchEmotes.end(), + [pos, len](const auto &item) { + // returns true for emotes outside the range + return !((item.start >= pos) && item.start < (pos + len)); + }); + std::vector emotesInRange(it, + twitchEmotes.end()); + twitchEmotes.erase(it, twitchEmotes.end()); + return emotesInRange; + }; + + auto shiftIndicesAfter = [&twitchEmotes](int pos, int by) { + for (auto &item : twitchEmotes) + { + auto &index = item.start; + if (index >= pos) { - linkMELowercase->setLink(originalLink)->updateLink(); - linkMEOriginal->setLink(originalLink)->updateLink(); + index += by; + item.end += by; } - linkMELowercase->setThumbnail(thumbnail); - linkMELowercase->setThumbnailType( - MessageElement::ThumbnailType::Link_Thumbnail); - linkMEOriginal->setThumbnail(thumbnail); - linkMEOriginal->setThumbnailType( - MessageElement::ThumbnailType::Link_Thumbnail); - }); + } + }; + + auto addReplEmotes = [&twitchEmotes](const IgnorePhrase &phrase, + const auto &midrepl, + SizeType startIndex) { + if (!phrase.containsEmote()) + { + return; + } + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + auto words = midrepl.tokenize(u' '); +#else + auto words = midrepl.split(' '); +#endif + SizeType pos = 0; + for (const auto &word : words) + { + for (const auto &emote : phrase.getEmotes()) + { + if (word == emote.first.string) + { + if (emote.second == nullptr) + { + qCDebug(chatterinoTwitch) + << "emote null" << emote.first.string; + } + twitchEmotes.push_back(TwitchEmoteOccurrence{ + static_cast(startIndex + pos), + static_cast(startIndex + pos + + emote.first.string.length()), + emote.second, + emote.first, + }); + } + } + pos += word.length() + 1; + } + }; + + auto replaceMessageAt = [&](const IgnorePhrase &phrase, SizeType from, + SizeType length, const QString &replacement) { + auto removedEmotes = removeEmotesInRange(from, length); + originalMessage.replace(from, length, replacement); + auto wordStart = from; + while (wordStart > 0) + { + if (originalMessage[wordStart - 1] == ' ') + { + break; + } + --wordStart; + } + auto wordEnd = from + replacement.length(); + while (wordEnd < originalMessage.length()) + { + if (originalMessage[wordEnd] == ' ') + { + break; + } + ++wordEnd; + } + + shiftIndicesAfter(static_cast(from + length), + static_cast(replacement.length() - length)); + + auto midExtendedRef = + QStringView{originalMessage}.mid(wordStart, wordEnd - wordStart); + + for (auto &emote : removedEmotes) + { + if (emote.ptr == nullptr) + { + qCDebug(chatterinoTwitch) + << "Invalid emote occurrence" << emote.name.string; + continue; + } + QRegularExpression emoteregex( + "\\b" + emote.name.string + "\\b", + QRegularExpression::UseUnicodePropertiesOption); +#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) + auto match = emoteregex.matchView(midExtendedRef); +#else + auto match = emoteregex.match(midExtendedRef); +#endif + if (match.hasMatch()) + { + emote.start = static_cast(from + match.capturedStart()); + emote.end = static_cast(from + match.capturedEnd()); + twitchEmotes.push_back(std::move(emote)); + } + } + + addReplEmotes(phrase, midExtendedRef, wordStart); + }; + + for (const auto &phrase : phrases) + { + if (phrase.isBlock()) + { + continue; + } + const auto &pattern = phrase.getPattern(); + if (pattern.isEmpty()) + { + continue; + } + if (phrase.isRegex()) + { + const auto ®ex = phrase.getRegex(); + if (!regex.isValid()) + { + continue; + } + + QRegularExpressionMatch match; + size_t iterations = 0; + SizeType from = 0; + while ((from = originalMessage.indexOf(regex, from, &match)) != -1) + { + auto replacement = phrase.getReplace(); + if (regex.captureCount() > 0) + { + replacement = makeRegexReplacement(originalMessage, regex, + match, replacement); + } + + replaceMessageAt(phrase, from, match.capturedLength(), + replacement); + from += phrase.getReplace().length(); + iterations++; + if (iterations >= 128) + { + originalMessage = + u"Too many replacements - check your ignores!"_s; + return; + } + } + + continue; + } + + SizeType from = 0; + while ((from = originalMessage.indexOf(pattern, from, + phrase.caseSensitivity())) != -1) + { + replaceMessageAt(phrase, from, pattern.length(), + phrase.getReplace()); + from += phrase.getReplace().length(); + } + } +} + +void MessageBuilder::addTextOrEmoji(EmotePtr emote) +{ + this->emplace(emote, MessageElementFlag::EmojiAll); +} + +void MessageBuilder::addTextOrEmoji(const QString &string_) +{ + auto string = QString(string_); + + if (this->hasBits_ && this->tryParseCheermote(string)) + { + // This string was parsed as a cheermote + return; + } + + // TODO: Implement ignored emotes + // Format of ignored emotes: + // Emote name: "forsenPuke" - if string in ignoredEmotes + // Will match emote regardless of source (i.e. bttv, ffz) + // Emote source + name: "bttv:nyanPls" + if (this->tryAppendEmote({string})) + { + // Successfully appended an emote + return; + } + + // Actually just text + auto link = linkparser::parse(string); + auto textColor = this->textColor_; + + if (link) + { + this->addLink(*link, string); + return; + } + + if (string.startsWith('@')) + { + auto match = mentionRegex.match(string); + // Only treat as @mention if valid username + if (match.hasMatch()) + { + QString username = match.captured(1); + auto originalTextColor = textColor; + + if (this->twitchChannel != nullptr) + { + if (auto userColor = + this->twitchChannel->getUserColor(username); + userColor.isValid()) + { + textColor = userColor; + } + } + + auto prefixedUsername = '@' + username; + auto remainder = string.remove(prefixedUsername); + this->emplace(prefixedUsername, username, + originalTextColor, textColor) + ->setTrailingSpace(remainder.isEmpty()); + + if (!remainder.isEmpty()) + { + this->emplace(remainder, MessageElementFlag::Text, + originalTextColor); + } + + return; + } + } + + if (this->twitchChannel != nullptr && getSettings()->findAllUsernames) + { + auto match = allUsernamesMentionRegex.match(string); + QString username = match.captured(1); + + if (match.hasMatch() && + this->twitchChannel->accessChatters()->contains(username)) + { + auto originalTextColor = textColor; + + if (auto userColor = this->twitchChannel->getUserColor(username); + userColor.isValid()) + { + textColor = userColor; + } + + auto remainder = string.remove(username); + this->emplace(username, username, originalTextColor, + textColor) + ->setTrailingSpace(remainder.isEmpty()); + + if (!remainder.isEmpty()) + { + this->emplace(remainder, MessageElementFlag::Text, + originalTextColor); + } + + return; + } + } + + this->emplace(string, MessageElementFlag::Text, textColor); +} + +bool MessageBuilder::isEmpty() const +{ + return this->message_->elements.empty(); +} + +MessageElement &MessageBuilder::back() +{ + assert(!this->isEmpty()); + return *this->message().elements.back(); +} + +std::unique_ptr MessageBuilder::releaseBack() +{ + assert(!this->isEmpty()); + + auto ptr = std::move(this->message().elements.back()); + this->message().elements.pop_back(); + return ptr; } TextElement *MessageBuilder::emplaceSystemTextAndUpdate(const QString &text, @@ -586,4 +2569,661 @@ TextElement *MessageBuilder::emplaceSystemTextAndUpdate(const QString &text, MessageColor::System); } +void MessageBuilder::parse() +{ + this->parseUsernameColor(); + + if (this->action_) + { + this->textColor_ = this->usernameColor_; + this->message().flags.set(MessageFlag::Action); + } + + this->parseUsername(); + + this->message().flags.set(MessageFlag::Collapsed); +} + +void MessageBuilder::parseUsernameColor() +{ + const auto *userData = getApp()->getUserData(); + assert(userData != nullptr); + + if (const auto &user = userData->getUser(this->userId_)) + { + if (user->color) + { + this->usernameColor_ = user->color.value(); + return; + } + } + + const auto iterator = this->tags.find("color"); + if (iterator != this->tags.end()) + { + if (const auto color = iterator.value().toString(); !color.isEmpty()) + { + this->usernameColor_ = QColor(color); + this->message().usernameColor = this->usernameColor_; + return; + } + } + + if (getSettings()->colorizeNicknames && this->tags.contains("user-id")) + { + this->usernameColor_ = + getRandomColor(this->tags.value("user-id").toString()); + this->message().usernameColor = this->usernameColor_; + } +} + +void MessageBuilder::parseUsername() +{ + // username + this->userName = this->ircMessage->nick(); + + this->message().loginName = this->userName; + + if (this->userName.isEmpty() || this->args.trimSubscriberUsername) + { + this->userName = this->tags.value(QLatin1String("login")).toString(); + } + + // display name + // auto displayNameVariant = this->tags.value("display-name"); + // if (displayNameVariant.isValid()) { + // this->userName = displayNameVariant.toString() + " (" + + // this->userName + ")"; + // } + + this->message().loginName = this->userName; + if (this->twitchChannel != nullptr) + { + this->twitchChannel->setUserColor(this->userName, this->usernameColor_); + } + + // Update current user color if this is our message + auto currentUser = getApp()->getAccounts()->twitch.getCurrent(); + if (this->ircMessage->nick() == currentUser->getUserName()) + { + currentUser->setColor(this->usernameColor_); + } +} + +void MessageBuilder::parseMessageID() +{ + auto iterator = this->tags.find("id"); + + if (iterator != this->tags.end()) + { + this->message().id = iterator.value().toString(); + } +} + +void MessageBuilder::parseRoomID() +{ + if (this->twitchChannel == nullptr) + { + return; + } + + auto iterator = this->tags.find("room-id"); + + if (iterator != std::end(this->tags)) + { + this->roomID_ = iterator.value().toString(); + + if (this->twitchChannel->roomId().isEmpty()) + { + this->twitchChannel->setRoomId(this->roomID_); + } + } +} + +void MessageBuilder::parseThread() +{ + if (this->thread_) + { + // set references + this->message().replyThread = this->thread_; + this->message().replyParent = this->parent_; + this->thread_->addToThread(this->weakOf()); + + // enable reply flag + this->message().flags.set(MessageFlag::ReplyMessage); + + MessagePtr threadRoot; + if (!this->parent_) + { + threadRoot = this->thread_->root(); + } + else + { + threadRoot = this->parent_; + } + + QString usernameText = + stylizeUsername(threadRoot->loginName, *threadRoot); + + this->emplace(); + + // construct reply elements + this->emplace( + "Replying to", MessageElementFlag::RepliedMessage, + MessageColor::System, FontStyle::ChatMediumSmall) + ->setLink({Link::ViewThread, this->thread_->rootId()}); + + this->emplace( + "@" + usernameText + + (threadRoot->flags.has(MessageFlag::Action) ? "" : ":"), + MessageElementFlag::RepliedMessage, threadRoot->usernameColor, + FontStyle::ChatMediumSmall) + ->setLink({Link::UserInfo, threadRoot->displayName}); + + MessageColor color = MessageColor::Text; + if (threadRoot->flags.has(MessageFlag::Action)) + { + color = threadRoot->usernameColor; + } + this->emplace( + threadRoot->messageText, + MessageElementFlags({MessageElementFlag::RepliedMessage, + MessageElementFlag::Text}), + color, FontStyle::ChatMediumSmall) + ->setLink({Link::ViewThread, this->thread_->rootId()}); + } + else if (this->tags.find("reply-parent-msg-id") != this->tags.end()) + { + // Message is a reply but we couldn't find the original message. + // Render the message using the additional reply tags + + auto replyDisplayName = this->tags.find("reply-parent-display-name"); + auto replyBody = this->tags.find("reply-parent-msg-body"); + + if (replyDisplayName != this->tags.end() && + replyBody != this->tags.end()) + { + QString body; + + this->emplace(); + this->emplace( + "Replying to", MessageElementFlag::RepliedMessage, + MessageColor::System, FontStyle::ChatMediumSmall); + + if (this->isIgnoredReply()) + { + body = QString("[Blocked user]"); + } + else + { + auto name = replyDisplayName->toString(); + body = parseTagString(replyBody->toString()); + + this->emplace( + "@" + name + ":", MessageElementFlag::RepliedMessage, + this->textColor_, FontStyle::ChatMediumSmall) + ->setLink({Link::UserInfo, name}); + } + + this->emplace( + body, + MessageElementFlags({MessageElementFlag::RepliedMessage, + MessageElementFlag::Text}), + this->textColor_, FontStyle::ChatMediumSmall); + } + } +} + +void MessageBuilder::parseHighlights() +{ + if (getSettings()->isBlacklistedUser(this->message().loginName)) + { + // Do nothing. We ignore highlights from this user. + return; + } + + auto badges = parseBadgeTag(this->tags); + auto [highlighted, highlightResult] = getApp()->getHighlights()->check( + this->args, badges, this->message().loginName, this->originalMessage_, + this->message().flags); + + if (!highlighted) + { + return; + } + + // This message triggered one or more highlights, act upon the highlight result + + this->message().flags.set(MessageFlag::Highlighted); + + this->highlightAlert_ = highlightResult.alert; + + this->highlightSound_ = highlightResult.playSound; + this->highlightSoundCustomUrl_ = highlightResult.customSoundUrl; + + this->message().highlightColor = highlightResult.color; + + if (highlightResult.showInMentions) + { + this->message().flags.set(MessageFlag::ShowInMentions); + } +} + +void MessageBuilder::appendChannelName() +{ + QString channelName("#" + this->channel->getName()); + Link link(Link::JumpToChannel, this->channel->getName()); + + this->emplace(channelName, MessageElementFlag::ChannelName, + MessageColor::System) + ->setLink(link); +} + +void MessageBuilder::appendUsername() +{ + auto *app = getApp(); + + QString username = this->userName; + this->message().loginName = username; + QString localizedName; + + auto iterator = this->tags.find("display-name"); + if (iterator != this->tags.end()) + { + QString displayName = + parseTagString(iterator.value().toString()).trimmed(); + + if (QString::compare(displayName, this->userName, + Qt::CaseInsensitive) == 0) + { + username = displayName; + + this->message().displayName = displayName; + } + else + { + localizedName = displayName; + + this->message().displayName = username; + this->message().localizedName = displayName; + } + } + + QString usernameText = stylizeUsername(username, this->message()); + + if (this->args.isSentWhisper) + { + // TODO(pajlada): Re-implement + // userDisplayString += + // IrcManager::instance().getUser().getUserName(); + } + else if (this->args.isReceivedWhisper) + { + // Sender username + this->emplace(usernameText, MessageElementFlag::Username, + this->usernameColor_, + FontStyle::ChatMediumBold) + ->setLink({Link::UserWhisper, this->message().displayName}); + + auto currentUser = app->getAccounts()->twitch.getCurrent(); + + // Separator + this->emplace("->", MessageElementFlag::Username, + MessageColor::System, FontStyle::ChatMedium); + + QColor selfColor = currentUser->color(); + MessageColor selfMsgColor = + selfColor.isValid() ? selfColor : MessageColor::System; + + // Your own username + this->emplace(currentUser->getUserName() + ":", + MessageElementFlag::Username, selfMsgColor, + FontStyle::ChatMediumBold); + } + else + { + if (!this->action_) + { + usernameText += ":"; + } + + this->emplace(usernameText, MessageElementFlag::Username, + this->usernameColor_, + FontStyle::ChatMediumBold) + ->setLink({Link::UserInfo, this->message().displayName}); + } +} + +Outcome MessageBuilder::tryAppendEmote(const EmoteName &name) +{ + auto *app = getApp(); + + const auto *globalBttvEmotes = app->getBttvEmotes(); + const auto *globalFfzEmotes = app->getFfzEmotes(); + const auto *globalSeventvEmotes = app->getSeventvEmotes(); + + auto flags = MessageElementFlags(); + auto emote = std::optional{}; + bool zeroWidth = false; + + // Emote order: + // - FrankerFaceZ Channel + // - BetterTTV Channel + // - 7TV Channel + // - FrankerFaceZ Global + // - BetterTTV Global + // - 7TV Global + if (this->twitchChannel && (emote = this->twitchChannel->ffzEmote(name))) + { + flags = MessageElementFlag::FfzEmote; + } + else if (this->twitchChannel && + (emote = this->twitchChannel->bttvEmote(name))) + { + flags = MessageElementFlag::BttvEmote; + } + else if (this->twitchChannel != nullptr && + (emote = this->twitchChannel->seventvEmote(name))) + { + flags = MessageElementFlag::SevenTVEmote; + zeroWidth = emote.value()->zeroWidth; + } + else if ((emote = globalFfzEmotes->emote(name))) + { + flags = MessageElementFlag::FfzEmote; + } + else if ((emote = globalBttvEmotes->emote(name))) + { + flags = MessageElementFlag::BttvEmote; + zeroWidth = zeroWidthEmotes.contains(name.string); + } + else if ((emote = globalSeventvEmotes->globalEmote(name))) + { + flags = MessageElementFlag::SevenTVEmote; + zeroWidth = emote.value()->zeroWidth; + } + + if (emote) + { + if (zeroWidth && getSettings()->enableZeroWidthEmotes && + !this->isEmpty()) + { + // Attempt to merge current zero-width emote into any previous emotes + auto *asEmote = dynamic_cast(&this->back()); + if (asEmote) + { + // Make sure to access asEmote before taking ownership when releasing + auto baseEmote = asEmote->getEmote(); + // Need to remove EmoteElement and replace with LayeredEmoteElement + auto baseEmoteElement = this->releaseBack(); + + std::vector layers = { + {baseEmote, baseEmoteElement->getFlags()}, {*emote, flags}}; + this->emplace( + std::move(layers), baseEmoteElement->getFlags() | flags, + this->textColor_); + return Success; + } + + auto *asLayered = + dynamic_cast(&this->back()); + if (asLayered) + { + asLayered->addEmoteLayer({*emote, flags}); + asLayered->addFlags(flags); + return Success; + } + + // No emote to merge with, just show as regular emote + } + + this->emplace(*emote, flags, this->textColor_); + return Success; + } + + return Failure; +} + +void MessageBuilder::addWords( + const QStringList &words, + const std::vector &twitchEmotes) +{ + // cursor currently indicates what character index we're currently operating in the full list of words + int cursor = 0; + auto currentTwitchEmoteIt = twitchEmotes.begin(); + + for (auto word : words) + { + if (word.isEmpty()) + { + cursor++; + continue; + } + + while (doesWordContainATwitchEmote(cursor, word, twitchEmotes, + currentTwitchEmoteIt)) + { + const auto ¤tTwitchEmote = *currentTwitchEmoteIt; + + if (currentTwitchEmote.start == cursor) + { + // This emote exists right at the start of the word! + this->emplace(currentTwitchEmote.ptr, + MessageElementFlag::TwitchEmote, + this->textColor_); + + auto len = currentTwitchEmote.name.string.length(); + cursor += len; + word = word.mid(len); + + ++currentTwitchEmoteIt; + + if (word.isEmpty()) + { + // space + cursor += 1; + break; + } + else + { + this->message().elements.back()->setTrailingSpace(false); + } + + continue; + } + + // Emote is not at the start + + // 1. Add text before the emote + QString preText = word.left(currentTwitchEmote.start - cursor); + for (auto &variant : + getApp()->getEmotes()->getEmojis()->parse(preText)) + { + boost::apply_visitor( + [&](auto &&arg) { + this->addTextOrEmoji(arg); + }, + variant); + } + + cursor += preText.size(); + + word = word.mid(preText.size()); + } + + if (word.isEmpty()) + { + continue; + } + + // split words + for (auto &variant : getApp()->getEmotes()->getEmojis()->parse(word)) + { + boost::apply_visitor( + [&](auto &&arg) { + this->addTextOrEmoji(arg); + }, + variant); + } + + cursor += word.size() + 1; + } +} + +void MessageBuilder::appendTwitchBadges() +{ + if (this->twitchChannel == nullptr) + { + return; + } + + auto badgeInfos = MessageBuilder::parseBadgeInfoTag(this->tags); + auto badges = parseBadgeTag(this->tags); + appendBadges(this, badges, badgeInfos, this->twitchChannel); +} + +void MessageBuilder::appendChatterinoBadges() +{ + if (auto badge = getApp()->getChatterinoBadges()->getBadge({this->userId_})) + { + this->emplace(*badge, + MessageElementFlag::BadgeChatterino); + } +} + +void MessageBuilder::appendFfzBadges() +{ + for (const auto &badge : + getApp()->getFfzBadges()->getUserBadges({this->userId_})) + { + this->emplace( + badge.emote, MessageElementFlag::BadgeFfz, badge.color); + } + + if (this->twitchChannel == nullptr) + { + return; + } + + for (const auto &badge : + this->twitchChannel->ffzChannelBadges(this->userId_)) + { + this->emplace( + badge.emote, MessageElementFlag::BadgeFfz, badge.color); + } +} + +void MessageBuilder::appendSeventvBadges() +{ + if (auto badge = getApp()->getSeventvBadges()->getBadge({this->userId_})) + { + this->emplace(*badge, MessageElementFlag::BadgeSevenTV); + } +} + +Outcome MessageBuilder::tryParseCheermote(const QString &string) +{ + if (this->bitsLeft == 0) + { + return Failure; + } + + auto cheerOpt = this->twitchChannel->cheerEmote(string); + + if (!cheerOpt) + { + return Failure; + } + + auto &cheerEmote = *cheerOpt; + auto match = cheerEmote.regex.match(string); + + if (!match.hasMatch()) + { + return Failure; + } + + int cheerValue = match.captured(1).toInt(); + + if (getSettings()->stackBits) + { + if (this->bitsStacked) + { + return Success; + } + if (cheerEmote.staticEmote) + { + this->emplace(cheerEmote.staticEmote, + MessageElementFlag::BitsStatic, + this->textColor_); + } + if (cheerEmote.animatedEmote) + { + this->emplace(cheerEmote.animatedEmote, + MessageElementFlag::BitsAnimated, + this->textColor_); + } + if (cheerEmote.color != QColor()) + { + this->emplace(QString::number(this->bitsLeft), + MessageElementFlag::BitsAmount, + cheerEmote.color); + } + this->bitsStacked = true; + return Success; + } + + if (this->bitsLeft >= cheerValue) + { + this->bitsLeft -= cheerValue; + } + else + { + QString newString = string; + newString.chop(QString::number(cheerValue).length()); + newString += QString::number(cheerValue - this->bitsLeft); + + return tryParseCheermote(newString); + } + + if (cheerEmote.staticEmote) + { + this->emplace(cheerEmote.staticEmote, + MessageElementFlag::BitsStatic, + this->textColor_); + } + if (cheerEmote.animatedEmote) + { + this->emplace(cheerEmote.animatedEmote, + MessageElementFlag::BitsAnimated, + this->textColor_); + } + if (cheerEmote.color != QColor()) + { + this->emplace(match.captured(1), + MessageElementFlag::BitsAmount, + cheerEmote.color); + } + + return Success; +} + +bool MessageBuilder::shouldAddModerationElements() const +{ + if (this->senderIsBroadcaster) + { + // You cannot timeout the broadcaster + return false; + } + + if (this->tags.value("user-type").toString() == "mod" && + !this->args.isStaffOrBroadcaster) + { + // You cannot timeout moderators UNLESS you are Twitch Staff or the broadcaster of the channel + return false; + } + + return true; +} + } // namespace chatterino diff --git a/src/messages/MessageBuilder.hpp b/src/messages/MessageBuilder.hpp index c217cf1ed..996595a8e 100644 --- a/src/messages/MessageBuilder.hpp +++ b/src/messages/MessageBuilder.hpp @@ -1,32 +1,82 @@ #pragma once -#include "messages/MessageElement.hpp" +#include "common/Aliases.hpp" +#include "common/Outcome.hpp" +#include "messages/MessageColor.hpp" +#include "messages/MessageFlag.hpp" +#include "providers/twitch/pubsubmessages/LowTrustUsers.hpp" +#include #include +#include +#include +#include + #include +#include +#include +#include #include namespace chatterino { + struct BanAction; struct UnbanAction; +struct WarnAction; +struct RaidAction; +struct UnraidAction; struct AutomodAction; struct AutomodUserAction; struct AutomodInfoAction; struct Message; using MessagePtr = std::shared_ptr; +class MessageElement; +class TextElement; +struct Emote; +using EmotePtr = std::shared_ptr; + +class Channel; +class TwitchChannel; +class MessageThread; +class IgnorePhrase; +struct HelixVip; +using HelixModerator = HelixVip; +struct ChannelPointReward; +struct DeleteAction; + +namespace linkparser { + struct Parsed; +} // namespace linkparser + struct SystemMessageTag { }; struct TimeoutMessageTag { }; +struct LiveUpdatesUpdateEmoteMessageTag { +}; +struct LiveUpdatesRemoveEmoteMessageTag { +}; +struct LiveUpdatesAddEmoteMessageTag { +}; +struct LiveUpdatesUpdateEmoteSetMessageTag { +}; +struct ImageUploaderResultTag { +}; + const SystemMessageTag systemMessage{}; const TimeoutMessageTag timeoutMessage{}; +const LiveUpdatesUpdateEmoteMessageTag liveUpdatesUpdateEmoteMessage{}; +const LiveUpdatesRemoveEmoteMessageTag liveUpdatesRemoveEmoteMessage{}; +const LiveUpdatesAddEmoteMessageTag liveUpdatesAddEmoteMessage{}; +const LiveUpdatesUpdateEmoteSetMessageTag liveUpdatesUpdateEmoteSetMessage{}; + +// This signifies that you want to construct a message containing the result of +// a successful image upload. +const ImageUploaderResultTag imageUploaderResultMessage{}; MessagePtr makeSystemMessage(const QString &text); MessagePtr makeSystemMessage(const QString &text, const QTime &time); -std::pair makeAutomodMessage( - const AutomodAction &action); -MessagePtr makeAutomodInfoMessage(const AutomodInfoAction &action); struct MessageParseArgs { bool disablePingSounds = false; @@ -38,10 +88,36 @@ struct MessageParseArgs { QString channelPointRewardId = ""; }; +struct TwitchEmoteOccurrence { + int start; + int end; + EmotePtr ptr; + EmoteName name; + + bool operator==(const TwitchEmoteOccurrence &other) const + { + return std::tie(this->start, this->end, this->ptr, this->name) == + std::tie(other.start, other.end, other.ptr, other.name); + } +}; + class MessageBuilder { public: + /// Build a message without a base IRC message. MessageBuilder(); + + /// Build a message based on an incoming IRC PRIVMSG + explicit MessageBuilder(Channel *_channel, + const Communi::IrcPrivateMessage *_ircMessage, + const MessageParseArgs &_args); + + /// Build a message based on an incoming IRC message (e.g. notice) + explicit MessageBuilder(Channel *_channel, + const Communi::IrcMessage *_ircMessage, + const MessageParseArgs &_args, QString content, + bool isAction); + MessageBuilder(SystemMessageTag, const QString &text, const QTime &time = QTime::currentTime()); MessageBuilder(TimeoutMessageTag, const QString &timeoutUser, @@ -52,8 +128,43 @@ public: const QTime &time = QTime::currentTime()); MessageBuilder(const BanAction &action, uint32_t count = 1); MessageBuilder(const UnbanAction &action); + MessageBuilder(const WarnAction &action); + MessageBuilder(const RaidAction &action); + MessageBuilder(const UnraidAction &action); MessageBuilder(const AutomodUserAction &action); - virtual ~MessageBuilder() = default; + + MessageBuilder(LiveUpdatesAddEmoteMessageTag, const QString &platform, + const QString &actor, + const std::vector &emoteNames); + MessageBuilder(LiveUpdatesRemoveEmoteMessageTag, const QString &platform, + const QString &actor, + const std::vector &emoteNames); + MessageBuilder(LiveUpdatesUpdateEmoteMessageTag, const QString &platform, + const QString &actor, const QString &emoteName, + const QString &oldEmoteName); + MessageBuilder(LiveUpdatesUpdateEmoteSetMessageTag, const QString &platform, + const QString &actor, const QString &emoteSetName); + + /** + * "Your image has been uploaded to %1[ (Deletion link: %2)]." + * or "Your image has been uploaded to %1 %2. %3 left. " + * "Please wait until all of them are uploaded. " + * "About %4 seconds left." + */ + MessageBuilder(ImageUploaderResultTag, const QString &imageLink, + const QString &deletionLink, size_t imagesStillQueued = 0, + size_t secondsLeft = 0); + + MessageBuilder(const MessageBuilder &) = delete; + MessageBuilder(MessageBuilder &&) = delete; + MessageBuilder &operator=(const MessageBuilder &) = delete; + MessageBuilder &operator=(MessageBuilder &&) = delete; + + ~MessageBuilder() = default; + + QString userName; + + TwitchChannel *twitchChannel = nullptr; Message *operator->(); Message &message(); @@ -61,14 +172,10 @@ public: std::weak_ptr weakOf(); void append(std::unique_ptr element); - QString matchLink(const QString &string); - void addLink(const QString &origLink, const QString &matchedLink); + void addLink(const linkparser::Parsed &parsedLink, const QString &source); template - // clang-format off - // clang-format can be enabled once clang-format v11+ has been installed in CI T *emplace(Args &&...args) - // clang-format on { static_assert(std::is_base_of::value, "T must extend MessageElement"); @@ -79,7 +186,77 @@ public: return pointer; } -private: + [[nodiscard]] bool isIgnored() const; + bool isIgnoredReply() const; + void triggerHighlights(); + MessagePtr build(); + + void setThread(std::shared_ptr thread); + void setParent(MessagePtr parent); + void setMessageOffset(int offset); + + void appendChannelPointRewardMessage(const ChannelPointReward &reward, + bool isMod, bool isBroadcaster); + + static MessagePtr makeChannelPointRewardMessage( + const ChannelPointReward &reward, bool isMod, bool isBroadcaster); + + /// Make a "CHANNEL_NAME has gone live!" message + static MessagePtr makeLiveMessage(const QString &channelName, + const QString &channelID, + MessageFlags extraFlags = {}); + + // Messages in normal chat for channel stuff + static MessagePtr makeOfflineSystemMessage(const QString &channelName, + const QString &channelID); + static MessagePtr makeHostingSystemMessage(const QString &channelName, + bool hostOn); + static MessagePtr makeDeletionMessageFromIRC( + const MessagePtr &originalMessage); + static MessagePtr makeDeletionMessageFromPubSub(const DeleteAction &action); + static MessagePtr makeListOfUsersMessage(QString prefix, QStringList users, + Channel *channel, + MessageFlags extraFlags = {}); + static MessagePtr makeListOfUsersMessage( + QString prefix, const std::vector &users, + Channel *channel, MessageFlags extraFlags = {}); + + static MessagePtr buildHypeChatMessage(Communi::IrcPrivateMessage *message); + + static std::pair makeAutomodMessage( + const AutomodAction &action, const QString &channelName); + static MessagePtr makeAutomodInfoMessage(const AutomodInfoAction &action); + + static std::pair makeLowTrustUserMessage( + const PubSubLowTrustUsersMessage &action, const QString &channelName, + const TwitchChannel *twitchChannel); + static MessagePtr makeLowTrustUpdateMessage( + const PubSubLowTrustUsersMessage &action); + + static std::unordered_map parseBadgeInfoTag( + const QVariantMap &tags); + + // Parses "badges" tag which contains a comma separated list of key-value elements + static std::vector parseBadgeTag(const QVariantMap &tags); + + static std::vector parseTwitchEmotes( + const QVariantMap &tags, const QString &originalMessage, + int messageOffset); + + static void processIgnorePhrases( + const std::vector &phrases, QString &originalMessage, + std::vector &twitchEmotes); + +protected: + void addTextOrEmoji(EmotePtr emote); + void addTextOrEmoji(const QString &string_); + + bool isEmpty() const; + MessageElement &back(); + std::unique_ptr releaseBack(); + + MessageColor textColor_ = MessageColor::Text; + // Helper method that emplaces some text stylized as system text // and then appends that text to the QString parameter "toUpdate". // Returns the TextElement that was emplaced. @@ -87,6 +264,70 @@ private: QString &toUpdate); std::shared_ptr message_; + + void parse(); + void parseUsernameColor(); + void parseUsername(); + void parseMessageID(); + void parseRoomID(); + // Parse & build thread information into the message + // Will read information from thread_ or from IRC tags + void parseThread(); + // parseHighlights only updates the visual state of the message, but leaves the playing of alerts and sounds to the triggerHighlights function + void parseHighlights(); + void appendChannelName(); + void appendUsername(); + + Outcome tryAppendEmote(const EmoteName &name); + + void addWords(const QStringList &words, + const std::vector &twitchEmotes); + + void appendTwitchBadges(); + void appendChatterinoBadges(); + void appendFfzBadges(); + void appendSeventvBadges(); + Outcome tryParseCheermote(const QString &string); + + bool shouldAddModerationElements() const; + + QString roomID_; + bool hasBits_ = false; + QString bits; + int bitsLeft{}; + bool bitsStacked = false; + bool historicalMessage_ = false; + std::shared_ptr thread_; + MessagePtr parent_; + + /** + * Starting offset to be used on index-based operations on `originalMessage_`. + * + * For example: + * originalMessage_ = "there" + * messageOffset_ = 4 + * (the irc message is "hey there") + * + * then the index 6 would resolve to 6 - 4 = 2 => 'e' + */ + int messageOffset_ = 0; + + QString userId_; + bool senderIsBroadcaster{}; + + Channel *channel = nullptr; + const Communi::IrcMessage *ircMessage; + MessageParseArgs args; + const QVariantMap tags; + QString originalMessage_; + + const bool action_{}; + + QColor usernameColor_ = {153, 153, 153}; + + bool highlightAlert_ = false; + bool highlightSound_ = false; + std::optional highlightSoundCustomUrl_{}; }; } // namespace chatterino diff --git a/src/messages/MessageColor.cpp b/src/messages/MessageColor.cpp index bd784197a..6e2f01c67 100644 --- a/src/messages/MessageColor.cpp +++ b/src/messages/MessageColor.cpp @@ -1,4 +1,4 @@ -#include "MessageColor.hpp" +#include "messages/MessageColor.hpp" #include "singletons/Theme.hpp" diff --git a/src/messages/MessageElement.cpp b/src/messages/MessageElement.cpp index a266e5855..a8beed9c0 100644 --- a/src/messages/MessageElement.cpp +++ b/src/messages/MessageElement.cpp @@ -1,8 +1,10 @@ #include "messages/MessageElement.hpp" #include "Application.hpp" +#include "controllers/moderationactions/ModerationAction.hpp" #include "debug/Benchmark.hpp" #include "messages/Emote.hpp" +#include "messages/Image.hpp" #include "messages/layouts/MessageLayoutContainer.hpp" #include "messages/layouts/MessageLayoutElement.hpp" #include "providers/emoji/Emojis.hpp" @@ -10,9 +12,28 @@ #include "singletons/Settings.hpp" #include "singletons/Theme.hpp" #include "util/DebugCount.hpp" +#include "util/Variant.hpp" namespace chatterino { +namespace { + + // Computes the bounding box for the given vector of images + QSize getBoundingBoxSize(const std::vector &images) + { + int width = 0; + int height = 0; + for (const auto &img : images) + { + width = std::max(width, img->width()); + height = std::max(height, img->height()); + } + + return QSize(width, height); + } + +} // namespace + MessageElement::MessageElement(MessageElementFlags flags) : flags_(flags) { @@ -30,30 +51,12 @@ MessageElement *MessageElement::setLink(const Link &link) return this; } -MessageElement *MessageElement::setText(const QString &text) -{ - this->text_ = text; - return this; -} - MessageElement *MessageElement::setTooltip(const QString &tooltip) { this->tooltip_ = tooltip; return this; } -MessageElement *MessageElement::setThumbnail(const ImagePtr &thumbnail) -{ - this->thumbnail_ = thumbnail; - return this; -} - -MessageElement *MessageElement::setThumbnailType(const ThumbnailType type) -{ - this->thumbnailType_ = type; - return this; -} - MessageElement *MessageElement::setTrailingSpace(bool value) { this->trailingSpace = value; @@ -65,17 +68,7 @@ const QString &MessageElement::getTooltip() const return this->tooltip_; } -const ImagePtr &MessageElement::getThumbnail() const -{ - return this->thumbnail_; -} - -const MessageElement::ThumbnailType &MessageElement::getThumbnailType() const -{ - return this->thumbnailType_; -} - -const Link &MessageElement::getLink() const +Link MessageElement::getLink() const { return this->link_; } @@ -90,35 +83,16 @@ MessageElementFlags MessageElement::getFlags() const return this->flags_; } -MessageElement *MessageElement::updateLink() +void MessageElement::addFlags(MessageElementFlags flags) { - this->linkChanged.invoke(); - return this; -} - -// Empty -EmptyElement::EmptyElement() - : MessageElement(MessageElementFlag::None) -{ -} - -void EmptyElement::addToContainer(MessageLayoutContainer &container, - MessageElementFlags flags) -{ -} - -EmptyElement &EmptyElement::instance() -{ - static EmptyElement instance; - return instance; + this->flags_.set(flags); } // IMAGE ImageElement::ImageElement(ImagePtr image, MessageElementFlags flags) : MessageElement(flags) - , image_(image) + , image_(std::move(image)) { - // this->setTooltip(image->getTooltip()); } void ImageElement::addToContainer(MessageLayoutContainer &container, @@ -129,8 +103,8 @@ void ImageElement::addToContainer(MessageLayoutContainer &container, auto size = QSize(this->image_->width() * container.getScale(), this->image_->height() * container.getScale()); - container.addElement((new ImageLayoutElement(*this, this->image_, size)) - ->setLink(this->getLink())); + container.addElement( + (new ImageLayoutElement(*this, this->image_, size))); } } @@ -138,7 +112,7 @@ CircularImageElement::CircularImageElement(ImagePtr image, int padding, QColor background, MessageElementFlags flags) : MessageElement(flags) - , image_(image) + , image_(std::move(image)) , padding_(padding) , background_(background) { @@ -152,10 +126,8 @@ void CircularImageElement::addToContainer(MessageLayoutContainer &container, auto imgSize = QSize(this->image_->width(), this->image_->height()) * container.getScale(); - container.addElement((new ImageWithCircleBackgroundLayoutElement( - *this, this->image_, imgSize, - this->background_, this->padding_)) - ->setLink(this->getLink())); + container.addElement(new ImageWithCircleBackgroundLayoutElement( + *this, this->image_, imgSize, this->background_, this->padding_)); } } @@ -183,10 +155,12 @@ void EmoteElement::addToContainer(MessageLayoutContainer &container, { if (flags.has(MessageElementFlag::EmoteImages)) { - auto image = - this->emote_->images.getImageOrLoaded(container.getScale()); + auto image = this->emote_->images.getImageOrLoaded( + container.getImageScale()); if (image->isEmpty()) + { return; + } auto emoteScale = getSettings()->emoteScale.getValue(); @@ -194,8 +168,7 @@ void EmoteElement::addToContainer(MessageLayoutContainer &container, QSize(int(container.getScale() * image->width() * emoteScale), int(container.getScale() * image->height() * emoteScale)); - container.addElement(this->makeImageLayoutElement(image, size) - ->setLink(this->getLink())); + container.addElement(this->makeImageLayoutElement(image, size)); } else { @@ -214,6 +187,169 @@ MessageLayoutElement *EmoteElement::makeImageLayoutElement( return new ImageLayoutElement(*this, image, size); } +LayeredEmoteElement::LayeredEmoteElement( + std::vector &&emotes, MessageElementFlags flags, + const MessageColor &textElementColor) + : MessageElement(flags) + , emotes_(std::move(emotes)) + , textElementColor_(textElementColor) +{ + this->updateTooltips(); +} + +void LayeredEmoteElement::addEmoteLayer(const LayeredEmoteElement::Emote &emote) +{ + this->emotes_.push_back(emote); + this->updateTooltips(); +} + +void LayeredEmoteElement::addToContainer(MessageLayoutContainer &container, + MessageElementFlags flags) +{ + if (flags.hasAny(this->getFlags())) + { + if (flags.has(MessageElementFlag::EmoteImages)) + { + auto images = this->getLoadedImages(container.getImageScale()); + if (images.empty()) + { + return; + } + + auto emoteScale = getSettings()->emoteScale.getValue(); + float overallScale = emoteScale * container.getScale(); + + auto largestSize = getBoundingBoxSize(images) * overallScale; + std::vector individualSizes; + individualSizes.reserve(this->emotes_.size()); + for (auto img : images) + { + individualSizes.push_back(QSize(img->width(), img->height()) * + overallScale); + } + + container.addElement(this->makeImageLayoutElement( + images, individualSizes, largestSize)); + } + else + { + if (this->textElement_) + { + this->textElement_->addToContainer(container, + MessageElementFlag::Misc); + } + } + } +} + +std::vector LayeredEmoteElement::getLoadedImages(float scale) +{ + std::vector res; + res.reserve(this->emotes_.size()); + + for (const auto &emote : this->emotes_) + { + auto image = emote.ptr->images.getImageOrLoaded(scale); + if (image->isEmpty()) + { + continue; + } + res.push_back(image); + } + return res; +} + +MessageLayoutElement *LayeredEmoteElement::makeImageLayoutElement( + const std::vector &images, const std::vector &sizes, + QSize largestSize) +{ + return new LayeredImageLayoutElement(*this, images, sizes, largestSize); +} + +void LayeredEmoteElement::updateTooltips() +{ + if (!this->emotes_.empty()) + { + QString copyStr = this->getCopyString(); + this->textElement_.reset(new TextElement( + copyStr, MessageElementFlag::Misc, this->textElementColor_)); + this->setTooltip(copyStr); + } + + std::vector result; + result.reserve(this->emotes_.size()); + + for (const auto &emote : this->emotes_) + { + result.push_back(emote.ptr->tooltip.string); + } + + this->emoteTooltips_ = std::move(result); +} + +const std::vector &LayeredEmoteElement::getEmoteTooltips() const +{ + return this->emoteTooltips_; +} + +QString LayeredEmoteElement::getCleanCopyString() const +{ + QString result; + for (size_t i = 0; i < this->emotes_.size(); ++i) + { + if (i != 0) + { + result += " "; + } + result += TwitchEmotes::cleanUpEmoteCode( + this->emotes_[i].ptr->getCopyString()); + } + return result; +} + +QString LayeredEmoteElement::getCopyString() const +{ + QString result; + for (size_t i = 0; i < this->emotes_.size(); ++i) + { + if (i != 0) + { + result += " "; + } + result += this->emotes_[i].ptr->getCopyString(); + } + return result; +} + +const std::vector &LayeredEmoteElement::getEmotes() + const +{ + return this->emotes_; +} + +std::vector LayeredEmoteElement::getUniqueEmotes() + const +{ + // Functor for std::copy_if that keeps track of seen elements + struct NotDuplicate { + bool operator()(const Emote &element) + { + return seen.insert(element.ptr).second; + } + + private: + std::set seen; + }; + + // Get unique emotes while maintaining relative layering order + NotDuplicate dup; + std::vector unique; + std::copy_if(this->emotes_.begin(), this->emotes_.end(), + std::back_insert_iterator(unique), dup); + + return unique; +} + // BADGE BadgeElement::BadgeElement(const EmotePtr &emote, MessageElementFlags flags) : MessageElement(flags) @@ -228,9 +364,11 @@ void BadgeElement::addToContainer(MessageLayoutContainer &container, if (flags.hasAny(this->getFlags())) { auto image = - this->emote_->images.getImageOrLoaded(container.getScale()); + this->emote_->images.getImageOrLoaded(container.getImageScale()); if (image->isEmpty()) + { return; + } auto size = QSize(int(container.getScale() * image->width()), int(container.getScale() * image->height())); @@ -247,8 +385,7 @@ EmotePtr BadgeElement::getEmote() const MessageLayoutElement *BadgeElement::makeImageLayoutElement( const ImagePtr &image, const QSize &size) { - auto element = - (new ImageLayoutElement(*this, image, size))->setLink(this->getLink()); + auto *element = new ImageLayoutElement(*this, image, size); return element; } @@ -265,9 +402,8 @@ MessageLayoutElement *ModBadgeElement::makeImageLayoutElement( { static const QColor modBadgeBackgroundColor("#34AE0A"); - auto element = (new ImageWithBackgroundLayoutElement( - *this, image, size, modBadgeBackgroundColor)) - ->setLink(this->getLink()); + auto *element = new ImageWithBackgroundLayoutElement( + *this, image, size, modBadgeBackgroundColor); return element; } @@ -282,8 +418,7 @@ VipBadgeElement::VipBadgeElement(const EmotePtr &data, MessageLayoutElement *VipBadgeElement::makeImageLayoutElement( const ImagePtr &image, const QSize &size) { - auto element = - (new ImageLayoutElement(*this, image, size))->setLink(this->getLink()); + auto *element = new ImageLayoutElement(*this, image, size); return element; } @@ -299,9 +434,8 @@ FfzBadgeElement::FfzBadgeElement(const EmotePtr &data, MessageLayoutElement *FfzBadgeElement::makeImageLayoutElement( const ImagePtr &image, const QSize &size) { - auto element = - (new ImageWithBackgroundLayoutElement(*this, image, size, this->color)) - ->setLink(this->getLink()); + auto *element = + new ImageWithBackgroundLayoutElement(*this, image, size, this->color); return element; } @@ -313,56 +447,46 @@ TextElement::TextElement(const QString &text, MessageElementFlags flags, , color_(color) , style_(style) { - for (const auto &word : text.split(' ')) - { - this->words_.push_back({word, -1}); - // fourtf: add logic to store multiple spaces after message - } + this->words_ = text.split(' '); + // fourtf: add logic to store multiple spaces after message } void TextElement::addToContainer(MessageLayoutContainer &container, MessageElementFlags flags) { - auto app = getApp(); + auto *app = getApp(); if (flags.hasAny(this->getFlags())) { QFontMetrics metrics = - app->fonts->getFontMetrics(this->style_, container.getScale()); + app->getFonts()->getFontMetrics(this->style_, container.getScale()); - for (Word &word : this->words_) + for (const auto &word : this->words_) { + auto wordId = container.nextWordId(); + auto getTextLayoutElement = [&](QString text, int width, bool hasTrailingSpace) { - auto color = this->color_.getColor(*app->themes); - app->themes->normalizeColor(color); + auto color = this->color_.getColor(*app->getThemes()); + app->getThemes()->normalizeColor(color); - auto e = (new TextLayoutElement( - *this, text, QSize(width, metrics.height()), - color, this->style_, container.getScale())) - ->setLink(this->getLink()); + auto *e = new TextLayoutElement( + *this, text, QSize(width, metrics.height()), color, + this->style_, container.getScale()); e->setTrailingSpace(hasTrailingSpace); e->setText(text); + e->setWordId(wordId); - // If URL link was changed, - // Should update it in MessageLayoutElement too! - if (this->getLink().type == Link::Url) - { - static_cast(e)->listenToLinkChanges(); - } return e; }; - // fourtf: add again - // if (word.width == -1) { - word.width = metrics.horizontalAdvance(word.text); - // } + auto width = metrics.horizontalAdvance(word); // see if the text fits in the current line - if (container.fitsInLine(word.width)) + if (container.fitsInLine(width)) { container.addElementNoLineBreak(getTextLayoutElement( - word.text, word.width, this->hasTrailingSpace())); + word, width, this->hasTrailingSpace())); continue; } @@ -371,53 +495,56 @@ void TextElement::addToContainer(MessageLayoutContainer &container, { container.breakLine(); - if (container.fitsInLine(word.width)) + if (container.fitsInLine(width)) { container.addElementNoLineBreak(getTextLayoutElement( - word.text, word.width, this->hasTrailingSpace())); + word, width, this->hasTrailingSpace())); continue; } } // we done goofed, we need to wrap the text - QString text = word.text; - int textLength = text.length(); + auto textLength = word.length(); int wordStart = 0; - int width = 0; + width = 0; // QChar::isHighSurrogate(text[0].unicode()) ? 2 : 1 for (int i = 0; i < textLength; i++) { - auto isSurrogate = text.size() > i + 1 && - QChar::isHighSurrogate(text[i].unicode()); + auto isSurrogate = word.size() > i + 1 && + QChar::isHighSurrogate(word[i].unicode()); auto charWidth = isSurrogate - ? metrics.horizontalAdvance(text.mid(i, 2)) - : metrics.horizontalAdvance(text[i]); + ? metrics.horizontalAdvance(word.mid(i, 2)) + : metrics.horizontalAdvance(word[i]); if (!container.fitsInLine(width + charWidth)) { container.addElementNoLineBreak(getTextLayoutElement( - text.mid(wordStart, i - wordStart), width, false)); + word.mid(wordStart, i - wordStart), width, false)); container.breakLine(); wordStart = i; width = charWidth; if (isSurrogate) + { i++; + } continue; } width += charWidth; if (isSurrogate) + { i++; + } } //add the final piece of wrapped text container.addElementNoLineBreak(getTextLayoutElement( - text.mid(wordStart), width, this->hasTrailingSpace())); + word.mid(wordStart), width, this->hasTrailingSpace())); } } } @@ -439,120 +566,196 @@ SingleLineTextElement::SingleLineTextElement(const QString &text, void SingleLineTextElement::addToContainer(MessageLayoutContainer &container, MessageElementFlags flags) { - auto app = getApp(); + auto *app = getApp(); if (flags.hasAny(this->getFlags())) { QFontMetrics metrics = - app->fonts->getFontMetrics(this->style_, container.getScale()); + app->getFonts()->getFontMetrics(this->style_, container.getScale()); auto getTextLayoutElement = [&](QString text, int width, bool hasTrailingSpace) { - auto color = this->color_.getColor(*app->themes); - app->themes->normalizeColor(color); + auto color = this->color_.getColor(*app->getThemes()); + app->getThemes()->normalizeColor(color); - auto e = (new TextLayoutElement( - *this, text, QSize(width, metrics.height()), color, - this->style_, container.getScale())) - ->setLink(this->getLink()); + auto *e = new TextLayoutElement( + *this, text, QSize(width, metrics.height()), color, + this->style_, container.getScale()); e->setTrailingSpace(hasTrailingSpace); e->setText(text); - // If URL link was changed, - // Should update it in MessageLayoutElement too! - if (this->getLink().type == Link::Url) - { - static_cast(e)->listenToLinkChanges(); - } return e; }; - static const auto ellipsis = QStringLiteral("..."); - auto addEllipsis = [&]() { - int ellipsisSize = metrics.horizontalAdvance(ellipsis); - container.addElementNoLineBreak( - getTextLayoutElement(ellipsis, ellipsisSize, false)); - }; + static const auto ellipsis = QStringLiteral("…"); + // String to continuously append words onto until we place it in the container + // once we encounter an emote or reach the end of the message text. */ + QString currentText; + + bool firstIteration = true; for (Word &word : this->words_) { - auto parsedWords = app->emotes->emojis.parse(word.text); - if (parsedWords.size() == 0) + if (firstIteration) { - continue; // sanity check + firstIteration = false; + } + else + { + currentText += ' '; } - auto &parsedWord = parsedWords[0]; - if (parsedWord.type() == typeid(EmotePtr)) + bool done = false; + for (const auto &parsedWord : + app->getEmotes()->getEmojis()->parse(word.text)) { - auto emote = boost::get(parsedWord); - auto image = - emote->images.getImageOrLoaded(container.getScale()); - if (!image->isEmpty()) + if (parsedWord.type() == typeid(QString)) { - auto emoteScale = getSettings()->emoteScale.getValue(); - - auto size = QSize(image->width(), image->height()) * - (emoteScale * container.getScale()); - - if (!container.fitsInLine(size.width())) + currentText += boost::get(parsedWord); + QString prev = + currentText; // only increments the ref-count + currentText = + metrics.elidedText(currentText, Qt::ElideRight, + container.remainingWidth()); + if (currentText != prev) { - addEllipsis(); + done = true; break; } - - container.addElementNoLineBreak( - (new ImageLayoutElement(*this, image, size)) - ->setLink(this->getLink())); } - } - else if (parsedWord.type() == typeid(QString)) - { - word.width = metrics.horizontalAdvance(word.text); - - // see if the text fits in the current line - if (container.fitsInLine(word.width)) + else if (parsedWord.type() == typeid(EmotePtr)) { - container.addElementNoLineBreak(getTextLayoutElement( - word.text, word.width, this->hasTrailingSpace())); - } - else - { - // word overflows, try minimum truncation - bool cutSuccess = false; - for (size_t cut = 1; cut < word.text.length(); ++cut) + auto emote = boost::get(parsedWord); + auto image = + emote->images.getImageOrLoaded(container.getScale()); + if (!image->isEmpty()) { - // Cut off n characters and append the ellipsis. - // Try removing characters one by one until the word fits. - QString truncatedWord = - word.text.chopped(cut) + ellipsis; - int newSize = metrics.horizontalAdvance(truncatedWord); - if (container.fitsInLine(newSize)) + auto emoteScale = getSettings()->emoteScale.getValue(); + + int currentWidth = + metrics.horizontalAdvance(currentText); + auto emoteSize = + QSize(image->width(), image->height()) * + (emoteScale * container.getScale()); + + if (!container.fitsInLine(currentWidth + + emoteSize.width())) { - container.addElementNoLineBreak( - getTextLayoutElement(truncatedWord, newSize, - false)); - cutSuccess = true; + currentText += ellipsis; + done = true; break; } - } - if (!cutSuccess) - { - // We weren't able to show any part of the current word, so - // just append the ellipsis. - addEllipsis(); - } + // Add currently pending text to container, then add the emote after. + container.addElementNoLineBreak(getTextLayoutElement( + currentText, currentWidth, false)); + currentText.clear(); - break; + container.addElementNoLineBreak( + (new ImageLayoutElement(*this, image, emoteSize)) + ->setLink(this->getLink()) + ->setTrailingSpace(false)); + } } } + + if (done) + { + break; + } + } + + // Add the last of the pending message text to the container. + if (!currentText.isEmpty()) + { + int width = metrics.horizontalAdvance(currentText); + container.addElementNoLineBreak( + getTextLayoutElement(currentText, width, false)); } container.breakLine(); } } +LinkElement::LinkElement(const Parsed &parsed, const QString &fullUrl, + MessageElementFlags flags, const MessageColor &color, + FontStyle style) + : TextElement({}, flags, color, style) + , linkInfo_(fullUrl) + , lowercase_({parsed.lowercase}) + , original_({parsed.original}) +{ + this->setTooltip(parsed.original); +} + +void LinkElement::addToContainer(MessageLayoutContainer &container, + MessageElementFlags flags) +{ + this->words_ = + getSettings()->lowercaseDomains ? this->lowercase_ : this->original_; + TextElement::addToContainer(container, flags); +} + +Link LinkElement::getLink() const +{ + return {Link::Url, this->linkInfo_.url()}; +} + +MentionElement::MentionElement(const QString &displayName, QString loginName_, + MessageColor fallbackColor_, + MessageColor userColor_) + : TextElement(displayName, + {MessageElementFlag::Text, MessageElementFlag::Mention}) + , fallbackColor(fallbackColor_) + , userColor(userColor_) + , userLoginName(std::move(loginName_)) +{ +} + +void MentionElement::addToContainer(MessageLayoutContainer &container, + MessageElementFlags flags) +{ + if (getSettings()->colorUsernames) + { + this->color_ = this->userColor; + } + else + { + this->color_ = this->fallbackColor; + } + + if (getSettings()->boldUsernames) + { + this->style_ = FontStyle::ChatMediumBold; + } + else + { + this->style_ = FontStyle::ChatMedium; + } + + TextElement::addToContainer(container, flags); +} + +MessageElement *MentionElement::setLink(const Link &link) +{ + assert(false && "MentionElement::setLink should not be called. Pass " + "through a valid login name in the constructor and it will " + "automatically be a UserInfo link"); + + return TextElement::setLink(link); +} + +Link MentionElement::getLink() const +{ + if (this->userLoginName.isEmpty()) + { + // Some rare mention elements don't have the knowledge of the login name + return {}; + } + + return {Link::UserInfo, this->userLoginName}; +} + // TIMESTAMP TimestampElement::TimestampElement(QTime time) : MessageElement(MessageElementFlag::Timestamp) @@ -600,13 +803,13 @@ void TwitchModerationElement::addToContainer(MessageLayoutContainer &container, { QSize size(int(container.getScale() * 16), int(container.getScale() * 16)); - auto actions = getCSettings().moderationActions.readOnly(); + auto actions = getSettings()->moderationActions.readOnly(); for (const auto &action : *actions) { if (auto image = action.getImage()) { container.addElement( - (new ImageLayoutElement(*this, image.get(), size)) + (new ImageLayoutElement(*this, *image, size)) ->setLink(Link(Link::UserAction, action.getAction()))); } else @@ -638,7 +841,7 @@ void LinebreakElement::addToContainer(MessageLayoutContainer &container, ScalingImageElement::ScalingImageElement(ImageSet images, MessageElementFlags flags) : MessageElement(flags) - , images_(images) + , images_(std::move(images)) { } @@ -648,35 +851,38 @@ void ScalingImageElement::addToContainer(MessageLayoutContainer &container, if (flags.hasAny(this->getFlags())) { const auto &image = - this->images_.getImageOrLoaded(container.getScale()); + this->images_.getImageOrLoaded(container.getImageScale()); if (image->isEmpty()) + { return; + } auto size = QSize(image->width() * container.getScale(), image->height() * container.getScale()); - container.addElement((new ImageLayoutElement(*this, image, size)) - ->setLink(this->getLink())); + container.addElement(new ImageLayoutElement(*this, image, size)); } } ReplyCurveElement::ReplyCurveElement() : MessageElement(MessageElementFlag::RepliedMessage) - // these values nicely align with a single badge - , neededMargin_(3) - , size_(18, 14) { } void ReplyCurveElement::addToContainer(MessageLayoutContainer &container, MessageElementFlags flags) { + static const int width = 18; // Overall width + static const float thickness = 1.5; // Pen width + static const int radius = 6; // Radius of the top left corner + static const int margin = 2; // Top/Left/Bottom margin + if (flags.hasAny(this->getFlags())) { - QSize boxSize = this->size_ * container.getScale(); - container.addElement(new ReplyCurveLayoutElement( - *this, boxSize, 1.5 * container.getScale(), - this->neededMargin_ * container.getScale())); + float scale = container.getScale(); + container.addElement( + new ReplyCurveLayoutElement(*this, width * scale, thickness * scale, + radius * scale, margin * scale)); } } diff --git a/src/messages/MessageElement.hpp b/src/messages/MessageElement.hpp index 4cc47873a..49ce762cb 100644 --- a/src/messages/MessageElement.hpp +++ b/src/messages/MessageElement.hpp @@ -4,15 +4,16 @@ #include "messages/ImageSet.hpp" #include "messages/Link.hpp" #include "messages/MessageColor.hpp" +#include "providers/links/LinkInfo.hpp" #include "singletons/Fonts.hpp" +#include #include #include #include -#include + #include #include -#include #include namespace chatterino { @@ -37,6 +38,7 @@ enum class MessageElementFlag : int64_t { TwitchEmoteImage = (1LL << 4), TwitchEmoteText = (1LL << 5), TwitchEmote = TwitchEmoteImage | TwitchEmoteText, + BttvEmoteImage = (1LL << 6), BttvEmoteText = (1LL << 7), BttvEmote = BttvEmoteImage | BttvEmoteText, @@ -47,8 +49,15 @@ enum class MessageElementFlag : int64_t { FfzEmoteImage = (1LL << 9), FfzEmoteText = (1LL << 10), FfzEmote = FfzEmoteImage | FfzEmoteText, - EmoteImages = TwitchEmoteImage | BttvEmoteImage | FfzEmoteImage, - EmoteText = TwitchEmoteText | BttvEmoteText | FfzEmoteText, + + SevenTVEmoteImage = (1LL << 34), + SevenTVEmoteText = (1LL << 35), + SevenTVEmote = SevenTVEmoteImage | SevenTVEmoteText, + + EmoteImages = + TwitchEmoteImage | BttvEmoteImage | FfzEmoteImage | SevenTVEmoteImage, + EmoteText = + TwitchEmoteText | BttvEmoteText | FfzEmoteText | SevenTVEmoteText, BitsStatic = (1LL << 11), BitsAnimated = (1LL << 12), @@ -89,6 +98,15 @@ enum class MessageElementFlag : int64_t { // - Chatterino gnome badge BadgeChatterino = (1LL << 18), + // Slot 7: 7TV + // - 7TV Admin + // - 7TV Dungeon Mistress + // - 7TV Moderator + // - 7TV Subscriber + // - 7TV Translator + // - 7TV Contributor + BadgeSevenTV = (1LL << 36), + // Slot 7: FrankerFaceZ // - FFZ developer badge // - FFZ bot badge @@ -96,7 +114,8 @@ enum class MessageElementFlag : int64_t { BadgeFfz = (1LL << 19), Badges = BadgeGlobalAuthority | BadgePredictions | BadgeChannelAuthority | - BadgeSubscription | BadgeVanity | BadgeChatterino | BadgeFfz, + BadgeSubscription | BadgeVanity | BadgeChatterino | BadgeSevenTV | + BadgeFfz, ChannelName = (1LL << 20), @@ -114,17 +133,15 @@ enum class MessageElementFlag : int64_t { // needed Collapsed = (1LL << 26), - // used for dynamic bold usernames - BoldUsername = (1LL << 27), - NonBoldUsername = (1LL << 28), + // A mention of a username that isn't the author of the message + Mention = (1LL << 27), - // for links - LowercaseLink = (1LL << 29), - OriginalLink = (1LL << 30), + // Unused = (1LL << 28), - // ZeroWidthEmotes are emotes that are supposed to overlay over any pre-existing emotes - // e.g. BTTV's SoSnowy during christmas season - ZeroWidthEmote = (1LL << 31), + // used to check if links should be lowercased + LowercaseLinks = (1LL << 29), + // Unused = (1LL << 30) + // Unused: (1LL << 31) // for elements of the message reply RepliedMessage = (1LL << 32), @@ -132,76 +149,50 @@ enum class MessageElementFlag : int64_t { // for the reply button element ReplyButton = (1LL << 33), + // (1LL << 34) through (1LL << 36) are occupied by + // SevenTVEmoteImage, SevenTVEmoteText, and BadgeSevenTV, + Default = Timestamp | Badges | Username | BitsStatic | FfzEmoteImage | - BttvEmoteImage | TwitchEmoteImage | BitsAmount | Text | - AlwaysShow, + BttvEmoteImage | SevenTVEmoteImage | TwitchEmoteImage | + BitsAmount | Text | AlwaysShow, }; using MessageElementFlags = FlagsEnum; -class MessageElement : boost::noncopyable +class MessageElement { public: - enum UpdateFlags : char { - Update_Text = 1, - Update_Emotes = 2, - Update_Images = 4, - Update_All = Update_Text | Update_Emotes | Update_Images - }; - enum ThumbnailType : char { - Link_Thumbnail = 1, - }; - virtual ~MessageElement(); - MessageElement *setLink(const Link &link); - MessageElement *setText(const QString &text); + MessageElement(const MessageElement &) = delete; + MessageElement &operator=(const MessageElement &) = delete; + + MessageElement(MessageElement &&) = delete; + MessageElement &operator=(MessageElement &&) = delete; + + virtual MessageElement *setLink(const Link &link); MessageElement *setTooltip(const QString &tooltip); - MessageElement *setThumbnailType(const ThumbnailType type); - MessageElement *setThumbnail(const ImagePtr &thumbnail); MessageElement *setTrailingSpace(bool value); const QString &getTooltip() const; - const ImagePtr &getThumbnail() const; - const ThumbnailType &getThumbnailType() const; - const Link &getLink() const; + virtual Link getLink() const; bool hasTrailingSpace() const; MessageElementFlags getFlags() const; - MessageElement *updateLink(); + void addFlags(MessageElementFlags flags); virtual void addToContainer(MessageLayoutContainer &container, MessageElementFlags flags) = 0; - pajlada::Signals::NoArgSignal linkChanged; - protected: MessageElement(MessageElementFlags flags); bool trailingSpace = true; private: - QString text_; Link link_; QString tooltip_; - ImagePtr thumbnail_; - ThumbnailType thumbnailType_; MessageElementFlags flags_; }; -// used when layout element doesn't have a creator -class EmptyElement : public MessageElement -{ -public: - EmptyElement(); - - void addToContainer(MessageLayoutContainer &container, - MessageElementFlags flags) override; - - static EmptyElement &instance(); - -private: - ImagePtr image_; -}; - // contains a simple image class ImageElement : public MessageElement { @@ -243,15 +234,11 @@ public: void addToContainer(MessageLayoutContainer &container, MessageElementFlags flags) override; -private: +protected: + QStringList words_; + MessageColor color_; FontStyle style_; - - struct Word { - QString text; - int width = -1; - }; - std::vector words_; }; // contains a text that will be truncated to one line @@ -277,6 +264,84 @@ private: std::vector words_; }; +class LinkElement : public TextElement +{ +public: + struct Parsed { + QString lowercase; + QString original; + }; + + /// @param parsed The link as it appeared in the message + /// @param fullUrl A full URL (notably with a protocol) + LinkElement(const Parsed &parsed, const QString &fullUrl, + MessageElementFlags flags, + const MessageColor &color = MessageColor::Text, + FontStyle style = FontStyle::ChatMedium); + ~LinkElement() override = default; + LinkElement(const LinkElement &) = delete; + LinkElement(LinkElement &&) = delete; + LinkElement &operator=(const LinkElement &) = delete; + LinkElement &operator=(LinkElement &&) = delete; + + void addToContainer(MessageLayoutContainer &container, + MessageElementFlags flags) override; + + Link getLink() const override; + + [[nodiscard]] LinkInfo *linkInfo() + { + return &this->linkInfo_; + } + +private: + LinkInfo linkInfo_; + // these are implicitly shared + QStringList lowercase_; + QStringList original_; +}; + +/** + * @brief Contains a username mention. + * + * Examples of mentions: + * V + * 13:37 pajlada: hello @forsen + * + * V V + * 13:37 The moderators of this channel are: forsen, nuuls + */ +class MentionElement : public TextElement +{ +public: + MentionElement(const QString &displayName, QString loginName_, + MessageColor fallbackColor_, MessageColor userColor_); + ~MentionElement() override = default; + MentionElement(const MentionElement &) = delete; + MentionElement(MentionElement &&) = delete; + MentionElement &operator=(const MentionElement &) = delete; + MentionElement &operator=(MentionElement &&) = delete; + + void addToContainer(MessageLayoutContainer &container, + MessageElementFlags flags) override; + + MessageElement *setLink(const Link &link) override; + Link getLink() const override; + +private: + /** + * The color of the element in case the "Colorize @usernames" is disabled + **/ + MessageColor fallbackColor; + + /** + * The color of the element in case the "Colorize @usernames" is enabled + **/ + MessageColor userColor; + + QString userLoginName; +}; + // contains emote data and will pick the emote based on : // a) are images for the emote type enabled // b) which size it wants @@ -299,6 +364,48 @@ private: EmotePtr emote_; }; +// A LayeredEmoteElement represents multiple Emotes layered on top of each other. +// This class takes care of rendering animated and non-animated emotes in the +// correct order and aligning them in the right way. +class LayeredEmoteElement : public MessageElement +{ +public: + struct Emote { + EmotePtr ptr; + MessageElementFlags flags; + }; + + LayeredEmoteElement( + std::vector &&emotes, MessageElementFlags flags, + const MessageColor &textElementColor = MessageColor::Text); + + void addEmoteLayer(const Emote &emote); + + void addToContainer(MessageLayoutContainer &container, + MessageElementFlags flags) override; + + // Returns a concatenation of each emote layer's cleaned copy string + QString getCleanCopyString() const; + const std::vector &getEmotes() const; + std::vector getUniqueEmotes() const; + const std::vector &getEmoteTooltips() const; + +private: + MessageLayoutElement *makeImageLayoutElement( + const std::vector &image, const std::vector &sizes, + QSize largestSize); + + QString getCopyString() const; + void updateTooltips(); + std::vector getLoadedImages(float scale); + + std::vector emotes_; + std::vector emoteTooltips_; + + std::unique_ptr textElement_; + MessageColor textElementColor_; +}; + class BadgeElement : public MessageElement { public: @@ -408,10 +515,6 @@ public: void addToContainer(MessageLayoutContainer &container, MessageElementFlags flags) override; - -private: - int neededMargin_; - QSize size_; }; } // namespace chatterino diff --git a/src/messages/MessageFlag.hpp b/src/messages/MessageFlag.hpp new file mode 100644 index 000000000..7648dadc7 --- /dev/null +++ b/src/messages/MessageFlag.hpp @@ -0,0 +1,62 @@ +#pragma once + +#include "common/FlagsEnum.hpp" + +#include + +namespace chatterino { + +enum class MessageFlag : std::int64_t { + None = 0LL, + System = (1LL << 0), + Timeout = (1LL << 1), + Highlighted = (1LL << 2), + DoNotTriggerNotification = (1LL << 3), // disable notification sound + Centered = (1LL << 4), + Disabled = (1LL << 5), + DisableCompactEmotes = (1LL << 6), + Collapsed = (1LL << 7), + ConnectedMessage = (1LL << 8), + DisconnectedMessage = (1LL << 9), + Untimeout = (1LL << 10), + PubSub = (1LL << 11), + Subscription = (1LL << 12), + DoNotLog = (1LL << 13), + AutoMod = (1LL << 14), + RecentMessage = (1LL << 15), + Whisper = (1LL << 16), + HighlightedWhisper = (1LL << 17), + Debug = (1LL << 18), + Similar = (1LL << 19), + RedeemedHighlight = (1LL << 20), + RedeemedChannelPointReward = (1LL << 21), + ShowInMentions = (1LL << 22), + FirstMessage = (1LL << 23), + ReplyMessage = (1LL << 24), + ElevatedMessage = (1LL << 25), + SubscribedThread = (1LL << 26), + CheerMessage = (1LL << 27), + LiveUpdatesAdd = (1LL << 28), + LiveUpdatesRemove = (1LL << 29), + LiveUpdatesUpdate = (1LL << 30), + /// The header of a message caught by AutoMod containing allow/disallow + AutoModOffendingMessageHeader = (1LL << 31), + /// The message caught by AutoMod containing the user who sent the message & its contents + AutoModOffendingMessage = (1LL << 32), + LowTrustUsers = (1LL << 33), + /// The message is sent by a user marked as restricted with Twitch's "Low Trust"/"Suspicious User" feature + RestrictedMessage = (1LL << 34), + /// The message is sent by a user marked as monitor with Twitch's "Low Trust"/"Suspicious User" feature + MonitoredMessage = (1LL << 35), + /// The message is an ACTION message (/me) + Action = (1LL << 36), +}; +using MessageFlags = FlagsEnum; + +} // namespace chatterino + +template <> +struct magic_enum::customize::enum_range { + // NOLINTNEXTLINE(readability-identifier-naming) + static constexpr bool is_flags = true; +}; diff --git a/src/messages/MessageParseArgs.hpp b/src/messages/MessageParseArgs.hpp deleted file mode 100644 index 471d1d648..000000000 --- a/src/messages/MessageParseArgs.hpp +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - -namespace chatterino { - -} // namespace chatterino diff --git a/src/messages/MessageThread.cpp b/src/messages/MessageThread.cpp index dc798d993..e1227ab09 100644 --- a/src/messages/MessageThread.cpp +++ b/src/messages/MessageThread.cpp @@ -1,4 +1,4 @@ -#include "MessageThread.hpp" +#include "messages/MessageThread.hpp" #include "messages/Message.hpp" #include "util/DebugCount.hpp" @@ -58,4 +58,26 @@ size_t MessageThread::liveCount( return count; } +void MessageThread::markSubscribed() +{ + if (this->subscription_ == Subscription::Subscribed) + { + return; + } + + this->subscription_ = Subscription::Subscribed; + this->subscriptionUpdated(); +} + +void MessageThread::markUnsubscribed() +{ + if (this->subscription_ == Subscription::Unsubscribed) + { + return; + } + + this->subscription_ = Subscription::Unsubscribed; + this->subscriptionUpdated(); +} + } // namespace chatterino diff --git a/src/messages/MessageThread.hpp b/src/messages/MessageThread.hpp index f2a8e57d5..442db46a6 100644 --- a/src/messages/MessageThread.hpp +++ b/src/messages/MessageThread.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -11,6 +12,12 @@ struct Message; class MessageThread { public: + enum class Subscription : uint8_t { + None, + Subscribed, + Unsubscribed, + }; + MessageThread(std::shared_ptr rootMessage); ~MessageThread(); @@ -23,6 +30,23 @@ public: /// Returns the number of live reply references size_t liveCount(const std::shared_ptr &exclude) const; + bool subscribed() const + { + return this->subscription_ == Subscription::Subscribed; + } + + /// Returns true if and only if the user manually unsubscribed from the thread + /// @see #markUnsubscribed() + bool unsubscribed() const + { + return this->subscription_ == Subscription::Unsubscribed; + } + + /// Subscribe to this thread. + void markSubscribed(); + /// Unsubscribe from this thread. + void markUnsubscribed(); + const QString &rootId() const { return rootMessageId_; @@ -38,10 +62,14 @@ public: return replies_; } + boost::signals2::signal subscriptionUpdated; + private: const QString rootMessageId_; const std::shared_ptr rootMessage_; std::vector> replies_; + + Subscription subscription_ = Subscription::None; }; } // namespace chatterino diff --git a/src/messages/Selection.hpp b/src/messages/Selection.hpp index 175a4657c..b04ad1673 100644 --- a/src/messages/Selection.hpp +++ b/src/messages/Selection.hpp @@ -1,25 +1,23 @@ #pragma once +#include +#include +#include #include #include namespace chatterino { struct SelectionItem { - int messageIndex; - int charIndex; + size_t messageIndex{0}; + size_t charIndex{0}; - SelectionItem() + SelectionItem() = default; + + SelectionItem(size_t _messageIndex, size_t _charIndex) + : messageIndex(_messageIndex) + , charIndex(_charIndex) { - this->messageIndex = 0; - this->charIndex = 0; - } - - SelectionItem(int _messageIndex, int _charIndex) - { - this->messageIndex = _messageIndex; - - this->charIndex = _charIndex; } bool operator<(const SelectionItem &b) const @@ -41,7 +39,7 @@ struct SelectionItem { bool operator!=(const SelectionItem &b) const { - return this->operator==(b); + return !this->operator==(b); } }; @@ -65,6 +63,23 @@ struct Selection { } } + bool operator==(const Selection &b) const + { + return this->start == b.start && this->end == b.end; + } + + bool operator!=(const Selection &b) const + { + return !this->operator==(b); + } + + //union of both selections + Selection operator|(const Selection &b) const + { + return {std::min(this->selectionMin, b.selectionMin), + std::max(this->selectionMax, b.selectionMax)}; + } + bool isEmpty() const { return this->start == this->end; @@ -75,16 +90,49 @@ struct Selection { return this->selectionMin.messageIndex == this->selectionMax.messageIndex; } -}; -struct DoubleClickSelection { - int originalStart = 0; - int originalEnd = 0; - int origMessageIndex; - bool selectingLeft = false; - bool selectingRight = false; - SelectionItem origStartItem; - SelectionItem origEndItem; -}; + // Shift all message selection indices `offset` back + void shiftMessageIndex(size_t offset) + { + if (offset > this->selectionMin.messageIndex) + { + this->selectionMin.messageIndex = 0; + this->selectionMin.charIndex = 0; + } + else + { + this->selectionMin.messageIndex -= offset; + } + if (offset > this->selectionMax.messageIndex) + { + this->selectionMax.messageIndex = 0; + this->selectionMax.charIndex = 0; + } + else + { + this->selectionMax.messageIndex -= offset; + } + + if (offset > this->start.messageIndex) + { + this->start.messageIndex = 0; + this->start.charIndex = 0; + } + else + { + this->start.messageIndex -= offset; + } + + if (offset > this->end.messageIndex) + { + this->end.messageIndex = 0; + this->end.charIndex = 0; + } + else + { + this->end.messageIndex -= offset; + } + } +}; } // namespace chatterino diff --git a/src/messages/SharedMessageBuilder.cpp b/src/messages/SharedMessageBuilder.cpp deleted file mode 100644 index f059a6708..000000000 --- a/src/messages/SharedMessageBuilder.cpp +++ /dev/null @@ -1,283 +0,0 @@ -#include "messages/SharedMessageBuilder.hpp" - -#include "Application.hpp" -#include "common/QLogging.hpp" -#include "controllers/highlights/HighlightController.hpp" -#include "controllers/ignores/IgnoreController.hpp" -#include "controllers/ignores/IgnorePhrase.hpp" -#include "messages/MessageElement.hpp" -#include "singletons/Settings.hpp" -#include "singletons/WindowManager.hpp" -#include "util/Helpers.hpp" -#include "util/Qt.hpp" -#include "util/StreamerMode.hpp" - -#include -#include - -namespace chatterino { - -namespace { - - QUrl getFallbackHighlightSound() - { - QString path = getSettings()->pathHighlightSound; - bool fileExists = QFileInfo::exists(path) && QFileInfo(path).isFile(); - - // Use fallback sound when checkbox is not checked - // or custom file doesn't exist - if (getSettings()->customHighlightSound && fileExists) - { - return QUrl::fromLocalFile(path); - } - else - { - return QUrl("qrc:/sounds/ping2.wav"); - } - } - -} // namespace - -SharedMessageBuilder::SharedMessageBuilder( - Channel *_channel, const Communi::IrcPrivateMessage *_ircMessage, - const MessageParseArgs &_args) - : channel(_channel) - , ircMessage(_ircMessage) - , args(_args) - , tags(this->ircMessage->tags()) - , originalMessage_(_ircMessage->content()) - , action_(_ircMessage->isAction()) -{ -} - -SharedMessageBuilder::SharedMessageBuilder( - Channel *_channel, const Communi::IrcMessage *_ircMessage, - const MessageParseArgs &_args, QString content, bool isAction) - : channel(_channel) - , ircMessage(_ircMessage) - , args(_args) - , tags(this->ircMessage->tags()) - , originalMessage_(content) - , action_(isAction) -{ -} - -void SharedMessageBuilder::parse() -{ - this->parseUsernameColor(); - - if (this->action_) - { - this->textColor_ = this->usernameColor_; - } - - this->parseUsername(); - - this->message().flags.set(MessageFlag::Collapsed); -} - -// "foo/bar/baz,tri/hard" can be a valid badge-info tag -// In that case, valid map content should be 'split by slash' only once: -// {"foo": "bar/baz", "tri": "hard"} -std::pair SharedMessageBuilder::slashKeyValue( - const QString &kvStr) -{ - return { - // part before first slash (index 0 of section) - kvStr.section('/', 0, 0), - // part after first slash (index 1 of section) - kvStr.section('/', 1, -1), - }; -} - -std::vector SharedMessageBuilder::parseBadgeTag(const QVariantMap &tags) -{ - std::vector b; - - auto badgesIt = tags.constFind("badges"); - if (badgesIt == tags.end()) - { - return b; - } - - auto badges = badgesIt.value().toString().split(',', Qt::SkipEmptyParts); - - for (const QString &badge : badges) - { - if (!badge.contains('/')) - { - continue; - } - - auto pair = SharedMessageBuilder::slashKeyValue(badge); - b.emplace_back(Badge{pair.first, pair.second}); - } - - return b; -} - -bool SharedMessageBuilder::isIgnored() const -{ - return isIgnoredMessage({ - /*.message = */ this->originalMessage_, - }); -} - -void SharedMessageBuilder::parseUsernameColor() -{ - if (getSettings()->colorizeNicknames) - { - this->usernameColor_ = getRandomColor(this->ircMessage->nick()); - } -} - -void SharedMessageBuilder::parseUsername() -{ - // username - this->userName = this->ircMessage->nick(); - - this->message().loginName = this->userName; -} - -void SharedMessageBuilder::parseHighlights() -{ - if (getCSettings().isBlacklistedUser(this->ircMessage->nick())) - { - // Do nothing. We ignore highlights from this user. - return; - } - - auto badges = SharedMessageBuilder::parseBadgeTag(this->tags); - auto [highlighted, highlightResult] = getIApp()->getHighlights()->check( - this->args, badges, this->ircMessage->nick(), this->originalMessage_); - - if (!highlighted) - { - return; - } - - // This message triggered one or more highlights, act upon the highlight result - - this->message().flags.set(MessageFlag::Highlighted); - - this->highlightAlert_ = highlightResult.alert; - - this->highlightSound_ = highlightResult.playSound; - - this->message().highlightColor = highlightResult.color; - - if (highlightResult.customSoundUrl) - { - this->highlightSoundUrl_ = highlightResult.customSoundUrl.get(); - } - else - { - this->highlightSoundUrl_ = getFallbackHighlightSound(); - } - - if (highlightResult.showInMentions) - { - this->message().flags.set(MessageFlag::ShowInMentions); - } -} - -void SharedMessageBuilder::addTextOrEmoji(EmotePtr emote) -{ - this->emplace(emote, MessageElementFlag::EmojiAll); -} - -void SharedMessageBuilder::addTextOrEmoji(const QString &string_) -{ - auto string = QString(string_); - - // Actually just text - auto linkString = this->matchLink(string); - auto link = Link(); - auto &&textColor = this->textColor_; - - if (linkString.isEmpty()) - { - if (string.startsWith('@')) - { - this->emplace(string, MessageElementFlag::BoldUsername, - textColor, FontStyle::ChatMediumBold); - this->emplace( - string, MessageElementFlag::NonBoldUsername, textColor); - } - else - { - this->emplace(string, MessageElementFlag::Text, - textColor); - } - } - else - { - this->addLink(string, linkString); - } -} - -void SharedMessageBuilder::appendChannelName() -{ - QString channelName("#" + this->channel->getName()); - Link link(Link::JumpToChannel, this->channel->getName()); - - this->emplace(channelName, MessageElementFlag::ChannelName, - MessageColor::System) - ->setLink(link); -} - -inline QMediaPlayer *getPlayer() -{ - if (isGuiThread()) - { - static auto player = new QMediaPlayer; - return player; - } - else - { - return nullptr; - } -} - -void SharedMessageBuilder::triggerHighlights() -{ - static QUrl currentPlayerUrl; - - if (isInStreamerMode() && getSettings()->streamerModeMuteMentions) - { - // We are in streamer mode with muting mention sounds enabled. Do nothing. - return; - } - - if (getCSettings().isMutedChannel(this->channel->getName())) - { - // Do nothing. Pings are muted in this channel. - return; - } - - bool hasFocus = (QApplication::focusWidget() != nullptr); - bool resolveFocus = !hasFocus || getSettings()->highlightAlwaysPlaySound; - - if (this->highlightSound_ && resolveFocus) - { - if (auto player = getPlayer()) - { - // update the media player url if necessary - if (currentPlayerUrl != this->highlightSoundUrl_) - { - player->setMedia(this->highlightSoundUrl_); - - currentPlayerUrl = this->highlightSoundUrl_; - } - - player->play(); - } - } - - if (this->highlightAlert_) - { - getApp()->windows->sendAlert(); - } -} - -} // namespace chatterino diff --git a/src/messages/SharedMessageBuilder.hpp b/src/messages/SharedMessageBuilder.hpp deleted file mode 100644 index 62adb45df..000000000 --- a/src/messages/SharedMessageBuilder.hpp +++ /dev/null @@ -1,78 +0,0 @@ -#include "messages/MessageBuilder.hpp" - -#include "common/Aliases.hpp" -#include "common/Outcome.hpp" -#include "messages/MessageColor.hpp" -#include "providers/twitch/TwitchBadge.hpp" - -#include -#include -#include - -namespace chatterino { - -class SharedMessageBuilder : public MessageBuilder -{ -public: - SharedMessageBuilder() = delete; - - explicit SharedMessageBuilder(Channel *_channel, - const Communi::IrcPrivateMessage *_ircMessage, - const MessageParseArgs &_args); - - explicit SharedMessageBuilder(Channel *_channel, - const Communi::IrcMessage *_ircMessage, - const MessageParseArgs &_args, - QString content, bool isAction); - - QString userName; - - [[nodiscard]] virtual bool isIgnored() const; - - // triggerHighlights triggers any alerts or sounds parsed by parseHighlights - virtual void triggerHighlights(); - virtual MessagePtr build() = 0; - - static std::pair slashKeyValue(const QString &kvStr); - - // Parses "badges" tag which contains a comma separated list of key-value elements - static std::vector parseBadgeTag(const QVariantMap &tags); - -protected: - virtual void parse(); - - virtual void parseUsernameColor(); - - virtual void parseUsername(); - - virtual Outcome tryAppendEmote(const EmoteName &name) - { - return Failure; - } - - // parseHighlights only updates the visual state of the message, but leaves the playing of alerts and sounds to the triggerHighlights function - virtual void parseHighlights(); - - virtual void addTextOrEmoji(EmotePtr emote); - virtual void addTextOrEmoji(const QString &value); - - void appendChannelName(); - - Channel *channel; - const Communi::IrcMessage *ircMessage; - MessageParseArgs args; - const QVariantMap tags; - QString originalMessage_; - - const bool action_{}; - - QColor usernameColor_ = {153, 153, 153}; - MessageColor textColor_ = MessageColor::Text; - - bool highlightAlert_ = false; - bool highlightSound_ = false; - - QUrl highlightSoundUrl_; -}; - -} // namespace chatterino diff --git a/src/messages/layouts/MessageLayout.cpp b/src/messages/layouts/MessageLayout.cpp index f0f01f933..e1a3d8a3c 100644 --- a/src/messages/layouts/MessageLayout.cpp +++ b/src/messages/layouts/MessageLayout.cpp @@ -1,27 +1,23 @@ #include "messages/layouts/MessageLayout.hpp" #include "Application.hpp" -#include "debug/Benchmark.hpp" +#include "messages/layouts/MessageLayoutContainer.hpp" +#include "messages/layouts/MessageLayoutContext.hpp" +#include "messages/layouts/MessageLayoutElement.hpp" #include "messages/Message.hpp" #include "messages/MessageElement.hpp" -#include "messages/layouts/MessageLayoutContainer.hpp" -#include "singletons/Emotes.hpp" +#include "messages/Selection.hpp" +#include "providers/colors/ColorProvider.hpp" #include "singletons/Settings.hpp" -#include "singletons/Theme.hpp" +#include "singletons/StreamerMode.hpp" #include "singletons/WindowManager.hpp" #include "util/DebugCount.hpp" #include #include #include -#include #include - -#define MARGIN_LEFT (int)(8 * this->scale) -#define MARGIN_RIGHT (int)(8 * this->scale) -#define MARGIN_TOP (int)(4 * this->scale) -#define MARGIN_BOTTOM (int)(4 * this->scale) -#define COMPACT_EMOTES_OFFSET 6 +#include namespace chatterino { @@ -40,7 +36,6 @@ namespace { MessageLayout::MessageLayout(MessagePtr message) : message_(std::move(message)) - , container_(std::make_shared()) { DebugCount::increase("message layout"); } @@ -63,17 +58,22 @@ const MessagePtr &MessageLayout::getMessagePtr() const // Height int MessageLayout::getHeight() const { - return container_->getHeight(); + return this->container_.getHeight(); +} + +int MessageLayout::getWidth() const +{ + return this->container_.getWidth(); } // Layout // return true if redraw is required -bool MessageLayout::layout(int width, float scale, MessageElementFlags flags) +bool MessageLayout::layout(int width, float scale, float imageScale, + MessageElementFlags flags, + bool shouldInvalidateBuffer) { // BenchmarkGuard benchmark("MessageLayout::layout()"); - auto app = getApp(); - bool layoutRequired = false; // check if width changed @@ -82,11 +82,12 @@ bool MessageLayout::layout(int width, float scale, MessageElementFlags flags) this->currentLayoutWidth_ = width; // check if layout state changed - if (this->layoutState_ != app->windows->getGeneration()) + const auto layoutGeneration = getApp()->getWindows()->getGeneration(); + if (this->layoutState_ != layoutGeneration) { layoutRequired = true; this->flags.set(MessageLayoutFlag::RequiresBufferUpdate); - this->layoutState_ = app->windows->getGeneration(); + this->layoutState_ = layoutGeneration; } // check if work mask changed @@ -100,15 +101,22 @@ bool MessageLayout::layout(int width, float scale, MessageElementFlags flags) // check if dpi changed layoutRequired |= this->scale_ != scale; this->scale_ = scale; + layoutRequired |= this->imageScale_ != imageScale; + this->imageScale_ = imageScale; if (!layoutRequired) { + if (shouldInvalidateBuffer) + { + this->invalidateBuffer(); + return true; + } return false; } - int oldHeight = this->container_->getHeight(); + int oldHeight = this->container_.getHeight(); this->actuallyLayout(width, flags); - if (widthChanged || this->container_->getHeight() != oldHeight) + if (widthChanged || this->container_.getHeight() != oldHeight) { this->deleteBuffer(); } @@ -119,7 +127,10 @@ bool MessageLayout::layout(int width, float scale, MessageElementFlags flags) void MessageLayout::actuallyLayout(int width, MessageElementFlags flags) { +#ifdef FOURTF this->layoutCount_++; +#endif + auto messageFlags = this->message_->flags; if (this->flags.has(MessageLayoutFlag::Expanded) || @@ -129,218 +140,245 @@ void MessageLayout::actuallyLayout(int width, MessageElementFlags flags) messageFlags.unset(MessageFlag::Collapsed); } - this->container_->begin(width, this->scale_, messageFlags); + bool hideModerated = getSettings()->hideModerated; + bool hideModerationActions = getSettings()->hideModerationActions; + bool hideSimilar = getSettings()->hideSimilar; + bool hideReplies = !flags.has(MessageElementFlag::RepliedMessage); + + this->container_.beginLayout(width, this->scale_, this->imageScale_, + messageFlags); for (const auto &element : this->message_->elements) { - if (getSettings()->hideModerated && - this->message_->flags.has(MessageFlag::Disabled)) + if (hideModerated && this->message_->flags.has(MessageFlag::Disabled)) { continue; } - if (getSettings()->hideModerationActions && - (this->message_->flags.has(MessageFlag::Timeout) || - this->message_->flags.has(MessageFlag::Untimeout))) + if (this->message_->flags.has(MessageFlag::Timeout) || + this->message_->flags.has(MessageFlag::Untimeout)) + { + if (hideModerationActions || + (getSettings()->streamerModeHideModActions && + getApp()->getStreamerMode()->isEnabled())) + { + continue; + } + } + + if (hideSimilar && this->message_->flags.has(MessageFlag::Similar)) { continue; } - if (getSettings()->hideSimilar && - this->message_->flags.has(MessageFlag::Similar)) - { - continue; - } - - if (!this->renderReplies_ && + if (hideReplies && element->getFlags().has(MessageElementFlag::RepliedMessage)) { continue; } - element->addToContainer(*this->container_, flags); + element->addToContainer(this->container_, flags); } - if (this->height_ != this->container_->getHeight()) + if (this->height_ != this->container_.getHeight()) { this->deleteBuffer(); } - this->container_->end(); - this->height_ = this->container_->getHeight(); + this->container_.endLayout(); + this->height_ = this->container_.getHeight(); // collapsed state this->flags.unset(MessageLayoutFlag::Collapsed); - if (this->container_->isCollapsed()) + if (this->container_.isCollapsed()) { this->flags.set(MessageLayoutFlag::Collapsed); } } // Painting -void MessageLayout::paint(QPainter &painter, int width, int y, int messageIndex, - Selection &selection, bool isLastReadMessage, - bool isWindowFocused, bool isMentions) +MessagePaintResult MessageLayout::paint(const MessagePaintContext &ctx) { - auto app = getApp(); - QPixmap *pixmap = this->buffer_.get(); + MessagePaintResult result; - // create new buffer if required - if (!pixmap) + QPixmap *pixmap = this->ensureBuffer(ctx.painter, ctx.canvasWidth); + + if (!this->bufferValid_) { -#if defined(Q_OS_MACOS) || defined(Q_OS_LINUX) - pixmap = new QPixmap(int(width * painter.device()->devicePixelRatioF()), - int(container_->getHeight() * - painter.device()->devicePixelRatioF())); - pixmap->setDevicePixelRatio(painter.device()->devicePixelRatioF()); -#else - pixmap = - new QPixmap(width, std::max(16, this->container_->getHeight())); -#endif - - this->buffer_ = std::shared_ptr(pixmap); - this->bufferValid_ = false; - DebugCount::increase("message drawing buffers"); - } - - if (!this->bufferValid_ || !selection.isEmpty()) - { - this->updateBuffer(pixmap, messageIndex, selection); + this->updateBuffer(pixmap, ctx); } // draw on buffer - painter.drawPixmap(0, y, *pixmap); - // painter.drawPixmap(0, y, this->container.width, - // this->container.getHeight(), *pixmap); + ctx.painter.drawPixmap(0, ctx.y, *pixmap); // draw gif emotes - this->container_->paintAnimatedElements(painter, y); + result.hasAnimatedElements = + this->container_.paintAnimatedElements(ctx.painter, ctx.y); // draw disabled if (this->message_->flags.has(MessageFlag::Disabled)) { - painter.fillRect(0, y, pixmap->width(), pixmap->height(), - app->themes->messages.disabled); - // painter.fillRect(0, y, pixmap->width(), pixmap->height(), - // QBrush(QColor(64, 64, 64, 64))); + ctx.painter.fillRect(0, ctx.y, pixmap->width(), pixmap->height(), + ctx.messageColors.disabled); } if (this->message_->flags.has(MessageFlag::RecentMessage)) { - painter.fillRect(0, y, pixmap->width(), pixmap->height(), - app->themes->messages.disabled); + ctx.painter.fillRect(0, ctx.y, pixmap->width(), pixmap->height(), + ctx.messageColors.disabled); } - if (!isMentions && + if (!ctx.isMentions && (this->message_->flags.has(MessageFlag::RedeemedChannelPointReward) || this->message_->flags.has(MessageFlag::RedeemedHighlight)) && - getSettings()->enableRedeemedHighlight.getValue()) + ctx.preferences.enableRedeemedHighlight) { - painter.fillRect( - 0, y, this->scale_ * 4, pixmap->height(), + ctx.painter.fillRect( + 0, ctx.y, int(this->scale_ * 4), pixmap->height(), *ColorProvider::instance().color(ColorType::RedeemedHighlight)); } // draw selection - if (!selection.isEmpty()) + if (!ctx.selection.isEmpty()) { - this->container_->paintSelection(painter, messageIndex, selection, y); + this->container_.paintSelection(ctx.painter, ctx.messageIndex, + ctx.selection, ctx.y); } // draw message seperation line - if (getSettings()->separateMessages.getValue()) + if (ctx.preferences.separateMessages) { - painter.fillRect(0, y, this->container_->getWidth() + 64, 1, - app->themes->splits.messageSeperator); + ctx.painter.fillRect(0, ctx.y, this->container_.getWidth() + 64, 1, + ctx.messageColors.messageSeperator); } // draw last read message line - if (isLastReadMessage) + if (ctx.isLastReadMessage) { QColor color; - if (getSettings()->lastMessageColor != "") + if (ctx.preferences.lastMessageColor.isValid()) { - color = QColor(getSettings()->lastMessageColor.getValue()); + color = ctx.preferences.lastMessageColor; } else { - color = - isWindowFocused - ? app->themes->tabs.selected.backgrounds.regular.color() - : app->themes->tabs.selected.backgrounds.unfocused.color(); + color = ctx.isWindowFocused + ? ctx.messageColors.focusedLastMessageLine + : ctx.messageColors.unfocusedLastMessageLine; } - QBrush brush(color, static_cast( - getSettings()->lastMessagePattern.getValue())); + QBrush brush(color, ctx.preferences.lastMessagePattern); - painter.fillRect(0, y + this->container_->getHeight() - 1, - pixmap->width(), 1, brush); + ctx.painter.fillRect(0, ctx.y + this->container_.getHeight() - 1, + pixmap->width(), 1, brush); } this->bufferValid_ = true; + + return result; } -void MessageLayout::updateBuffer(QPixmap *buffer, int /*messageIndex*/, - Selection & /*selection*/) +QPixmap *MessageLayout::ensureBuffer(QPainter &painter, int width) +{ + if (this->buffer_ != nullptr) + { + return this->buffer_.get(); + } + + // Create new buffer + this->buffer_ = std::make_unique( + int(width * painter.device()->devicePixelRatioF()), + int(this->container_.getHeight() * + painter.device()->devicePixelRatioF())); + this->buffer_->setDevicePixelRatio(painter.device()->devicePixelRatioF()); + + this->bufferValid_ = false; + DebugCount::increase("message drawing buffers"); + return this->buffer_.get(); +} + +void MessageLayout::updateBuffer(QPixmap *buffer, + const MessagePaintContext &ctx) { if (buffer->isNull()) + { return; - - auto app = getApp(); - auto settings = getSettings(); + } QPainter painter(buffer); painter.setRenderHint(QPainter::SmoothPixmapTransform); // draw background - QColor backgroundColor = [this, &app] { - if (getSettings()->alternateMessages.getValue() && + QColor backgroundColor = [&] { + if (ctx.preferences.alternateMessages && this->flags.has(MessageLayoutFlag::AlternateBackground)) { - return app->themes->messages.backgrounds.alternate; - } - else - { - return app->themes->messages.backgrounds.regular; + return ctx.messageColors.alternate; } + + return ctx.messageColors.regular; }(); - if (this->message_->flags.has(MessageFlag::FirstMessage) && - getSettings()->enableFirstMessageHighlight.getValue()) + if (this->message_->flags.has(MessageFlag::ElevatedMessage) && + ctx.preferences.enableElevatedMessageHighlight) { backgroundColor = blendColors( backgroundColor, - *ColorProvider::instance().color(ColorType::FirstMessageHighlight)); + *ctx.colorProvider.color(ColorType::ElevatedMessageHighlight)); + } + + else if (this->message_->flags.has(MessageFlag::FirstMessage) && + ctx.preferences.enableFirstMessageHighlight) + { + backgroundColor = blendColors( + backgroundColor, + *ctx.colorProvider.color(ColorType::FirstMessageHighlight)); } else if ((this->message_->flags.has(MessageFlag::Highlighted) || this->message_->flags.has(MessageFlag::HighlightedWhisper)) && !this->flags.has(MessageLayoutFlag::IgnoreHighlights)) { - // Blend highlight color with usual background color - backgroundColor = - blendColors(backgroundColor, *this->message_->highlightColor); + assert(this->message_->highlightColor); + if (this->message_->highlightColor) + { + // Blend highlight color with usual background color + backgroundColor = + blendColors(backgroundColor, *this->message_->highlightColor); + } } else if (this->message_->flags.has(MessageFlag::Subscription) && - getSettings()->enableSubHighlight) + ctx.preferences.enableSubHighlight) { // Blend highlight color with usual background color backgroundColor = blendColors( - backgroundColor, - *ColorProvider::instance().color(ColorType::Subscription)); + backgroundColor, *ctx.colorProvider.color(ColorType::Subscription)); } else if ((this->message_->flags.has(MessageFlag::RedeemedHighlight) || this->message_->flags.has( MessageFlag::RedeemedChannelPointReward)) && - settings->enableRedeemedHighlight.getValue()) + ctx.preferences.enableRedeemedHighlight) { // Blend highlight color with usual background color - backgroundColor = blendColors( - backgroundColor, - *ColorProvider::instance().color(ColorType::RedeemedHighlight)); + backgroundColor = + blendColors(backgroundColor, + *ctx.colorProvider.color(ColorType::RedeemedHighlight)); } - else if (this->message_->flags.has(MessageFlag::AutoMod)) + else if (this->message_->flags.has(MessageFlag::AutoMod) || + this->message_->flags.has(MessageFlag::LowTrustUsers)) { - backgroundColor = QColor("#404040"); + if (ctx.preferences.enableAutomodHighlight && + (this->message_->flags.has(MessageFlag::AutoModOffendingMessage) || + this->message_->flags.has( + MessageFlag::AutoModOffendingMessageHeader))) + { + backgroundColor = blendColors( + backgroundColor, + *ctx.colorProvider.color(ColorType::AutomodHighlight)); + } + else + { + backgroundColor = QColor("#404040"); + } } else if (this->message_->flags.has(MessageFlag::Debug)) { @@ -350,7 +388,7 @@ void MessageLayout::updateBuffer(QPixmap *buffer, int /*messageIndex*/, painter.fillRect(buffer->rect(), backgroundColor); // draw message - this->container_->paintElements(painter); + this->container_.paintElements(painter, ctx); #ifdef FOURTF // debug @@ -361,7 +399,7 @@ void MessageLayout::updateBuffer(QPixmap *buffer, int /*messageIndex*/, QTextOption option; option.setAlignment(Qt::AlignRight | Qt::AlignTop); - painter.drawText(QRectF(1, 1, this->container_->getWidth() - 3, 1000), + painter.drawText(QRectF(1, 1, this->container_.getWidth() - 3, 1000), QString::number(this->layoutCount_) + ", " + QString::number(++this->bufferUpdatedCount_), option); @@ -388,7 +426,7 @@ void MessageLayout::deleteCache() this->deleteBuffer(); #ifdef XD - this->container_->clear(); + this->container_.clear(); #endif } @@ -398,31 +436,50 @@ void MessageLayout::deleteCache() // returns nullptr if none was found // fourtf: this should return a MessageLayoutItem -const MessageLayoutElement *MessageLayout::getElementAt(QPoint point) +const MessageLayoutElement *MessageLayout::getElementAt(QPoint point) const { // go through all words and return the first one that contains the point. - return this->container_->getElementAt(point); + return this->container_.getElementAt(point); } -int MessageLayout::getLastCharacterIndex() const +std::pair MessageLayout::getWordBounds( + const MessageLayoutElement *hoveredElement, QPoint relativePos) const { - return this->container_->getLastCharacterIndex(); + // An element with wordId != -1 can be multiline, so we need to check all + // elements in the container + if (hoveredElement->getWordId() != -1) + { + return this->container_.getWordBounds(hoveredElement); + } + + const auto wordStart = this->getSelectionIndex(relativePos) - + hoveredElement->getMouseOverIndex(relativePos); + const auto selectionLength = hoveredElement->getSelectionIndexCount(); + const auto length = hoveredElement->hasTrailingSpace() ? selectionLength - 1 + : selectionLength; + + return {wordStart, wordStart + length}; } -int MessageLayout::getFirstMessageCharacterIndex() const +size_t MessageLayout::getLastCharacterIndex() const { - return this->container_->getFirstMessageCharacterIndex(); + return this->container_.getLastCharacterIndex(); } -int MessageLayout::getSelectionIndex(QPoint position) +size_t MessageLayout::getFirstMessageCharacterIndex() const { - return this->container_->getSelectionIndex(position); + return this->container_.getFirstMessageCharacterIndex(); } -void MessageLayout::addSelectionText(QString &str, int from, int to, +size_t MessageLayout::getSelectionIndex(QPoint position) const +{ + return this->container_.getSelectionIndex(position); +} + +void MessageLayout::addSelectionText(QString &str, uint32_t from, uint32_t to, CopyMode copymode) { - this->container_->addSelectionText(str, from, to, copymode); + this->container_.addSelectionText(str, from, to, copymode); } bool MessageLayout::isReplyable() const @@ -442,9 +499,4 @@ bool MessageLayout::isReplyable() const return true; } -void MessageLayout::setRenderReplies(bool render) -{ - this->renderReplies_ = render; -} - } // namespace chatterino diff --git a/src/messages/layouts/MessageLayout.hpp b/src/messages/layouts/MessageLayout.hpp index 33c1fad72..12a8b153a 100644 --- a/src/messages/layouts/MessageLayout.hpp +++ b/src/messages/layouts/MessageLayout.hpp @@ -2,9 +2,10 @@ #include "common/Common.hpp" #include "common/FlagsEnum.hpp" +#include "messages/layouts/MessageLayoutContainer.hpp" #include -#include + #include #include @@ -16,6 +17,7 @@ using MessagePtr = std::shared_ptr; struct Selection; struct MessageLayoutContainer; class MessageLayoutElement; +struct MessagePaintContext; enum class MessageElementFlag : int64_t; using MessageElementFlags = FlagsEnum; @@ -30,65 +32,110 @@ enum class MessageLayoutFlag : uint8_t { }; using MessageLayoutFlags = FlagsEnum; -class MessageLayout : boost::noncopyable +struct MessagePaintResult { + bool hasAnimatedElements = false; +}; + +class MessageLayout { public: MessageLayout(MessagePtr message_); ~MessageLayout(); + MessageLayout(const MessageLayout &) = delete; + MessageLayout &operator=(const MessageLayout &) = delete; + + MessageLayout(MessageLayout &&) = delete; + MessageLayout &operator=(MessageLayout &&) = delete; + const Message *getMessage(); const MessagePtr &getMessagePtr() const; int getHeight() const; + int getWidth() const; MessageLayoutFlags flags; - bool layout(int width, float scale_, MessageElementFlags flags); + bool layout(int width, float scale_, float imageScale, + MessageElementFlags flags, bool shouldInvalidateBuffer); // Painting - void paint(QPainter &painter, int width, int y, int messageIndex, - Selection &selection, bool isLastReadMessage, - bool isWindowFocused, bool isMentions); + MessagePaintResult paint(const MessagePaintContext &ctx); void invalidateBuffer(); void deleteBuffer(); void deleteCache(); - // Elements - const MessageLayoutElement *getElementAt(QPoint point); - int getLastCharacterIndex() const; - int getFirstMessageCharacterIndex() const; - int getSelectionIndex(QPoint position); - void addSelectionText(QString &str, int from = 0, int to = INT_MAX, + /** + * Returns a raw pointer to the element at the given point + * + * If no element is found at the given point, this returns a null pointer + */ + const MessageLayoutElement *getElementAt(QPoint point) const; + + /** + * @brief Returns the word bounds of the given element + * + * The first value is the index of the first character in the word, + * the second value is the index of the character after the last character in the word. + * + * Given the word "abc" by itself, we would return (0, 3) + * + * V V + * "abc " + */ + std::pair getWordBounds( + const MessageLayoutElement *hoveredElement, QPoint relativePos) const; + + /** + * Get the index of the last character in this message's container + * This is the sum of all the characters in `elements_` + */ + size_t getLastCharacterIndex() const; + + /** + * Get the index of the first visible character in this message's container + * This is not always 0 in case there elements that are skipped + */ + size_t getFirstMessageCharacterIndex() const; + + /** + * Get the character index at the given position, in the context of selections + */ + size_t getSelectionIndex(QPoint position) const; + void addSelectionText(QString &str, uint32_t from = 0, + uint32_t to = UINT32_MAX, CopyMode copymode = CopyMode::Everything); // Misc bool isDisabled() const; bool isReplyable() const; - void setRenderReplies(bool render); private: + // methods + void actuallyLayout(int width, MessageElementFlags flags); + void updateBuffer(QPixmap *buffer, const MessagePaintContext &ctx); + + // Create new buffer if required, returning the buffer + QPixmap *ensureBuffer(QPainter &painter, int width); + // variables - MessagePtr message_; - std::shared_ptr container_; - std::shared_ptr buffer_{}; + const MessagePtr message_; + MessageLayoutContainer container_; + std::unique_ptr buffer_; bool bufferValid_ = false; - bool renderReplies_ = true; int height_ = 0; - int currentLayoutWidth_ = -1; int layoutState_ = -1; float scale_ = -1; - unsigned int layoutCount_ = 0; - unsigned int bufferUpdatedCount_ = 0; - + float imageScale_ = -1.F; MessageElementFlags currentWordFlags_; - int collapsedHeight_ = 32; - - // methods - void actuallyLayout(int width, MessageElementFlags flags); - void updateBuffer(QPixmap *pixmap, int messageIndex, Selection &selection); +#ifdef FOURTF + // Debug counters + unsigned int layoutCount_ = 0; + unsigned int bufferUpdatedCount_ = 0; +#endif }; using MessageLayoutPtr = std::shared_ptr; diff --git a/src/messages/layouts/MessageLayoutContainer.cpp b/src/messages/layouts/MessageLayoutContainer.cpp index eefc88ae0..0e84d3252 100644 --- a/src/messages/layouts/MessageLayoutContainer.cpp +++ b/src/messages/layouts/MessageLayoutContainer.cpp @@ -1,294 +1,70 @@ -#include "MessageLayoutContainer.hpp" +#include "messages/layouts/MessageLayoutContainer.hpp" #include "Application.hpp" +#include "messages/layouts/MessageLayoutContext.hpp" +#include "messages/layouts/MessageLayoutElement.hpp" #include "messages/Message.hpp" #include "messages/MessageElement.hpp" #include "messages/Selection.hpp" -#include "messages/layouts/MessageLayoutElement.hpp" #include "singletons/Fonts.hpp" #include "singletons/Settings.hpp" #include "singletons/Theme.hpp" +#include "util/Helpers.hpp" #include +#include #include +#include -#define COMPACT_EMOTES_OFFSET 4 -#define MAX_UNCOLLAPSED_LINES \ - (getSettings()->collpseMessagesMinLines.getValue()) +#include + +namespace { + +using namespace chatterino; + +constexpr QMargins MARGIN{8, 4, 8, 4}; +constexpr int COMPACT_EMOTES_OFFSET = 4; + +int maxUncollapsedLines() +{ + return getSettings()->collpseMessagesMinLines.getValue(); +} + +} // namespace namespace chatterino { -int MessageLayoutContainer::getHeight() const -{ - return this->height_; -} - -int MessageLayoutContainer::getWidth() const -{ - return this->width_; -} - -float MessageLayoutContainer::getScale() const -{ - return this->scale_; -} - -// methods -void MessageLayoutContainer::begin(int width, float scale, MessageFlags flags) -{ - this->clear(); - this->width_ = width; - this->scale_ = scale; - this->flags_ = flags; - auto mediumFontMetrics = - getApp()->fonts->getFontMetrics(FontStyle::ChatMedium, scale); - this->textLineHeight_ = mediumFontMetrics.height(); - this->spaceWidth_ = mediumFontMetrics.horizontalAdvance(' '); - this->dotdotdotWidth_ = mediumFontMetrics.horizontalAdvance("..."); - this->canAddMessages_ = true; - this->isCollapsed_ = false; -} - -void MessageLayoutContainer::clear() +void MessageLayoutContainer::beginLayout(int width, float scale, + float imageScale, MessageFlags flags) { this->elements_.clear(); this->lines_.clear(); - this->height_ = 0; this->line_ = 0; this->currentX_ = 0; this->currentY_ = 0; this->lineStart_ = 0; this->lineHeight_ = 0; this->charIndex_ = 0; + + this->width_ = width; + this->height_ = 0; + this->scale_ = scale; + this->imageScale_ = imageScale; + this->flags_ = flags; + auto mediumFontMetrics = + getApp()->getFonts()->getFontMetrics(FontStyle::ChatMedium, scale); + this->textLineHeight_ = mediumFontMetrics.height(); + this->spaceWidth_ = mediumFontMetrics.horizontalAdvance(' '); + this->dotdotdotWidth_ = mediumFontMetrics.horizontalAdvance("..."); + this->currentWordId_ = 0; + this->canAddMessages_ = true; + this->isCollapsed_ = false; + this->lineContainsRTL_ = false; + this->anyReorderingDone_ = false; } -void MessageLayoutContainer::addElement(MessageLayoutElement *element) -{ - bool isZeroWidth = - element->getFlags().has(MessageElementFlag::ZeroWidthEmote); - - if (!isZeroWidth && !this->fitsInLine(element->getRect().width())) - { - this->breakLine(); - } - - this->_addElement(element); -} - -void MessageLayoutContainer::addElementNoLineBreak( - MessageLayoutElement *element) -{ - this->_addElement(element); -} - -bool MessageLayoutContainer::canAddElements() -{ - return this->canAddMessages_; -} - -void MessageLayoutContainer::_addElement(MessageLayoutElement *element, - bool forceAdd) -{ - if (!this->canAddElements() && !forceAdd) - { - delete element; - return; - } - - // This lambda contains the logic for when to step one 'space width' back for compact x emotes - auto shouldRemoveSpaceBetweenEmotes = [this]() -> bool { - if (this->elements_.empty()) - { - // No previous element found - return false; - } - - const auto &lastElement = this->elements_.back(); - - if (!lastElement) - { - return false; - } - - if (!lastElement->hasTrailingSpace()) - { - // Last element did not have a trailing space, so we don't need to do anything. - return false; - } - - if (lastElement->getLine() != this->line_) - { - // Last element was not on the same line as us - return false; - } - - // Returns true if the last element was an emote image - return lastElement->getFlags().has(MessageElementFlag::EmoteImages); - }; - - // top margin - if (this->elements_.size() == 0) - { - this->currentY_ = int(this->margin.top * this->scale_); - } - - int newLineHeight = element->getRect().height(); - - // compact emote offset - bool isCompactEmote = - getSettings()->compactEmotes && - !this->flags_.has(MessageFlag::DisableCompactEmotes) && - element->getCreator().getFlags().has(MessageElementFlag::EmoteImages); - - if (isCompactEmote) - { - newLineHeight -= COMPACT_EMOTES_OFFSET * this->scale_; - } - - // update line height - this->lineHeight_ = std::max(this->lineHeight_, newLineHeight); - - auto xOffset = 0; - bool isZeroWidthEmote = element->getCreator().getFlags().has( - MessageElementFlag::ZeroWidthEmote); - - if (isZeroWidthEmote) - { - xOffset -= element->getRect().width() + this->spaceWidth_; - } - - auto yOffset = 0; - - if (element->getCreator().getFlags().has( - MessageElementFlag::ChannelPointReward) && - element->getCreator().getFlags().hasNone( - {MessageElementFlag::TwitchEmoteImage})) - { - yOffset -= (this->margin.top * this->scale_); - } - - if (getSettings()->removeSpacesBetweenEmotes && - element->getFlags().hasAny({MessageElementFlag::EmoteImages}) && - !isZeroWidthEmote && shouldRemoveSpaceBetweenEmotes()) - { - // Move cursor one 'space width' to the left to combine hug the previous emote - this->currentX_ -= this->spaceWidth_; - } - - // set move element - element->setPosition( - QPoint(this->currentX_ + xOffset, - this->currentY_ - element->getRect().height() + yOffset)); - - element->setLine(this->line_); - - // add element - this->elements_.push_back(std::unique_ptr(element)); - - // set current x - if (!isZeroWidthEmote) - { - this->currentX_ += element->getRect().width(); - } - - if (element->hasTrailingSpace()) - { - this->currentX_ += this->spaceWidth_; - } -} - -void MessageLayoutContainer::breakLine() -{ - int xOffset = 0; - - if (this->flags_.has(MessageFlag::Centered) && this->elements_.size() > 0) - { - const int marginOffset = int(this->margin.left * this->scale_) + - int(this->margin.right * this->scale_); - xOffset = (width_ - marginOffset - - this->elements_.at(this->elements_.size() - 1) - ->getRect() - .right()) / - 2; - } - - for (size_t i = lineStart_; i < this->elements_.size(); i++) - { - MessageLayoutElement *element = this->elements_.at(i).get(); - - bool isCompactEmote = - getSettings()->compactEmotes && - !this->flags_.has(MessageFlag::DisableCompactEmotes) && - element->getCreator().getFlags().has( - MessageElementFlag::EmoteImages); - - int yExtra = 0; - if (isCompactEmote) - { - yExtra = (COMPACT_EMOTES_OFFSET / 2) * this->scale_; - } - - // if (element->getCreator().getFlags() & - // MessageElementFlag::Badges) - // { - if (element->getRect().height() < this->textLineHeight_) - { - // yExtra -= (this->textLineHeight_ - element->getRect().height()) / - // 2; - } - - element->setPosition( - QPoint(element->getRect().x() + xOffset + - int(this->margin.left * this->scale_), - element->getRect().y() + this->lineHeight_ + yExtra)); - } - - if (this->lines_.size() != 0) - { - this->lines_.back().endIndex = this->lineStart_; - this->lines_.back().endCharIndex = this->charIndex_; - } - this->lines_.push_back( - {(int)lineStart_, 0, this->charIndex_, 0, - QRect(-100000, this->currentY_, 200000, lineHeight_)}); - - for (int i = this->lineStart_; i < this->elements_.size(); i++) - { - this->charIndex_ += this->elements_[i]->getSelectionIndexCount(); - } - - this->lineStart_ = this->elements_.size(); - // this->currentX = (int)(this->scale * 8); - - if (this->canCollapse() && line_ + 1 >= MAX_UNCOLLAPSED_LINES) - { - this->canAddMessages_ = false; - return; - } - - this->currentX_ = 0; - this->currentY_ += this->lineHeight_; - this->height_ = this->currentY_ + int(this->margin.bottom * this->scale_); - this->lineHeight_ = 0; - this->line_++; -} - -bool MessageLayoutContainer::atStartOfLine() -{ - return this->lineStart_ == this->elements_.size(); -} - -bool MessageLayoutContainer::fitsInLine(int _width) -{ - return this->currentX_ + _width <= - (this->width_ - int(this->margin.left * this->scale_) - - int(this->margin.right * this->scale_) - - (this->line_ + 1 == MAX_UNCOLLAPSED_LINES ? this->dotdotdotWidth_ - : 0)); -} - -void MessageLayoutContainer::end() +void MessageLayoutContainer::endLayout() { if (!this->canAddElements()) { @@ -301,8 +77,18 @@ void MessageLayoutContainer::end() QSize(this->dotdotdotWidth_, this->textLineHeight_), QColor("#00D80A"), FontStyle::ChatMediumBold, this->scale_); - // getApp()->themes->messages.textColors.system - this->_addElement(element, true); + if (this->isRTL()) + { + // Shift all elements in the next line to the left + for (auto i = this->lines_.back().startIndex; + i < this->elements_.size(); i++) + { + QPoint prevPos = this->elements_[i]->getRect().topLeft(); + this->elements_[i]->setPosition( + QPoint(prevPos.x() + this->dotdotdotWidth_, prevPos.y())); + } + } + this->addElement(element, true, -2); this->isCollapsed_ = true; } @@ -313,29 +99,292 @@ void MessageLayoutContainer::end() this->height_ += this->lineHeight_; - if (this->lines_.size() != 0) + if (!this->lines_.empty()) { this->lines_[0].rect.setTop(-100000); this->lines_.back().rect.setBottom(100000); this->lines_.back().endIndex = this->elements_.size(); this->lines_.back().endCharIndex = this->charIndex_; } + + if (!this->elements_.empty()) + { + this->elements_.back()->setTrailingSpace(false); + } + + if (this->anyReorderingDone_) + { + std::ranges::sort(this->elements_, [](const auto &a, const auto &b) { + if (a->getLine() == b->getLine()) + { + return a->getRect().x() < b->getRect().x(); + } + return a->getLine() < b->getLine(); + }); + } } -bool MessageLayoutContainer::canCollapse() +void MessageLayoutContainer::addElement(MessageLayoutElement *element) { - return getSettings()->collpseMessagesMinLines.getValue() > 0 && - this->flags_.has(MessageFlag::Collapsed); + if (!this->fitsInLine(element->getRect().width())) + { + this->breakLine(); + } + + this->addElement(element, false, -2); } -bool MessageLayoutContainer::isCollapsed() +void MessageLayoutContainer::addElementNoLineBreak( + MessageLayoutElement *element) { - return this->isCollapsed_; + this->addElement(element, false, -2); } -MessageLayoutElement *MessageLayoutContainer::getElementAt(QPoint point) +void MessageLayoutContainer::breakLine() { - for (std::unique_ptr &element : this->elements_) + if (this->lineContainsRTL_ || this->isRTL()) + { + for (size_t i = 0; i < this->elements_.size(); i++) + { + if (this->elements_[i]->getFlags().has( + MessageElementFlag::Username)) + { + this->reorderRTL(i + 1); + break; + } + } + this->lineContainsRTL_ = false; + this->anyReorderingDone_ = true; + } + + int xOffset = 0; + + if (this->flags_.has(MessageFlag::Centered) && this->elements_.size() > 0) + { + const int marginOffset = int(MARGIN.left() * this->scale_) + + int(MARGIN.right() * this->scale_); + xOffset = (width_ - marginOffset - + this->elements_.at(this->elements_.size() - 1) + ->getRect() + .right()) / + 2; + } + + for (size_t i = lineStart_; i < this->elements_.size(); i++) + { + MessageLayoutElement *element = this->elements_.at(i).get(); + + bool isCompactEmote = + !this->flags_.has(MessageFlag::DisableCompactEmotes) && + element->getCreator().getFlags().has( + MessageElementFlag::EmoteImages); + + int yExtra = 0; + if (isCompactEmote) + { + yExtra = (COMPACT_EMOTES_OFFSET / 2) * this->scale_; + } + + element->setPosition( + QPoint(element->getRect().x() + xOffset + + int(MARGIN.left() * this->scale_), + element->getRect().y() + this->lineHeight_ + yExtra)); + } + + if (!this->lines_.empty()) + { + this->lines_.back().endIndex = this->lineStart_; + this->lines_.back().endCharIndex = this->charIndex_; + } + this->lines_.push_back({ + .startIndex = lineStart_, + .endIndex = 0, + .startCharIndex = this->charIndex_, + .endCharIndex = 0, + .rect = QRect(-100000, this->currentY_, 200000, lineHeight_), + }); + + for (auto i = this->lineStart_; i < this->elements_.size(); i++) + { + this->charIndex_ += this->elements_[i]->getSelectionIndexCount(); + } + + this->lineStart_ = this->elements_.size(); + // this->currentX = (int)(this->scale * 8); + + if (this->canCollapse() && this->line_ + 1 >= maxUncollapsedLines()) + { + this->canAddMessages_ = false; + return; + } + + this->currentX_ = 0; + this->currentY_ += this->lineHeight_; + this->height_ = this->currentY_ + int(MARGIN.bottom() * this->scale_); + this->lineHeight_ = 0; + this->line_++; +} + +void MessageLayoutContainer::paintElements(QPainter &painter, + const MessagePaintContext &ctx) const +{ +#ifdef FOURTF + static constexpr std::array lineColors{ + QColor{255, 0, 0, 60}, // RED + QColor{0, 255, 0, 60}, // GREEN + QColor{0, 0, 255, 60}, // BLUE + QColor{255, 0, 255, 60}, // PINk + QColor{0, 255, 255, 60}, // CYAN + }; + + int lineNum = 0; + for (const auto &line : this->lines_) + { + const auto &color = lineColors[lineNum++ % 5]; + painter.fillRect(line.rect, color); + } +#endif + + for (const auto &element : this->elements_) + { +#ifdef FOURTF + painter.setPen(QColor(0, 255, 0)); + painter.drawRect(element->getRect()); +#endif + + painter.save(); + element->paint(painter, ctx.messageColors); + painter.restore(); + } +} + +bool MessageLayoutContainer::paintAnimatedElements(QPainter &painter, + int yOffset) const +{ + bool anyAnimatedElement = false; + for (const auto &element : this->elements_) + { + anyAnimatedElement |= element->paintAnimated(painter, yOffset); + } + return anyAnimatedElement; +} + +void MessageLayoutContainer::paintSelection(QPainter &painter, + const size_t messageIndex, + const Selection &selection, + const int yOffset) const +{ + if (selection.selectionMin.messageIndex > messageIndex || + selection.selectionMax.messageIndex < messageIndex) + { + // This message is not part of the selection, don't draw anything + return; + } + + const auto selectionColor = getTheme()->messages.selection; + + if (selection.selectionMin.messageIndex < messageIndex && + selection.selectionMax.messageIndex > messageIndex) + { + // The selection fully covers this message + // Paint all lines completely + + for (const Line &line : this->lines_) + { + // Fully paint a selection rectangle over all lines + auto left = this->elements_[line.startIndex]->getRect().left(); + auto right = this->elements_[line.endIndex - 1]->getRect().right(); + this->paintSelectionRect(painter, line, left, right, yOffset, + selectionColor); + } + + return; + } + + size_t lineIndex = 0; + + if (selection.selectionMin.messageIndex == messageIndex) + { + auto oLineIndex = this->paintSelectionStart(painter, messageIndex, + selection, yOffset); + + if (!oLineIndex) + { + // There's no more selection to be drawn in this message + return; + } + + // There's further selection to be painted in this message + lineIndex = *oLineIndex; + } + + // Paint the selection starting at lineIndex + this->paintSelectionEnd(painter, lineIndex, selection, yOffset); +} + +void MessageLayoutContainer::addSelectionText(QString &str, uint32_t from, + uint32_t to, + CopyMode copymode) const +{ + uint32_t index = 0; + bool first = true; + + for (const auto &element : this->elements_) + { + if (copymode != CopyMode::Everything && + element->getCreator().getFlags().has( + MessageElementFlag::RepliedMessage)) + { + // Don't include the message being replied to + continue; + } + + if (copymode == CopyMode::OnlyTextAndEmotes) + { + if (element->getCreator().getFlags().hasAny({ + MessageElementFlag::Timestamp, + MessageElementFlag::Username, + MessageElementFlag::Badges, + MessageElementFlag::ChannelName, + })) + { + continue; + } + } + + auto indexCount = element->getSelectionIndexCount(); + + if (first) + { + if (index + indexCount > from) + { + element->addCopyTextToString(str, from - index, to - index); + first = false; + + if (index + indexCount >= to) + { + break; + } + } + } + else + { + if (index + indexCount >= to) + { + element->addCopyTextToString(str, 0, to - index); + break; + } + + element->addCopyTextToString(str); + } + + index += indexCount; + } +} + +MessageLayoutElement *MessageLayoutContainer::getElementAt(QPoint point) const +{ + for (const auto &element : this->elements_) { if (element->getRect().contains(point)) { @@ -346,222 +395,9 @@ MessageLayoutElement *MessageLayoutContainer::getElementAt(QPoint point) return nullptr; } -// painting -void MessageLayoutContainer::paintElements(QPainter &painter) +size_t MessageLayoutContainer::getSelectionIndex(QPoint point) const { - for (const std::unique_ptr &element : this->elements_) - { -#ifdef FOURTF - painter.setPen(QColor(0, 255, 0)); - painter.drawRect(element->getRect()); -#endif - - element->paint(painter); - } -} - -void MessageLayoutContainer::paintAnimatedElements(QPainter &painter, - int yOffset) -{ - for (const std::unique_ptr &element : this->elements_) - { - element->paintAnimated(painter, yOffset); - } -} - -void MessageLayoutContainer::paintSelection(QPainter &painter, int messageIndex, - Selection &selection, int yOffset) -{ - auto app = getApp(); - QColor selectionColor = app->themes->messages.selection; - - // don't draw anything - if (selection.selectionMin.messageIndex > messageIndex || - selection.selectionMax.messageIndex < messageIndex) - { - return; - } - - // fully selected - if (selection.selectionMin.messageIndex < messageIndex && - selection.selectionMax.messageIndex > messageIndex) - { - for (Line &line : this->lines_) - { - QRect rect = line.rect; - - rect.setTop(std::max(0, rect.top()) + yOffset); - rect.setBottom(std::min(this->height_, rect.bottom()) + yOffset); - rect.setLeft(this->elements_[line.startIndex]->getRect().left()); - rect.setRight( - this->elements_[line.endIndex - 1]->getRect().right()); - - painter.fillRect(rect, selectionColor); - } - return; - } - - int lineIndex = 0; - int index = 0; - - // start in this message - if (selection.selectionMin.messageIndex == messageIndex) - { - for (; lineIndex < this->lines_.size(); lineIndex++) - { - Line &line = this->lines_[lineIndex]; - index = line.startCharIndex; - - bool returnAfter = false; - bool breakAfter = false; - int x = this->elements_[line.startIndex]->getRect().left(); - int r = this->elements_[line.endIndex - 1]->getRect().right(); - - if (line.endCharIndex <= selection.selectionMin.charIndex) - { - continue; - } - - for (int i = line.startIndex; i < line.endIndex; i++) - { - int c = this->elements_[i]->getSelectionIndexCount(); - - if (index + c > selection.selectionMin.charIndex) - { - x = this->elements_[i]->getXFromIndex( - selection.selectionMin.charIndex - index); - - // ends in same line - if (selection.selectionMax.messageIndex == messageIndex && - line.endCharIndex > - /*=*/selection.selectionMax.charIndex) - { - returnAfter = true; - index = line.startCharIndex; - for (int i = line.startIndex; i < line.endIndex; i++) - { - int c = - this->elements_[i]->getSelectionIndexCount(); - - if (index + c > selection.selectionMax.charIndex) - { - r = this->elements_[i]->getXFromIndex( - selection.selectionMax.charIndex - index); - break; - } - index += c; - } - } - // ends in same line end - - if (selection.selectionMax.messageIndex != messageIndex) - { - int lineIndex2 = lineIndex + 1; - for (; lineIndex2 < this->lines_.size(); lineIndex2++) - { - Line &line2 = this->lines_[lineIndex2]; - QRect rect = line2.rect; - - rect.setTop(std::max(0, rect.top()) + yOffset); - rect.setBottom( - std::min(this->height_, rect.bottom()) + - yOffset); - rect.setLeft(this->elements_[line2.startIndex] - ->getRect() - .left()); - rect.setRight(this->elements_[line2.endIndex - 1] - ->getRect() - .right()); - - painter.fillRect(rect, selectionColor); - } - returnAfter = true; - } - else - { - lineIndex++; - breakAfter = true; - } - - break; - } - index += c; - } - - QRect rect = line.rect; - - rect.setTop(std::max(0, rect.top()) + yOffset); - rect.setBottom(std::min(this->height_, rect.bottom()) + yOffset); - rect.setLeft(x); - rect.setRight(r); - - painter.fillRect(rect, selectionColor); - - if (returnAfter) - { - return; - } - - if (breakAfter) - { - break; - } - } - } - - // start in this message - for (; lineIndex < this->lines_.size(); lineIndex++) - { - Line &line = this->lines_[lineIndex]; - index = line.startCharIndex; - - // just draw the garbage - if (line.endCharIndex < /*=*/selection.selectionMax.charIndex) - { - QRect rect = line.rect; - - rect.setTop(std::max(0, rect.top()) + yOffset); - rect.setBottom(std::min(this->height_, rect.bottom()) + yOffset); - rect.setLeft(this->elements_[line.startIndex]->getRect().left()); - rect.setRight( - this->elements_[line.endIndex - 1]->getRect().right()); - - painter.fillRect(rect, selectionColor); - continue; - } - - int r = this->elements_[line.endIndex - 1]->getRect().right(); - - for (int i = line.startIndex; i < line.endIndex; i++) - { - int c = this->elements_[i]->getSelectionIndexCount(); - - if (index + c > selection.selectionMax.charIndex) - { - r = this->elements_[i]->getXFromIndex( - selection.selectionMax.charIndex - index); - break; - } - - index += c; - } - - QRect rect = line.rect; - - rect.setTop(std::max(0, rect.top()) + yOffset); - rect.setBottom(std::min(this->height_, rect.bottom()) + yOffset); - rect.setLeft(this->elements_[line.startIndex]->getRect().left()); - rect.setRight(r); - - painter.fillRect(rect, selectionColor); - break; - } -} - -// selection -int MessageLayoutContainer::getSelectionIndex(QPoint point) -{ - if (this->elements_.size() == 0) + if (this->elements_.empty()) { return 0; } @@ -576,18 +412,18 @@ int MessageLayoutContainer::getSelectionIndex(QPoint point) } } - int lineStart = line == this->lines_.end() ? this->lines_.back().startIndex - : line->startIndex; + auto lineStart = line == this->lines_.end() ? this->lines_.back().startIndex + : line->startIndex; if (line != this->lines_.end()) { line++; } - int lineEnd = + auto lineEnd = line == this->lines_.end() ? this->elements_.size() : line->startIndex; - int index = 0; + size_t index = 0; - for (int i = 0; i < lineEnd; i++) + for (size_t i = 0; i < lineEnd; i++) { auto &&element = this->elements_[i]; @@ -619,29 +455,19 @@ int MessageLayoutContainer::getSelectionIndex(QPoint point) return index; } -// fourtf: no idea if this is acurate LOL -int MessageLayoutContainer::getLastCharacterIndex() const +size_t MessageLayoutContainer::getFirstMessageCharacterIndex() const { - if (this->lines_.size() == 0) - { - return 0; - } - return this->lines_.back().endCharIndex; -} - -int MessageLayoutContainer::getFirstMessageCharacterIndex() const -{ - static FlagsEnum flags; - flags.set(MessageElementFlag::Username); - flags.set(MessageElementFlag::Timestamp); - flags.set(MessageElementFlag::Badges); + static const FlagsEnum skippedFlags{ + MessageElementFlag::RepliedMessage, MessageElementFlag::Timestamp, + MessageElementFlag::ModeratorTools, MessageElementFlag::Badges, + MessageElementFlag::Username, + }; // Get the index of the first character of the real message - // (no badges/timestamps/username) - int index = 0; - for (auto &element : this->elements_) + size_t index = 0; + for (const auto &element : this->elements_) { - if (element->getFlags().hasAny(flags)) + if (element->getFlags().hasAny(skippedFlags)) { index += element->getSelectionIndexCount(); } @@ -653,60 +479,550 @@ int MessageLayoutContainer::getFirstMessageCharacterIndex() const return index; } -void MessageLayoutContainer::addSelectionText(QString &str, int from, int to, - CopyMode copymode) +std::pair MessageLayoutContainer::getWordBounds( + const MessageLayoutElement *hoveredElement) const { - int index = 0; - bool first = true; - - for (auto &element : this->elements_) + if (this->elements_.empty()) { - if (copymode != CopyMode::Everything && - element->getCreator().getFlags().has( - MessageElementFlag::RepliedMessage)) + return {0, 0}; + } + + size_t index = 0; + size_t wordStart = 0; + + for (; index < this->elements_.size(); index++) + { + const auto &element = this->elements_[index]; + if (element->getWordId() == hoveredElement->getWordId()) { - // Don't include the message being replied to - continue; + break; } - if (copymode == CopyMode::OnlyTextAndEmotes) + wordStart += element->getSelectionIndexCount(); + } + + size_t wordEnd = wordStart; + + for (; index < this->elements_.size(); index++) + { + const auto &element = this->elements_[index]; + if (element->getWordId() != hoveredElement->getWordId()) { - if (element->getCreator().getFlags().hasAny( - {MessageElementFlag::Timestamp, - MessageElementFlag::Username, MessageElementFlag::Badges})) - continue; + break; } - auto indexCount = element->getSelectionIndexCount(); + wordEnd += element->getSelectionIndexCount(); + } - if (first) + const auto *lastElementInSelection = this->elements_[index - 1].get(); + if (lastElementInSelection->hasTrailingSpace()) + { + wordEnd--; + } + + return {wordStart, wordEnd}; +} + +size_t MessageLayoutContainer::getLastCharacterIndex() const +{ + if (this->lines_.empty()) + { + return 0; + } + + return this->lines_.back().endCharIndex; +} + +int MessageLayoutContainer::getWidth() const +{ + return this->width_; +} + +int MessageLayoutContainer::getHeight() const +{ + return this->height_; +} + +float MessageLayoutContainer::getScale() const +{ + return this->scale_; +} + +float MessageLayoutContainer::getImageScale() const +{ + return this->imageScale_; +} + +bool MessageLayoutContainer::isCollapsed() const +{ + return this->isCollapsed_; +} + +bool MessageLayoutContainer::atStartOfLine() const +{ + return this->lineStart_ == this->elements_.size(); +} + +bool MessageLayoutContainer::fitsInLine(int width) const +{ + return width <= this->remainingWidth(); +} + +int MessageLayoutContainer::remainingWidth() const +{ + return (this->width_ - int(MARGIN.left() * this->scale_) - + int(MARGIN.right() * this->scale_) - + (static_cast(this->line_ + 1) == maxUncollapsedLines() + ? this->dotdotdotWidth_ + : 0)) - + this->currentX_; +} + +int MessageLayoutContainer::nextWordId() +{ + return this->currentWordId_++; +} + +void MessageLayoutContainer::addElement(MessageLayoutElement *element, + const bool forceAdd, + const qsizetype prevIndex) +{ + if (!this->canAddElements() && !forceAdd) + { + assert(prevIndex == -2 && + "element is still referenced in this->elements_"); + delete element; + return; + } + + bool isAddingMode = prevIndex == -2; + bool isRTLAdjusting = this->isRTL() && !isAddingMode; + + /// Returns `true` if a previously added `spaceWidth_` should be removed + /// before the to be added emote. The space was inserted by the + /// previous element but isn't desired as "removeSpacesBetweenEmotes" is + /// enabled and both elements are emotes. + auto shouldRemoveSpaceBetweenEmotes = [this, prevIndex, + isAddingMode]() -> bool { + if (prevIndex == -1 || this->elements_.empty()) { - if (index + indexCount > from) - { - element->addCopyTextToString(str, from - index, to - index); - first = false; + // No previous element found + return false; + } - if (index + indexCount > to) - { - break; - } - } + const auto &lastElement = + isAddingMode ? this->elements_.back() : this->elements_[prevIndex]; + + if (!lastElement) + { + assert(false && "Empty element in container found"); + return false; + } + + if (!lastElement->hasTrailingSpace()) + { + // Last element did not have a trailing space, so we don't need to do anything. + return false; + } + + if (lastElement->getLine() != this->line_) + { + // Last element was not on the same line as us + return false; + } + + // Returns true if the last element was an emote image + return lastElement->getFlags().has(MessageElementFlag::EmoteImages); + }; + + bool isRTLElement = element->getText().isRightToLeft(); + if (isRTLElement) + { + this->lineContainsRTL_ = true; + } + + // check the first non-neutral word to see if we should render RTL or LTR + if (isAddingMode && this->isNeutral() && + element->getFlags().has(MessageElementFlag::Text) && + !element->getFlags().has(MessageElementFlag::RepliedMessage)) + { + if (isRTLElement) + { + this->textDirection_ = TextDirection::RTL; + } + else if (!chatterino::isNeutral(element->getText())) + { + this->textDirection_ = TextDirection::LTR; + } + } + + // top margin + if (this->elements_.empty()) + { + this->currentY_ = int(MARGIN.top() * this->scale_); + } + + int elementLineHeight = element->getRect().height(); + + // compact emote offset + bool isCompactEmote = + !this->flags_.has(MessageFlag::DisableCompactEmotes) && + element->getCreator().getFlags().has(MessageElementFlag::EmoteImages); + + if (isCompactEmote) + { + elementLineHeight -= COMPACT_EMOTES_OFFSET * this->scale_; + } + + // update line height + this->lineHeight_ = std::max(this->lineHeight_, elementLineHeight); + + auto xOffset = 0; + auto yOffset = 0; + + if (element->getCreator().getFlags().has( + MessageElementFlag::ChannelPointReward) && + element->getCreator().getFlags().hasNone( + {MessageElementFlag::TwitchEmoteImage})) + { + yOffset -= (MARGIN.top() * this->scale_); + } + + if (getSettings()->removeSpacesBetweenEmotes && + element->getFlags().hasAny({MessageElementFlag::EmoteImages}) && + shouldRemoveSpaceBetweenEmotes()) + { + // Move cursor one 'space width' to the left (right in case of RTL) to combine hug the previous emote + if (isRTLAdjusting) + { + this->currentX_ += this->spaceWidth_; } else { - if (index + indexCount > to) - { - element->addCopyTextToString(str, 0, to - index); - break; - } - else - { - element->addCopyTextToString(str); - } + this->currentX_ -= this->spaceWidth_; } + } - index += indexCount; + if (isRTLAdjusting) + { + // shift by width since we are calculating according to top right in RTL mode + // but setPosition wants top left + xOffset -= element->getRect().width(); + } + + // set move element + element->setPosition( + QPoint(this->currentX_ + xOffset, + this->currentY_ - element->getRect().height() + yOffset)); + + element->setLine(this->line_); + + // add element + if (isAddingMode) + { + this->elements_.push_back( + std::unique_ptr(element)); + } + + // set current x + if (isRTLAdjusting) + { + this->currentX_ -= element->getRect().width(); + } + else + { + this->currentX_ += element->getRect().width(); + } + + if (element->hasTrailingSpace()) + { + if (isRTLAdjusting) + { + this->currentX_ -= this->spaceWidth_; + } + else + { + this->currentX_ += this->spaceWidth_; + } } } +void MessageLayoutContainer::reorderRTL(size_t firstTextIndex) +{ + if (this->elements_.empty()) + { + return; + } + + size_t startIndex = this->lineStart_; + size_t endIndex = this->elements_.size() - 1; + + if (firstTextIndex >= endIndex || startIndex >= this->elements_.size()) + { + return; + } + startIndex = std::max(startIndex, firstTextIndex); + + QVarLengthArray correctSequence; + // temporary buffer to store elements in opposite order + QVarLengthArray swappedSequence; + + bool isReversing = false; + for (size_t i = startIndex; i <= endIndex; i++) + { + auto &element = this->elements_[i]; + + const auto neutral = chatterino::isNeutral(element->getText()); + const auto neutralOrUsername = + neutral || element->getFlags().has(MessageElementFlag::Mention); + + // check if neutral words are treated as RTL to add U+202B (RTL + // embedding) which fixes punctuation marks + if (neutral && + ((this->isRTL() && !isReversing) || (this->isLTR() && isReversing))) + { + element->reversedNeutral = true; + } + + if ((element->getText().isRightToLeft() != this->isRTL() && + !neutralOrUsername) || + (neutralOrUsername && isReversing)) + { + swappedSequence.append(i); + isReversing = true; + } + else + { + while (!swappedSequence.empty()) + { + correctSequence.push_back(swappedSequence.last()); + swappedSequence.pop_back(); + } + correctSequence.push_back(i); + isReversing = false; + } + } + while (!swappedSequence.empty()) + { + correctSequence.push_back(swappedSequence.last()); + swappedSequence.pop_back(); + } + + // render right to left if we are in RTL mode, otherwise LTR + if (this->isRTL()) + { + this->currentX_ = this->elements_[endIndex]->getRect().right(); + } + else + { + this->currentX_ = this->elements_[startIndex]->getRect().left(); + } + // manually do the first call with -1 as previous index + if (this->canAddElements()) + { + this->addElement(this->elements_[correctSequence[0]].get(), false, -1); + } + + for (qsizetype i = 1; i < correctSequence.size() && this->canAddElements(); + i++) + { + this->addElement(this->elements_[correctSequence[i]].get(), false, + static_cast(correctSequence[i - 1])); + } +} + +void MessageLayoutContainer::paintSelectionRect(QPainter &painter, + const Line &line, + const int left, const int right, + const int yOffset, + const QColor &color) const +{ + QRect rect = line.rect; + + rect.setTop(std::max(0, rect.top()) + yOffset); + rect.setBottom(std::min(this->height_, rect.bottom()) + yOffset); + rect.setLeft(left); + rect.setRight(right); + + painter.fillRect(rect, color); +} + +std::optional MessageLayoutContainer::paintSelectionStart( + QPainter &painter, const size_t messageIndex, const Selection &selection, + const int yOffset) const +{ + const auto selectionColor = getTheme()->messages.selection; + + // The selection starts in this message + for (size_t lineIndex = 0; lineIndex < this->lines_.size(); lineIndex++) + { + const Line &line = this->lines_[lineIndex]; + + // Selection doesn't start in this line + if (selection.selectionMin.charIndex >= line.endCharIndex) + { + continue; + } + + if (selection.selectionMin.charIndex == line.endCharIndex - 1) + { + // Selection starts at the trailing newline + // NOTE: Should this be included in the selection? Right now this is + // painted since it's included in the copy action, but if it's trimmed we + // should stop painting this + auto right = this->elements_[line.endIndex - 1]->getRect().right(); + this->paintSelectionRect(painter, line, right, right, yOffset, + selectionColor); + return std::nullopt; + } + + int x = this->elements_[line.startIndex]->getRect().left(); + int r = this->elements_[line.endIndex - 1]->getRect().right(); + + auto index = line.startCharIndex; + for (auto i = line.startIndex; i < line.endIndex; i++) + { + auto indexCount = this->elements_[i]->getSelectionIndexCount(); + if (index + indexCount <= selection.selectionMin.charIndex) + { + index += indexCount; + continue; + } + + x = this->elements_[i]->getXFromIndex( + selection.selectionMin.charIndex - index); + + if (selection.selectionMax.messageIndex == messageIndex && + selection.selectionMax.charIndex < line.endCharIndex) + { + // The selection ends in the same line it started + index = line.startCharIndex; + for (auto elementIdx = line.startIndex; + elementIdx < line.endIndex; elementIdx++) + { + auto c = + this->elements_[elementIdx]->getSelectionIndexCount(); + + if (index + c > selection.selectionMax.charIndex) + { + r = this->elements_[elementIdx]->getXFromIndex( + selection.selectionMax.charIndex - index); + break; + } + index += c; + } + + this->paintSelectionRect(painter, line, x, r, yOffset, + selectionColor); + + return std::nullopt; + } + + // doesn't end in this message -> paint the following lines of this message + if (selection.selectionMax.messageIndex != messageIndex) + { + // The selection does not end in this message + for (size_t lineIndex2 = lineIndex + 1; + lineIndex2 < this->lines_.size(); lineIndex2++) + { + const auto &line2 = this->lines_[lineIndex2]; + auto left = + this->elements_[line2.startIndex]->getRect().left(); + auto right = + this->elements_[line2.endIndex - 1]->getRect().right(); + + this->paintSelectionRect(painter, line2, left, right, + yOffset, selectionColor); + } + + this->paintSelectionRect(painter, line, x, r, yOffset, + selectionColor); + + return std::nullopt; + } + + // The selection starts in this line, but ends in some next line or message + this->paintSelectionRect(painter, line, x, r, yOffset, + selectionColor); + + return {++lineIndex}; + } + } + + return std::nullopt; +} + +void MessageLayoutContainer::paintSelectionEnd(QPainter &painter, + size_t lineIndex, + const Selection &selection, + const int yOffset) const +{ + const auto selectionColor = getTheme()->messages.selection; + // [2] selection contains or ends in this message (starts before our message or line) + for (; lineIndex < this->lines_.size(); lineIndex++) + { + const Line &line = this->lines_[lineIndex]; + size_t index = line.startCharIndex; + + // the whole line is included + if (line.endCharIndex < selection.selectionMax.charIndex) + { + auto left = this->elements_[line.startIndex]->getRect().left(); + auto right = this->elements_[line.endIndex - 1]->getRect().right(); + this->paintSelectionRect(painter, line, left, right, yOffset, + selectionColor); + continue; + } + + // find the right end of the selection + int r = this->elements_[line.endIndex - 1]->getRect().right(); + + for (auto i = line.startIndex; i < line.endIndex; i++) + { + size_t c = this->elements_[i]->getSelectionIndexCount(); + + if (index + c > selection.selectionMax.charIndex) + { + r = this->elements_[i]->getXFromIndex( + selection.selectionMax.charIndex - index); + break; + } + + index += c; + } + + auto left = this->elements_[line.startIndex]->getRect().left(); + this->paintSelectionRect(painter, line, left, r, yOffset, + selectionColor); + + return; + } +} + +bool MessageLayoutContainer::canAddElements() const +{ + return this->canAddMessages_; +} + +bool MessageLayoutContainer::canCollapse() const +{ + return getSettings()->collpseMessagesMinLines.getValue() > 0 && + this->flags_.has(MessageFlag::Collapsed); +} + +bool MessageLayoutContainer::isRTL() const noexcept +{ + return this->textDirection_ == TextDirection::RTL; +} + +bool MessageLayoutContainer::isLTR() const noexcept +{ + return this->textDirection_ == TextDirection::LTR; +} + +bool MessageLayoutContainer::isNeutral() const noexcept +{ + return this->textDirection_ == TextDirection::Neutral; +} + } // namespace chatterino diff --git a/src/messages/layouts/MessageLayoutContainer.hpp b/src/messages/layouts/MessageLayoutContainer.hpp index 26daba24f..1cd7f6af2 100644 --- a/src/messages/layouts/MessageLayoutContainer.hpp +++ b/src/messages/layouts/MessageLayoutContainer.hpp @@ -1,117 +1,382 @@ #pragma once -#include -#include -#include -#include - #include "common/Common.hpp" #include "common/FlagsEnum.hpp" -#include "messages/Selection.hpp" -#include "messages/layouts/MessageLayoutElement.hpp" +#include "messages/MessageFlag.hpp" + +#include +#include + +#include +#include +#include + +#if __has_include() +# include +#endif class QPainter; namespace chatterino { -enum class MessageFlag : uint32_t; -using MessageFlags = FlagsEnum; - -struct Margin { - int top; - int right; - int bottom; - int left; - - Margin() - : Margin(0) - { - } - - Margin(int value) - : Margin(value, value, value, value) - { - } - - Margin(int _top, int _right, int _bottom, int _left) - : top(_top) - , right(_right) - , bottom(_bottom) - , left(_left) - { - } +enum class TextDirection : uint8_t { + Neutral, + RTL, + LTR, }; +class MessageLayoutElement; +struct Selection; +struct MessagePaintContext; + struct MessageLayoutContainer { MessageLayoutContainer() = default; - Margin margin = {4, 8, 4, 8}; - bool centered = false; - bool enableCompactEmotes = false; + /** + * Begin the layout process of this message + * + * This will reset all line calculations, and will be considered incomplete + * until the accompanying end function has been called + */ + void beginLayout(int width, float scale, float imageScale, + MessageFlags flags); - int getHeight() const; + /** + * Finish the layout process of this message + */ + void endLayout(); + + /** + * Add the given `element` to this message. + * + * This will also prepend a line break if the element + * does not fit in the current line + */ + void addElement(MessageLayoutElement *element); + + /** + * Add the given `element` to this message + */ + void addElementNoLineBreak(MessageLayoutElement *element); + + /** + * Break the current line + */ + void breakLine(); + + /** + * Paint the elements in this message + */ + void paintElements(QPainter &painter, const MessagePaintContext &ctx) const; + + /** + * Paint the animated elements in this message + * @returns true if this container contains at least one animated element + */ + bool paintAnimatedElements(QPainter &painter, int yOffset) const; + + /** + * Paint the selection for this container + * This container contains one or more message elements + * + * @param painter The painter we draw everything to + * @param messageIndex This container's message index in the context of + * the layout we're being painted in + * @param selection The selection we need to paint + * @param yOffset The extra offset added to Y for everything that's painted + */ + void paintSelection(QPainter &painter, size_t messageIndex, + const Selection &selection, int yOffset) const; + + /** + * Add text from this message into the `str` parameter + * + * @param[out] str The string where we append our selected text to + * @param from The character index from which we collecting our selected text + * @param to The character index where we stop collecting our selected text + * @param copymode Decides what from the message gets added to the selected text + */ + void addSelectionText(QString &str, uint32_t from, uint32_t to, + CopyMode copymode) const; + + /** + * Returns a raw pointer to the element at the given point + * + * If no element is found at the given point, this returns a null pointer + */ + MessageLayoutElement *getElementAt(QPoint point) const; + + /** + * Get the character index at the given point, in the context of selections + */ + size_t getSelectionIndex(QPoint point) const; + + /** + * Get the index of the first visible character in this message + * + * This can be non-zero if there are elements in this message that are skipped + */ + size_t getFirstMessageCharacterIndex() const; + + /** + * @brief Returns the word bounds of the given element + * + * The first value is the index of the first character in the word, + * the second value is the index of the character after the last character in the word. + * + * Given the word "abc" by itself, we would return (0, 3) + * + * V V + * "abc " + */ + std::pair getWordBounds( + const MessageLayoutElement *hoveredElement) const; + + /** + * Get the index of the last character in this message + * This is the sum of all the characters in `elements_` + */ + size_t getLastCharacterIndex() const; + + /** + * Returns the width of this message + */ int getWidth() const; + + /** + * Returns the height of this message + */ + int getHeight() const; + + /** + * Returns the scale of this message + */ float getScale() const; - // methods - void begin(int width_, float scale_, MessageFlags flags_); - void end(); + /** + * Returns the image scale + */ + float getImageScale() const; - void clear(); - bool canAddElements(); - void addElement(MessageLayoutElement *element); - void addElementNoLineBreak(MessageLayoutElement *element); - void breakLine(); - bool atStartOfLine(); - bool fitsInLine(int width_); - MessageLayoutElement *getElementAt(QPoint point); + /** + * Returns true if this message is collapsed + */ + bool isCollapsed() const; - // painting - void paintElements(QPainter &painter); - void paintAnimatedElements(QPainter &painter, int yOffset); - void paintSelection(QPainter &painter, int messageIndex, - Selection &selection, int yOffset); + /** + * Return true if we are at the start of a new line + */ + bool atStartOfLine() const; - // selection - int getSelectionIndex(QPoint point); - int getLastCharacterIndex() const; - int getFirstMessageCharacterIndex() const; - void addSelectionText(QString &str, int from, int to, CopyMode copymode); + /** + * Check whether an additional `width` would fit in the current line + * + * Returns true if it does fit, false if not + */ + bool fitsInLine(int width) const; - bool isCollapsed(); + /** + * Returns the remaining width of this line until we will need to start a new line + */ + int remainingWidth() const; + + /** + * Returns the id of the next word that can be added to this container + */ + int nextWordId(); private: struct Line { - int startIndex; - int endIndex; - int startCharIndex; - int endCharIndex; + /** + * The index of the first message element on this line + * Points into `elements_` + */ + size_t startIndex{}; + + /** + * The index of the last message element on this line + * Points into `elements_` + */ + size_t endIndex{}; + + /** + * In the context of selections, the index of the first character on this line + * The first line's startCharIndex will always be 0 + */ + size_t startCharIndex{}; + + /** + * In the context of selections, the index of the last character on this line + * The last line's startCharIndex will always be the sum of all characters in this message + */ + size_t endCharIndex{}; + + /** + * The rectangle that covers all elements on this line + * This rectangle will always take up 100% of the view's width + */ QRect rect; }; - // helpers - void _addElement(MessageLayoutElement *element, bool forceAdd = false); - bool canCollapse(); + /// @brief Attempts to add @a element to this container + /// + /// This can be called in two scenarios. + /// + /// 1. **Regular**: In this scenario, @a element is positioned and added + /// to the internal container. + /// This is active iff @a prevIndex is `-2`. + /// During this stage, if there isn't any @a textDirection_ detected yet, + /// the added element is checked if it contains RTL/LTR text to infer the + /// direction. Only upon calling @a breakLine, the elements will be + /// visually reorderd. + /// + /// 2. **Repositioning**: In this scenario, @a element is already added to + /// the container thus it's only repositioned. + /// This is active iff @a prevIndex is not `-2`. + /// @a prevIndex is used to handle compact emotes. `-1` is used to + /// indicate no predecessor. + /// + /// @param element[in] The element to add. This must be non-null and + /// allocated with `new`. Ownership is transferred + /// into this container. + /// @param forceAdd When enabled, @a element will be added regardless of + /// `canAddElements`. If @a element won't be added it will + /// be `delete`d. + /// @param prevIndex Controls the "scenario" (see above). `-2` indicates + /// "regular" mode; other values indicate "repositioning". + /// In case of repositioning, this contains the index of + /// the precesding element (visually, according to + /// @a textDirection_ [RTL/LTR]). + void addElement(MessageLayoutElement *element, bool forceAdd, + qsizetype prevIndex); + + /// @brief Reorders the last line according to @a textDirection_ + /// + /// If a line contains RTL or the text direction is RTL, elements need to be + /// reordered (see @a lineContainsRTL_ and @a isRTL respectively). + /// This method reverses sequences of text in the opposite direction for it + /// to remain in its intended direction when rendered. Non-text elements + /// won't be reordered. + /// + /// For example, in an RTL container, the sequence + /// "1_R 2_R 3_N 4_R 5_L 6_L 7_N 8_R" will be (visually) reordered to + /// "8_R 5_L 6_L 7_N 4_R 3_N 2_R 1_R" (x_{L,N,R} indicates the element with + /// id x which is in the direction {LTR,Neutral,RTL}). + /// + /// @param firstTextIndex The index of the first element of the message + /// (i.e. the index after the username). + void reorderRTL(size_t firstTextIndex); + + /** + * Paint a selection rectangle over the given line + * + * @param painter The painter we draw everything to + * @param line The line whose rect we use as the base top & bottom of the rect to paint + * @param left The left coordinates of the rect to paint + * @param right The right coordinates of the rect to paint + * @param yOffset Extra offset for line's top & bottom + * @param color Color of the selection + **/ + void paintSelectionRect(QPainter &painter, const Line &line, int left, + int right, int yOffset, const QColor &color) const; + + /** + * Paint the selection start + * + * Returns a line index if this message should also paint the selection end + */ + std::optional paintSelectionStart(QPainter &painter, + size_t messageIndex, + const Selection &selection, + int yOffset) const; + + /** + * Paint the selection end + * + * @param lineIndex The index of the line to start painting at + */ + void paintSelectionEnd(QPainter &painter, size_t lineIndex, + const Selection &selection, int yOffset) const; + + /** + * canAddElements returns true if it's possible to add more elements to this message + */ + bool canAddElements() const; + + /** + * Return true if this message can collapse + * + * TODO: comment this better :-) + */ + bool canCollapse() const; + + /// @returns true, if @a textDirection_ is RTL + [[nodiscard]] bool isRTL() const noexcept; + + /// @returns true, if @a textDirection_ is LTR + [[nodiscard]] bool isLTR() const noexcept; + + /// @returns true, if @a textDirection_ is Neutral + [[nodiscard]] bool isNeutral() const noexcept; // variables - float scale_ = 1.f; + float scale_ = 1.F; + /** + * Scale factor for images + */ + float imageScale_ = 1.F; int width_ = 0; MessageFlags flags_{}; - int line_ = 0; + /** + * line_ is the current line index we are adding + * This is not the number of lines this message contains, since this will stop + * incrementing if the message is collapsed + */ + size_t line_{}; int height_ = 0; int currentX_ = 0; int currentY_ = 0; - int charIndex_ = 0; + /** + * charIndex_ is the selection-contexted index of where we currently are in our message + * At the end, this will always be equal to the sum of `elements_` getSelectionIndexCount() + */ + size_t charIndex_ = 0; size_t lineStart_ = 0; int lineHeight_ = 0; int spaceWidth_ = 4; int textLineHeight_ = 0; int dotdotdotWidth_ = 0; + int currentWordId_ = 0; bool canAddMessages_ = true; bool isCollapsed_ = false; + /// @brief True if the current line contains any RTL text. + /// + /// If the line contains any RTL, it needs to be reordered after a + /// linebreak after which it's reset to `false`. + bool lineContainsRTL_ = false; + + /// True if there was any RTL/LTR reordering done in this container + bool anyReorderingDone_ = false; + + /// @brief The direction of the text in this container. + /// + /// This starts off as neutral until an element is encountered that is + /// either LTR or RTL (afterwards this remains constant). + TextDirection textDirection_ = TextDirection::Neutral; + std::vector> elements_; + + /** + * A list of lines covering this message + * A message that spans 3 lines in a view will have 3 elements in lines_ + * These lines hold no relation to the elements that are in this + */ std::vector lines_; + +#ifdef FRIEND_TEST + FRIEND_TEST(MessageLayoutContainerTest, RtlReordering); +#endif }; } // namespace chatterino diff --git a/src/messages/layouts/MessageLayoutContext.cpp b/src/messages/layouts/MessageLayoutContext.cpp new file mode 100644 index 000000000..98c963919 --- /dev/null +++ b/src/messages/layouts/MessageLayoutContext.cpp @@ -0,0 +1,88 @@ +#include "messages/layouts/MessageLayoutContext.hpp" + +#include "singletons/Settings.hpp" +#include "singletons/Theme.hpp" + +namespace chatterino { + +void MessageColors::applyTheme(Theme *theme) +{ + this->regular = theme->messages.backgrounds.regular; + this->alternate = theme->messages.backgrounds.alternate; + + this->disabled = theme->messages.disabled; + this->selection = theme->messages.selection; + this->system = theme->messages.textColors.system; + + this->messageSeperator = theme->splits.messageSeperator; + + this->focusedLastMessageLine = theme->tabs.selected.backgrounds.regular; + this->unfocusedLastMessageLine = theme->tabs.selected.backgrounds.unfocused; +} + +void MessagePreferences::connectSettings(Settings *settings, + pajlada::Signals::SignalHolder &holder) +{ + settings->enableRedeemedHighlight.connect( + [this](const auto &newValue) { + this->enableRedeemedHighlight = newValue; + }, + holder); + + settings->enableElevatedMessageHighlight.connect( + [this](const auto &newValue) { + this->enableElevatedMessageHighlight = newValue; + }, + holder); + + settings->enableFirstMessageHighlight.connect( + [this](const auto &newValue) { + this->enableFirstMessageHighlight = newValue; + }, + holder); + + settings->enableSubHighlight.connect( + [this](const auto &newValue) { + this->enableSubHighlight = newValue; + }, + holder); + + settings->enableAutomodHighlight.connect( + [this](const auto &newValue) { + this->enableAutomodHighlight = newValue; + }, + holder); + + settings->alternateMessages.connect( + [this](const auto &newValue) { + this->alternateMessages = newValue; + }, + holder); + + settings->separateMessages.connect( + [this](const auto &newValue) { + this->separateMessages = newValue; + }, + holder); + + settings->lastMessageColor.connect( + [this](const auto &newValue) { + if (newValue.isEmpty()) + { + this->lastMessageColor = QColor(); + } + else + { + this->lastMessageColor = QColor(newValue); + } + }, + holder); + + settings->lastMessagePattern.connect( + [this](const auto &newValue) { + this->lastMessagePattern = static_cast(newValue); + }, + holder); +} + +} // namespace chatterino diff --git a/src/messages/layouts/MessageLayoutContext.hpp b/src/messages/layouts/MessageLayoutContext.hpp new file mode 100644 index 000000000..d8f08ab3a --- /dev/null +++ b/src/messages/layouts/MessageLayoutContext.hpp @@ -0,0 +1,75 @@ +#pragma once + +#include +#include + +namespace pajlada::Signals { +class SignalHolder; +} // namespace pajlada::Signals + +namespace chatterino { + +class ColorProvider; +class Theme; +class Settings; +struct Selection; + +// TODO: Figure out if this could be a subset of Theme instead (e.g. Theme::MessageColors) +struct MessageColors { + QColor regular; + QColor alternate; + QColor disabled; + QColor selection; + QColor system; + + QColor messageSeperator; + + QColor focusedLastMessageLine; + QColor unfocusedLastMessageLine; + + void applyTheme(Theme *theme); +}; + +// TODO: Explore if we can let settings own this +struct MessagePreferences { + QColor lastMessageColor; + Qt::BrushStyle lastMessagePattern{}; + + bool enableRedeemedHighlight{}; + bool enableElevatedMessageHighlight{}; + bool enableFirstMessageHighlight{}; + bool enableSubHighlight{}; + bool enableAutomodHighlight{}; + + bool alternateMessages{}; + bool separateMessages{}; + + void connectSettings(Settings *settings, + pajlada::Signals::SignalHolder &holder); +}; + +struct MessagePaintContext { + QPainter &painter; + const Selection &selection; + const ColorProvider &colorProvider; + const MessageColors &messageColors; + const MessagePreferences &preferences; + + // width of the area we have to draw on + const int canvasWidth{}; + // whether the painting should be treated as if this view's window is focused + const bool isWindowFocused{}; + // whether the painting should be treated as if this view is the special mentions view + const bool isMentions{}; + + // y coordinate we're currently painting at + int y{}; + + // Index of the message that is currently being painted + // This index refers to the snapshot being used in the painting + size_t messageIndex{}; + + bool isLastReadMessage{}; +}; + +} // namespace chatterino diff --git a/src/messages/layouts/MessageLayoutElement.cpp b/src/messages/layouts/MessageLayoutElement.cpp index 492a4c8bb..3bfe77330 100644 --- a/src/messages/layouts/MessageLayoutElement.cpp +++ b/src/messages/layouts/MessageLayoutElement.cpp @@ -3,15 +3,28 @@ #include "Application.hpp" #include "messages/Emote.hpp" #include "messages/Image.hpp" +#include "messages/layouts/MessageLayoutContext.hpp" #include "messages/MessageElement.hpp" #include "providers/twitch/TwitchEmotes.hpp" -#include "singletons/Theme.hpp" #include "util/DebugCount.hpp" #include #include #include +namespace { + +const QChar RTL_EMBED(0x202B); + +void alignRectBottomCenter(QRectF &rect, const QRectF &reference) +{ + QPointF newCenter(reference.center().x(), + reference.bottom() - (rect.height() / 2.0)); + rect.moveCenter(newCenter); +} + +} // namespace + namespace chatterino { const QRect &MessageLayoutElement::getRect() const @@ -47,12 +60,12 @@ bool MessageLayoutElement::hasTrailingSpace() const return this->trailingSpace; } -int MessageLayoutElement::getLine() const +size_t MessageLayoutElement::getLine() const { return this->line_; } -void MessageLayoutElement::setLine(int line) +void MessageLayoutElement::setLine(size_t line) { this->line_ = line; } @@ -64,9 +77,9 @@ MessageLayoutElement *MessageLayoutElement::setTrailingSpace(bool value) return this; } -MessageLayoutElement *MessageLayoutElement::setLink(const Link &_link) +MessageLayoutElement *MessageLayoutElement::setLink(const Link &link) { - this->link_ = _link; + this->link_ = link; return this; } @@ -76,9 +89,13 @@ MessageLayoutElement *MessageLayoutElement::setText(const QString &_text) return this; } -const Link &MessageLayoutElement::getLink() const +Link MessageLayoutElement::getLink() const { - return this->link_; + if (this->link_) + { + return *this->link_; + } + return this->creator_.getLink(); } const QString &MessageLayoutElement::getText() const @@ -91,6 +108,16 @@ FlagsEnum MessageLayoutElement::getFlags() const return this->creator_.getFlags(); } +int MessageLayoutElement::getWordId() const +{ + return this->wordId_; +} + +void MessageLayoutElement::setWordId(int wordId) +{ + this->wordId_ = wordId; +} + // // IMAGE // @@ -103,8 +130,8 @@ ImageLayoutElement::ImageLayoutElement(MessageElement &creator, ImagePtr image, this->trailingSpace = creator.hasTrailingSpace(); } -void ImageLayoutElement::addCopyTextToString(QString &str, int from, - int to) const +void ImageLayoutElement::addCopyTextToString(QString &str, uint32_t from, + uint32_t to) const { const auto *emoteElement = dynamic_cast(&this->getCreator()); @@ -112,19 +139,20 @@ void ImageLayoutElement::addCopyTextToString(QString &str, int from, { str += emoteElement->getEmote()->getCopyString(); str = TwitchEmotes::cleanUpEmoteCode(str); - if (this->hasTrailingSpace()) + if (this->hasTrailingSpace() && to >= 2) { - str += " "; + str += ' '; } } } -int ImageLayoutElement::getSelectionIndexCount() const +size_t ImageLayoutElement::getSelectionIndexCount() const { return this->trailingSpace ? 2 : 1; } -void ImageLayoutElement::paint(QPainter &painter) +void ImageLayoutElement::paint(QPainter &painter, + const MessageColors & /*messageColors*/) { if (this->image_ == nullptr) { @@ -139,11 +167,11 @@ void ImageLayoutElement::paint(QPainter &painter) } } -void ImageLayoutElement::paintAnimated(QPainter &painter, int yOffset) +bool ImageLayoutElement::paintAnimated(QPainter &painter, int yOffset) { if (this->image_ == nullptr) { - return; + return false; } if (this->image_->animated()) @@ -153,8 +181,10 @@ void ImageLayoutElement::paintAnimated(QPainter &painter, int yOffset) auto rect = this->getRect(); rect.moveTop(rect.y() + yOffset); painter.drawPixmap(QRectF(rect), *pixmap, QRectF()); + return true; } } + return false; } int ImageLayoutElement::getMouseOverIndex(const QPoint &abs) const @@ -162,7 +192,136 @@ int ImageLayoutElement::getMouseOverIndex(const QPoint &abs) const return 0; } -int ImageLayoutElement::getXFromIndex(int index) +int ImageLayoutElement::getXFromIndex(size_t index) +{ + if (index <= 0) + { + return this->getRect().left(); + } + else if (index == 1) + { + // fourtf: remove space width + return this->getRect().right(); + } + else + { + return this->getRect().right(); + } +} + +// +// LAYERED IMAGE +// + +LayeredImageLayoutElement::LayeredImageLayoutElement( + MessageElement &creator, std::vector images, + std::vector sizes, QSize largestSize) + : MessageLayoutElement(creator, largestSize) + , images_(std::move(images)) + , sizes_(std::move(sizes)) +{ + assert(this->images_.size() == this->sizes_.size()); + this->trailingSpace = creator.hasTrailingSpace(); +} + +void LayeredImageLayoutElement::addCopyTextToString(QString &str, uint32_t from, + uint32_t to) const +{ + const auto *layeredEmoteElement = + dynamic_cast(&this->getCreator()); + if (layeredEmoteElement) + { + // cleaning is taken care in call + str += layeredEmoteElement->getCleanCopyString(); + if (this->hasTrailingSpace() && to >= 2) + { + str += ' '; + } + } +} + +size_t LayeredImageLayoutElement::getSelectionIndexCount() const +{ + return this->trailingSpace ? 2 : 1; +} + +void LayeredImageLayoutElement::paint(QPainter &painter, + const MessageColors & /*messageColors*/) +{ + auto fullRect = QRectF(this->getRect()); + + for (size_t i = 0; i < this->images_.size(); ++i) + { + auto &img = this->images_[i]; + if (img == nullptr) + { + continue; + } + + auto pixmap = img->pixmapOrLoad(); + if (img->animated()) + { + // As soon as we see an animated emote layer, we can stop rendering + // the static emotes. The paintAnimated function will render any + // static emotes layered on top of the first seen animated emote. + return; + } + + if (pixmap) + { + // Matching the web chat behavior, we center the emote within the overall + // binding box. E.g. small overlay emotes like cvMask will sit in the direct + // center of even wide emotes. + auto &size = this->sizes_[i]; + QRectF destRect(0, 0, size.width(), size.height()); + alignRectBottomCenter(destRect, fullRect); + + painter.drawPixmap(destRect, *pixmap, QRectF()); + } + } +} + +bool LayeredImageLayoutElement::paintAnimated(QPainter &painter, int yOffset) +{ + auto fullRect = QRectF(this->getRect()); + fullRect.moveTop(fullRect.y() + yOffset); + bool animatedFlag = false; + + for (size_t i = 0; i < this->images_.size(); ++i) + { + auto &img = this->images_[i]; + if (img == nullptr) + { + continue; + } + + // If we have a static emote layered on top of an animated emote, we need + // to render the static emote again after animating anything below it. + if (img->animated() || animatedFlag) + { + if (auto pixmap = img->pixmapOrLoad()) + { + // Matching the web chat behavior, we center the emote within the overall + // binding box. E.g. small overlay emotes like cvMask will sit in the direct + // center of even wide emotes. + auto &size = this->sizes_[i]; + QRectF destRect(0, 0, size.width(), size.height()); + alignRectBottomCenter(destRect, fullRect); + + painter.drawPixmap(destRect, *pixmap, QRectF()); + animatedFlag = true; + } + } + } + return animatedFlag; +} + +int LayeredImageLayoutElement::getMouseOverIndex(const QPoint &abs) const +{ + return 0; +} + +int LayeredImageLayoutElement::getXFromIndex(size_t index) { if (index <= 0) { @@ -189,7 +348,8 @@ ImageWithBackgroundLayoutElement::ImageWithBackgroundLayoutElement( { } -void ImageWithBackgroundLayoutElement::paint(QPainter &painter) +void ImageWithBackgroundLayoutElement::paint( + QPainter &painter, const MessageColors & /*messageColors*/) { if (this->image_ == nullptr) { @@ -220,7 +380,8 @@ ImageWithCircleBackgroundLayoutElement::ImageWithCircleBackgroundLayoutElement( { } -void ImageWithCircleBackgroundLayoutElement::paint(QPainter &painter) +void ImageWithCircleBackgroundLayoutElement::paint( + QPainter &painter, const MessageColors & /*messageColors*/) { if (this->image_ == nullptr) { @@ -231,6 +392,7 @@ void ImageWithCircleBackgroundLayoutElement::paint(QPainter &painter) if (pixmap && !this->image_->animated()) { QRectF boxRect(this->getRect()); + painter.setRenderHint(QPainter::Antialiasing); painter.setPen(Qt::NoPen); painter.setBrush(QBrush(this->color_, Qt::SolidPattern)); painter.drawEllipse(boxRect); @@ -259,45 +421,44 @@ TextLayoutElement::TextLayoutElement(MessageElement &_creator, QString &_text, this->setText(_text); } -void TextLayoutElement::listenToLinkChanges() -{ - this->managedConnections_.managedConnect( - static_cast(this->getCreator()).linkChanged, [this]() { - this->setLink(this->getCreator().getLink()); - }); -} - -void TextLayoutElement::addCopyTextToString(QString &str, int from, - int to) const +void TextLayoutElement::addCopyTextToString(QString &str, uint32_t from, + uint32_t to) const { str += this->getText().mid(from, to - from); - if (this->hasTrailingSpace()) + if (this->hasTrailingSpace() && to > this->getText().length()) { - str += " "; + str += ' '; } } -int TextLayoutElement::getSelectionIndexCount() const +size_t TextLayoutElement::getSelectionIndexCount() const { return this->getText().length() + (this->trailingSpace ? 1 : 0); } -void TextLayoutElement::paint(QPainter &painter) +void TextLayoutElement::paint(QPainter &painter, + const MessageColors & /*messageColors*/) { - auto app = getApp(); + auto *app = getApp(); + QString text = this->getText(); + if (text.isRightToLeft() || this->reversedNeutral) + { + text.prepend(RTL_EMBED); + } painter.setPen(this->color_); - painter.setFont(app->fonts->getFont(this->style_, this->scale_)); + painter.setFont(app->getFonts()->getFont(this->style_, this->scale_)); painter.drawText( - QRectF(this->getRect().x(), this->getRect().y(), 10000, 10000), - this->getText(), QTextOption(Qt::AlignLeft | Qt::AlignTop)); + QRectF(this->getRect().x(), this->getRect().y(), 10000, 10000), text, + QTextOption(Qt::AlignLeft | Qt::AlignTop)); } -void TextLayoutElement::paintAnimated(QPainter &, int) +bool TextLayoutElement::paintAnimated(QPainter & /*painter*/, int /*yOffset*/) { + return false; } int TextLayoutElement::getMouseOverIndex(const QPoint &abs) const @@ -307,9 +468,9 @@ int TextLayoutElement::getMouseOverIndex(const QPoint &abs) const return 0; } - auto app = getApp(); + auto *app = getApp(); - auto metrics = app->fonts->getFontMetrics(this->style_, this->scale_); + auto metrics = app->getFonts()->getFontMetrics(this->style_, this->scale_); auto x = this->getRect().left(); for (auto i = 0; i < this->getText().size(); i++) @@ -317,7 +478,8 @@ int TextLayoutElement::getMouseOverIndex(const QPoint &abs) const auto &&text = this->getText(); auto width = metrics.horizontalAdvance(this->getText()[i]); - if (x + width > abs.x()) + // accept mouse to be at only 50%+ of character width to increase index + if (x + (width * 0.5) > abs.x()) { if (text.size() > i + 1 && QChar::isLowSurrogate(text[i].unicode())) { @@ -338,18 +500,18 @@ int TextLayoutElement::getMouseOverIndex(const QPoint &abs) const return this->getSelectionIndexCount() - (this->hasTrailingSpace() ? 1 : 0); } -int TextLayoutElement::getXFromIndex(int index) +int TextLayoutElement::getXFromIndex(size_t index) { - auto app = getApp(); + auto *app = getApp(); QFontMetrics metrics = - app->fonts->getFontMetrics(this->style_, this->scale_); + app->getFonts()->getFontMetrics(this->style_, this->scale_); if (index <= 0) { return this->getRect().left(); } - else if (index < this->getText().size()) + else if (index < static_cast(this->getText().size())) { int x = 0; for (int i = 0; i < index; i++) @@ -376,23 +538,24 @@ TextIconLayoutElement::TextIconLayoutElement(MessageElement &creator, { } -void TextIconLayoutElement::addCopyTextToString(QString &str, int from, - int to) const +void TextIconLayoutElement::addCopyTextToString(QString &str, uint32_t from, + uint32_t to) const { } -int TextIconLayoutElement::getSelectionIndexCount() const +size_t TextIconLayoutElement::getSelectionIndexCount() const { return this->trailingSpace ? 2 : 1; } -void TextIconLayoutElement::paint(QPainter &painter) +void TextIconLayoutElement::paint(QPainter &painter, + const MessageColors &messageColors) { - auto app = getApp(); + auto *app = getApp(); - QFont font = app->fonts->getFont(FontStyle::Tiny, this->scale); + QFont font = app->getFonts()->getFont(FontStyle::Tiny, this->scale); - painter.setPen(app->themes->messages.textColors.system); + painter.setPen(messageColors.system); painter.setFont(font); QTextOption option; @@ -415,8 +578,10 @@ void TextIconLayoutElement::paint(QPainter &painter) } } -void TextIconLayoutElement::paintAnimated(QPainter &painter, int yOffset) +bool TextIconLayoutElement::paintAnimated(QPainter & /*painter*/, + int /*yOffset*/) { + return false; } int TextIconLayoutElement::getMouseOverIndex(const QPoint &abs) const @@ -424,7 +589,7 @@ int TextIconLayoutElement::getMouseOverIndex(const QPoint &abs) const return 0; } -int TextIconLayoutElement::getXFromIndex(int index) +int TextIconLayoutElement::getXFromIndex(size_t index) { if (index <= 0) { @@ -442,39 +607,56 @@ int TextIconLayoutElement::getXFromIndex(int index) } ReplyCurveLayoutElement::ReplyCurveLayoutElement(MessageElement &creator, - const QSize &size, - float thickness, + int width, float thickness, + float radius, float neededMargin) - : MessageLayoutElement(creator, size) + : MessageLayoutElement(creator, QSize(width, 0)) , pen_(QColor("#888"), thickness, Qt::SolidLine, Qt::RoundCap) + , radius_(radius) , neededMargin_(neededMargin) { } -void ReplyCurveLayoutElement::paint(QPainter &painter) +void ReplyCurveLayoutElement::paint(QPainter &painter, + const MessageColors & /*messageColors*/) { QRectF paintRect(this->getRect()); - QPainterPath bezierPath; + QPainterPath path; - qreal top = paintRect.top() + paintRect.height() * 0.25; // 25% from top - qreal left = paintRect.left() + this->neededMargin_; - qreal bottom = paintRect.bottom() - this->neededMargin_; - QPointF startPoint(left, bottom); - QPointF controlPoint(left, top); - QPointF endPoint(paintRect.right(), top); + QRectF curveRect = paintRect.marginsRemoved(QMarginsF( + this->neededMargin_, this->neededMargin_, 0, this->neededMargin_)); - // Create curve path - bezierPath.moveTo(startPoint); - bezierPath.quadTo(controlPoint, endPoint); + // Make sure that our curveRect can always fit the radius curve + if (curveRect.height() < this->radius_) + { + curveRect.setTop(curveRect.top() - + (this->radius_ - curveRect.height())); + } + + QPointF bStartPoint(curveRect.left(), curveRect.top() + this->radius_); + QPointF bEndPoint(curveRect.left() + this->radius_, curveRect.top()); + QPointF bControlPoint(curveRect.topLeft()); + + // Draw line from bottom left to curve + path.moveTo(curveRect.bottomLeft()); + path.lineTo(bStartPoint); + + // Draw curve path + path.quadTo(bControlPoint, bEndPoint); + + // Draw line from curve to top right + path.lineTo(curveRect.topRight()); // Render curve painter.setPen(this->pen_); painter.setRenderHint(QPainter::Antialiasing); - painter.drawPath(bezierPath); + painter.drawPath(path); } -void ReplyCurveLayoutElement::paintAnimated(QPainter &painter, int yOffset) +bool ReplyCurveLayoutElement::paintAnimated(QPainter & /*painter*/, + int /*yOffset*/) { + return false; } int ReplyCurveLayoutElement::getMouseOverIndex(const QPoint &abs) const @@ -482,24 +664,22 @@ int ReplyCurveLayoutElement::getMouseOverIndex(const QPoint &abs) const return 0; } -int ReplyCurveLayoutElement::getXFromIndex(int index) +int ReplyCurveLayoutElement::getXFromIndex(size_t index) { if (index <= 0) { return this->getRect().left(); } - else - { - return this->getRect().right(); - } + + return this->getRect().right(); } -void ReplyCurveLayoutElement::addCopyTextToString(QString &str, int from, - int to) const +void ReplyCurveLayoutElement::addCopyTextToString(QString &str, uint32_t from, + uint32_t to) const { } -int ReplyCurveLayoutElement::getSelectionIndexCount() const +size_t ReplyCurveLayoutElement::getSelectionIndexCount() const { return 1; } diff --git a/src/messages/layouts/MessageLayoutElement.hpp b/src/messages/layouts/MessageLayoutElement.hpp index 4bf26ad29..de68a43f7 100644 --- a/src/messages/layouts/MessageLayoutElement.hpp +++ b/src/messages/layouts/MessageLayoutElement.hpp @@ -1,18 +1,16 @@ #pragma once +#include "common/FlagsEnum.hpp" +#include "messages/Link.hpp" + +#include #include #include #include #include -#include + #include - -#include "common/FlagsEnum.hpp" -#include "messages/Link.hpp" -#include "messages/MessageColor.hpp" -#include "messages/MessageElement.hpp" - -#include +#include class QPainter; @@ -21,45 +19,80 @@ class MessageElement; class Image; using ImagePtr = std::shared_ptr; enum class FontStyle : uint8_t; +enum class MessageElementFlag : int64_t; +struct MessageColors; -class MessageLayoutElement : boost::noncopyable +class MessageLayoutElement { public: MessageLayoutElement(MessageElement &creator_, const QSize &size); virtual ~MessageLayoutElement(); + MessageLayoutElement(const MessageLayoutElement &) = delete; + MessageLayoutElement &operator=(const MessageLayoutElement &) = delete; + + MessageLayoutElement(MessageLayoutElement &&) = delete; + MessageLayoutElement &operator=(MessageLayoutElement &&) = delete; + + bool reversedNeutral = false; + const QRect &getRect() const; MessageElement &getCreator() const; void setPosition(QPoint point); bool hasTrailingSpace() const; - int getLine() const; - void setLine(int line); + size_t getLine() const; + void setLine(size_t line); MessageLayoutElement *setTrailingSpace(bool value); - MessageLayoutElement *setLink(const Link &link_); + + /// @brief Overwrites the link for this layout element + /// + /// @sa #getLink() + MessageLayoutElement *setLink(const Link &link); + MessageLayoutElement *setText(const QString &text_); - virtual void addCopyTextToString(QString &str, int from = 0, - int to = INT_MAX) const = 0; - virtual int getSelectionIndexCount() const = 0; - virtual void paint(QPainter &painter) = 0; - virtual void paintAnimated(QPainter &painter, int yOffset) = 0; + virtual void addCopyTextToString(QString &str, uint32_t from = 0, + uint32_t to = UINT32_MAX) const = 0; + virtual size_t getSelectionIndexCount() const = 0; + virtual void paint(QPainter &painter, + const MessageColors &messageColors) = 0; + /// @returns true if anything was painted + virtual bool paintAnimated(QPainter &painter, int yOffset) = 0; virtual int getMouseOverIndex(const QPoint &abs) const = 0; - virtual int getXFromIndex(int index) = 0; + virtual int getXFromIndex(size_t index) = 0; - const Link &getLink() const; + /// @brief Returns the link this layout element has + /// + /// If there isn't any, an empty link is returned (type: None). + /// The link is sourced from the creator, but can be overwritten with + /// #setLink(). + Link getLink() const; const QString &getText() const; FlagsEnum getFlags() const; + int getWordId() const; + void setWordId(int wordId); + protected: bool trailingSpace = true; private: QString text_; QRect rect_; - Link link_; + std::optional link_; MessageElement &creator_; - int line_{}; + /** + * The line of the container this element is laid out at + */ + size_t line_{}; + + /// @brief ID of a word inside its container + /// + /// One word has exactly one ID that is used to identify elements created + /// from the same word (due to wrapping). + /// IDs are unique in a MessageLayoutContainer. + int wordId_ = -1; }; // IMAGE @@ -70,17 +103,37 @@ public: const QSize &size); protected: - void addCopyTextToString(QString &str, int from = 0, - int to = INT_MAX) const override; - int getSelectionIndexCount() const override; - void paint(QPainter &painter) override; - void paintAnimated(QPainter &painter, int yOffset) override; + void addCopyTextToString(QString &str, uint32_t from = 0, + uint32_t to = UINT32_MAX) const override; + size_t getSelectionIndexCount() const override; + void paint(QPainter &painter, const MessageColors &messageColors) override; + bool paintAnimated(QPainter &painter, int yOffset) override; int getMouseOverIndex(const QPoint &abs) const override; - int getXFromIndex(int index) override; + int getXFromIndex(size_t index) override; ImagePtr image_; }; +class LayeredImageLayoutElement : public MessageLayoutElement +{ +public: + LayeredImageLayoutElement(MessageElement &creator, + std::vector images, + std::vector sizes, QSize largestSize); + +protected: + void addCopyTextToString(QString &str, uint32_t from = 0, + uint32_t to = UINT32_MAX) const override; + size_t getSelectionIndexCount() const override; + void paint(QPainter &painter, const MessageColors &messageColors) override; + bool paintAnimated(QPainter &painter, int yOffset) override; + int getMouseOverIndex(const QPoint &abs) const override; + int getXFromIndex(size_t index) override; + + std::vector images_; + std::vector sizes_; +}; + class ImageWithBackgroundLayoutElement : public ImageLayoutElement { public: @@ -88,7 +141,7 @@ public: const QSize &size, QColor color); protected: - void paint(QPainter &painter) override; + void paint(QPainter &painter, const MessageColors &messageColors) override; private: QColor color_; @@ -103,7 +156,7 @@ public: int padding); protected: - void paint(QPainter &painter) override; + void paint(QPainter &painter, const MessageColors &messageColors) override; private: const QColor color_; @@ -119,22 +172,18 @@ public: const QSize &size, QColor color_, FontStyle style_, float scale_); - void listenToLinkChanges(); - protected: - void addCopyTextToString(QString &str, int from = 0, - int to = INT_MAX) const override; - int getSelectionIndexCount() const override; - void paint(QPainter &painter) override; - void paintAnimated(QPainter &painter, int yOffset) override; + void addCopyTextToString(QString &str, uint32_t from = 0, + uint32_t to = UINT32_MAX) const override; + size_t getSelectionIndexCount() const override; + void paint(QPainter &painter, const MessageColors &messageColors) override; + bool paintAnimated(QPainter &painter, int yOffset) override; int getMouseOverIndex(const QPoint &abs) const override; - int getXFromIndex(int index) override; + int getXFromIndex(size_t index) override; QColor color_; FontStyle style_; float scale_; - - pajlada::Signals::SignalHolder managedConnections_; }; // TEXT ICON @@ -146,13 +195,13 @@ public: const QString &line2, float scale, const QSize &size); protected: - void addCopyTextToString(QString &str, int from = 0, - int to = INT_MAX) const override; - int getSelectionIndexCount() const override; - void paint(QPainter &painter) override; - void paintAnimated(QPainter &painter, int yOffset) override; + void addCopyTextToString(QString &str, uint32_t from = 0, + uint32_t to = UINT32_MAX) const override; + size_t getSelectionIndexCount() const override; + void paint(QPainter &painter, const MessageColors &messageColors) override; + bool paintAnimated(QPainter &painter, int yOffset) override; int getMouseOverIndex(const QPoint &abs) const override; - int getXFromIndex(int index) override; + int getXFromIndex(size_t index) override; private: float scale; @@ -163,20 +212,21 @@ private: class ReplyCurveLayoutElement : public MessageLayoutElement { public: - ReplyCurveLayoutElement(MessageElement &creator, const QSize &size, - float thickness, float lMargin); + ReplyCurveLayoutElement(MessageElement &creator, int width, float thickness, + float radius, float neededMargin); protected: - void paint(QPainter &painter) override; - void paintAnimated(QPainter &painter, int yOffset) override; + void paint(QPainter &painter, const MessageColors &messageColors) override; + bool paintAnimated(QPainter &painter, int yOffset) override; int getMouseOverIndex(const QPoint &abs) const override; - int getXFromIndex(int index) override; - void addCopyTextToString(QString &str, int from = 0, - int to = INT_MAX) const override; - int getSelectionIndexCount() const override; + int getXFromIndex(size_t index) override; + void addCopyTextToString(QString &str, uint32_t from = 0, + uint32_t to = UINT32_MAX) const override; + size_t getSelectionIndexCount() const override; private: const QPen pen_; + const float radius_; const float neededMargin_; }; diff --git a/src/messages/search/AuthorPredicate.cpp b/src/messages/search/AuthorPredicate.cpp index 77e02b296..ae951b8cd 100644 --- a/src/messages/search/AuthorPredicate.cpp +++ b/src/messages/search/AuthorPredicate.cpp @@ -1,23 +1,21 @@ #include "messages/search/AuthorPredicate.hpp" -#include "util/Qt.hpp" +#include "messages/Message.hpp" namespace chatterino { -AuthorPredicate::AuthorPredicate(const QStringList &authors) - : authors_() +AuthorPredicate::AuthorPredicate(const QString &authors, bool negate) + : MessagePredicate(negate) + , authors_() { // Check if any comma-seperated values were passed and transform those - for (const auto &entry : authors) + for (const auto &author : authors.split(',', Qt::SkipEmptyParts)) { - for (const auto &author : entry.split(',', Qt::SkipEmptyParts)) - { - this->authors_ << author; - } + this->authors_ << author; } } -bool AuthorPredicate::appliesTo(const Message &message) +bool AuthorPredicate::appliesToImpl(const Message &message) { return authors_.contains(message.displayName, Qt::CaseInsensitive) || authors_.contains(message.loginName, Qt::CaseInsensitive); diff --git a/src/messages/search/AuthorPredicate.hpp b/src/messages/search/AuthorPredicate.hpp index 8b3bbced2..19a2ac8fb 100644 --- a/src/messages/search/AuthorPredicate.hpp +++ b/src/messages/search/AuthorPredicate.hpp @@ -2,6 +2,9 @@ #include "messages/search/MessagePredicate.hpp" +#include +#include + namespace chatterino { /** @@ -16,10 +19,12 @@ public: /** * @brief Create an AuthorPredicate with a list of users to search for. * - * @param authors a list of user names that a message should be sent from + * @param authors one or more comma-separated user names that a message should be sent from + * @param negate when set, excludes list of user names from results */ - AuthorPredicate(const QStringList &authors); + AuthorPredicate(const QString &authors, bool negate); +protected: /** * @brief Checks whether the message is authored by any of the users passed * in the constructor. @@ -28,7 +33,7 @@ public: * @return true if the message was authored by one of the specified users, * false otherwise */ - bool appliesTo(const Message &message); + bool appliesToImpl(const Message &message) override; private: /// Holds the user names that will be searched for diff --git a/src/messages/search/BadgePredicate.cpp b/src/messages/search/BadgePredicate.cpp new file mode 100644 index 000000000..218ede8e1 --- /dev/null +++ b/src/messages/search/BadgePredicate.cpp @@ -0,0 +1,47 @@ +#include "messages/search/BadgePredicate.hpp" + +#include "messages/Message.hpp" +#include "providers/twitch/TwitchBadge.hpp" + +namespace chatterino { + +BadgePredicate::BadgePredicate(const QString &badges, bool negate) + : MessagePredicate(negate) +{ + // Check if any comma-seperated values were passed and transform those + for (const auto &badge : badges.split(',', Qt::SkipEmptyParts)) + { + // convert short form name of certain badges to formal name + if (badge.compare("mod", Qt::CaseInsensitive) == 0) + { + this->badges_ << "moderator"; + } + else if (badge.compare("sub", Qt::CaseInsensitive) == 0) + { + this->badges_ << "subscriber"; + } + else if (badge.compare("prime", Qt::CaseInsensitive) == 0) + { + this->badges_ << "premium"; + } + else + { + this->badges_ << badge; + } + } +} + +bool BadgePredicate::appliesToImpl(const Message &message) +{ + for (const Badge &badge : message.badges) + { + if (badges_.contains(badge.key_, Qt::CaseInsensitive)) + { + return true; + } + } + + return false; +} + +} // namespace chatterino diff --git a/src/messages/search/BadgePredicate.hpp b/src/messages/search/BadgePredicate.hpp new file mode 100644 index 000000000..f5d4901af --- /dev/null +++ b/src/messages/search/BadgePredicate.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include "messages/search/MessagePredicate.hpp" + +#include +#include + +namespace chatterino { + +/** + * @brief MessagePredicate checking for the badges of a message. + * + * This predicate will only allow messages that are sent by a list of users, + * specified by their user names, who have a badge specified (i.e 'staff'). + */ +class BadgePredicate : public MessagePredicate +{ +public: + /** + * @brief Create an BadgePredicate with a list of badges to search for. + * + * @param badges one or more comma-separated badges that a message should contain + * @param negate when set, excludes list of badges from results + */ + BadgePredicate(const QString &badges, bool negate); + +protected: + /** + * @brief Checks whether the message contains any of the badges passed + * in the constructor. + * + * @param message the message to check + * @return true if the message contains a badge listed in the specified badges, + * false otherwise + */ + bool appliesToImpl(const Message &message) override; + +private: + /// Holds the badges that will be searched for + QStringList badges_; +}; + +} // namespace chatterino diff --git a/src/messages/search/ChannelPredicate.cpp b/src/messages/search/ChannelPredicate.cpp index 798c2df52..47df247fa 100644 --- a/src/messages/search/ChannelPredicate.cpp +++ b/src/messages/search/ChannelPredicate.cpp @@ -1,23 +1,21 @@ #include "messages/search/ChannelPredicate.hpp" -#include "util/Qt.hpp" +#include "messages/Message.hpp" namespace chatterino { -ChannelPredicate::ChannelPredicate(const QStringList &channels) - : channels_() +ChannelPredicate::ChannelPredicate(const QString &channels, bool negate) + : MessagePredicate(negate) + , channels_() { // Check if any comma-seperated values were passed and transform those - for (const auto &entry : channels) + for (const auto &channel : channels.split(',', Qt::SkipEmptyParts)) { - for (const auto &channel : entry.split(',', Qt::SkipEmptyParts)) - { - this->channels_ << channel; - } + this->channels_ << channel; } } -bool ChannelPredicate::appliesTo(const Message &message) +bool ChannelPredicate::appliesToImpl(const Message &message) { return channels_.contains(message.channelName, Qt::CaseInsensitive); } diff --git a/src/messages/search/ChannelPredicate.hpp b/src/messages/search/ChannelPredicate.hpp index 08521942a..663a6c155 100644 --- a/src/messages/search/ChannelPredicate.hpp +++ b/src/messages/search/ChannelPredicate.hpp @@ -2,6 +2,9 @@ #include "messages/search/MessagePredicate.hpp" +#include +#include + namespace chatterino { /** @@ -16,10 +19,12 @@ public: /** * @brief Create a ChannelPredicate with a list of channels to search for. * - * @param channels a list of channel names that a message should be sent in + * @param channels one or more comma-separated channel names that a message should be sent in + * @param negate when set, excludes list of channel names from results */ - ChannelPredicate(const QStringList &channels); + ChannelPredicate(const QString &channels, bool negate); +protected: /** * @brief Checks whether the message was sent in any of the channels passed * in the constructor. @@ -28,7 +33,7 @@ public: * @return true if the message was sent in one of the specified channels, * false otherwise */ - bool appliesTo(const Message &message); + bool appliesToImpl(const Message &message) override; private: /// Holds the channel names that will be searched for diff --git a/src/messages/search/LinkPredicate.cpp b/src/messages/search/LinkPredicate.cpp index 45860a236..4c4def2ae 100644 --- a/src/messages/search/LinkPredicate.cpp +++ b/src/messages/search/LinkPredicate.cpp @@ -1,20 +1,23 @@ #include "messages/search/LinkPredicate.hpp" #include "common/LinkParser.hpp" -#include "util/Qt.hpp" +#include "messages/Message.hpp" namespace chatterino { -LinkPredicate::LinkPredicate() +LinkPredicate::LinkPredicate(bool negate) + : MessagePredicate(negate) { } -bool LinkPredicate::appliesTo(const Message &message) +bool LinkPredicate::appliesToImpl(const Message &message) { for (const auto &word : message.messageText.split(' ', Qt::SkipEmptyParts)) { - if (LinkParser(word).hasMatch()) + if (linkparser::parse(word).has_value()) + { return true; + } } return false; diff --git a/src/messages/search/LinkPredicate.hpp b/src/messages/search/LinkPredicate.hpp index 4d73920b5..4bda67c8b 100644 --- a/src/messages/search/LinkPredicate.hpp +++ b/src/messages/search/LinkPredicate.hpp @@ -2,6 +2,8 @@ #include "messages/search/MessagePredicate.hpp" +#include + namespace chatterino { /** @@ -12,15 +14,21 @@ namespace chatterino { class LinkPredicate : public MessagePredicate { public: - LinkPredicate(); + /** + * @brief Create an LinkPredicate + * + * @param negate when set, excludes messages containing links from results + */ + LinkPredicate(bool negate); +protected: /** * @brief Checks whether the message contains a link. * * @param message the message to check * @return true if the message contains a link, false otherwise */ - bool appliesTo(const Message &message); + bool appliesToImpl(const Message &message) override; }; } // namespace chatterino diff --git a/src/messages/search/MessageFlagsPredicate.cpp b/src/messages/search/MessageFlagsPredicate.cpp index 01a124020..d36fc72bb 100644 --- a/src/messages/search/MessageFlagsPredicate.cpp +++ b/src/messages/search/MessageFlagsPredicate.cpp @@ -1,11 +1,10 @@ #include "messages/search/MessageFlagsPredicate.hpp" -#include "util/Qt.hpp" - namespace chatterino { -MessageFlagsPredicate::MessageFlagsPredicate(const QString &flags) - : flags_() +MessageFlagsPredicate::MessageFlagsPredicate(const QString &flags, bool negate) + : MessagePredicate(negate) + , flags_() { // Check if any comma-seperated values were passed and transform those for (const auto &flag : flags.split(',', Qt::SkipEmptyParts)) @@ -34,16 +33,43 @@ MessageFlagsPredicate::MessageFlagsPredicate(const QString &flags) { this->flags_.set(MessageFlag::FirstMessage); } + else if (flag == "elevated-msg" || flag == "hype-chat") + { + this->flags_.set(MessageFlag::ElevatedMessage); + } + else if (flag == "cheer-msg") + { + this->flags_.set(MessageFlag::CheerMessage); + } + else if (flag == "redemption") + { + this->flags_.set(MessageFlag::RedeemedChannelPointReward); + this->flags_.set(MessageFlag::RedeemedHighlight); + } + else if (flag == "reply") + { + this->flags_.set(MessageFlag::ReplyMessage); + } + else if (flag == "restricted") + { + this->flags_.set(MessageFlag::RestrictedMessage); + } + else if (flag == "monitored") + { + this->flags_.set(MessageFlag::MonitoredMessage); + } } } -bool MessageFlagsPredicate::appliesTo(const Message &message) +bool MessageFlagsPredicate::appliesToImpl(const Message &message) { // Exclude timeout messages from system flag when timeout flag isn't present if (this->flags_.has(MessageFlag::System) && !this->flags_.has(MessageFlag::Timeout)) + { return message.flags.hasAny(flags_) && !message.flags.has(MessageFlag::Timeout); + } return message.flags.hasAny(flags_); } diff --git a/src/messages/search/MessageFlagsPredicate.hpp b/src/messages/search/MessageFlagsPredicate.hpp index 2c3896546..d1e37873f 100644 --- a/src/messages/search/MessageFlagsPredicate.hpp +++ b/src/messages/search/MessageFlagsPredicate.hpp @@ -1,6 +1,7 @@ #pragma once #include "common/FlagsEnum.hpp" +#include "messages/Message.hpp" #include "messages/search/MessagePredicate.hpp" namespace chatterino { @@ -27,9 +28,11 @@ public: * "system" is used for the "System" flag. * * @param flags a string comma seperated list of names for the flags a message should have + * @param negate when set, excludes messages containg selected flags from results */ - MessageFlagsPredicate(const QString &flags); + MessageFlagsPredicate(const QString &flags, bool negate); +protected: /** * @brief Checks whether the message has any of the flags passed * in the constructor. @@ -38,7 +41,7 @@ public: * @return true if the message has at least one of the specified flags, * false otherwise */ - bool appliesTo(const Message &message); + bool appliesToImpl(const Message &message) override; private: /// Holds the flags that will be searched for diff --git a/src/messages/search/MessagePredicate.hpp b/src/messages/search/MessagePredicate.hpp index 6deb2de52..7a6a9a07c 100644 --- a/src/messages/search/MessagePredicate.hpp +++ b/src/messages/search/MessagePredicate.hpp @@ -1,17 +1,17 @@ #pragma once -#include "messages/Message.hpp" - #include namespace chatterino { +struct Message; + /** * @brief Abstract base class for message predicates. * * Message predicates define certain features a message can satisfy. * Features are represented by classes derived from this abstract class. - * A derived class must override `appliesTo` in order to test for the desired + * A derived class must override `appliesToImpl` in order to test for the desired * feature. */ class MessagePredicate @@ -19,15 +19,43 @@ class MessagePredicate public: virtual ~MessagePredicate() = default; + /** + * @brief Checks whether this predicate applies to the passed message + * + * Calls the derived classes `appliedTo` implementation, and respects the `isNegated_` flag + * it's set. + * + * @param message the message to check for this predicate + * @return true if this predicate applies, false otherwise + **/ + bool appliesTo(const Message &message) + { + auto result = this->appliesToImpl(message); + if (this->isNegated_) + { + return !result; + } + return result; + } + +protected: + explicit MessagePredicate(bool negate) + : isNegated_(negate) + { + } + /** * @brief Checks whether this predicate applies to the passed message. * - * Implementations of `appliesTo` should never change the message's content + * Implementations of `appliesToImpl` should never change the message's content * in order to be compatible with other MessagePredicates. * * @param message the message to check for this predicate * @return true if this predicate applies, false otherwise */ - virtual bool appliesTo(const Message &message) = 0; + virtual bool appliesToImpl(const Message &message) = 0; + +private: + const bool isNegated_ = false; }; } // namespace chatterino diff --git a/src/messages/search/RegexPredicate.cpp b/src/messages/search/RegexPredicate.cpp index b16dcf4d1..733949afa 100644 --- a/src/messages/search/RegexPredicate.cpp +++ b/src/messages/search/RegexPredicate.cpp @@ -1,13 +1,16 @@ -#include "RegexPredicate.hpp" +#include "messages/search/RegexPredicate.hpp" + +#include "messages/Message.hpp" namespace chatterino { -RegexPredicate::RegexPredicate(const QString ®ex) - : regex_(regex, QRegularExpression::CaseInsensitiveOption) +RegexPredicate::RegexPredicate(const QString ®ex, bool negate) + : MessagePredicate(negate) + , regex_(regex, QRegularExpression::CaseInsensitiveOption) { } -bool RegexPredicate::appliesTo(const Message &message) +bool RegexPredicate::appliesToImpl(const Message &message) { if (!regex_.isValid()) { @@ -19,4 +22,4 @@ bool RegexPredicate::appliesTo(const Message &message) return match.hasMatch(); } -} // namespace chatterino \ No newline at end of file +} // namespace chatterino diff --git a/src/messages/search/RegexPredicate.hpp b/src/messages/search/RegexPredicate.hpp index bacf1e6dc..e1cc6e15e 100644 --- a/src/messages/search/RegexPredicate.hpp +++ b/src/messages/search/RegexPredicate.hpp @@ -1,8 +1,10 @@ #pragma once -#include "QRegularExpression" #include "messages/search/MessagePredicate.hpp" +#include +#include + namespace chatterino { /** @@ -20,9 +22,11 @@ public: * The message is being matched case-insensitively. * * @param regex the regex to match the message against + * @param negate when set, excludes messages matching the regex from results */ - RegexPredicate(const QString ®ex); + RegexPredicate(const QString ®ex, bool negate); +protected: /** * @brief Checks whether the message matches the regex passed in the * constructor @@ -32,11 +36,11 @@ public: * @param message the message to check * @return true if the message matches the regex, false otherwise */ - bool appliesTo(const Message &message); + bool appliesToImpl(const Message &message) override; private: /// Holds the regular expression to match the message against QRegularExpression regex_; }; -} // namespace chatterino \ No newline at end of file +} // namespace chatterino diff --git a/src/messages/search/SubstringPredicate.cpp b/src/messages/search/SubstringPredicate.cpp index 752ed9338..36e4b8e87 100644 --- a/src/messages/search/SubstringPredicate.cpp +++ b/src/messages/search/SubstringPredicate.cpp @@ -1,13 +1,16 @@ #include "messages/search/SubstringPredicate.hpp" +#include "messages/Message.hpp" + namespace chatterino { SubstringPredicate::SubstringPredicate(const QString &search) - : search_(search) + : MessagePredicate(false) + , search_(search) { } -bool SubstringPredicate::appliesTo(const Message &message) +bool SubstringPredicate::appliesToImpl(const Message &message) { return message.searchText.contains(this->search_, Qt::CaseInsensitive); } diff --git a/src/messages/search/SubstringPredicate.hpp b/src/messages/search/SubstringPredicate.hpp index 31f43102e..1d5f2b858 100644 --- a/src/messages/search/SubstringPredicate.hpp +++ b/src/messages/search/SubstringPredicate.hpp @@ -2,6 +2,8 @@ #include "messages/search/MessagePredicate.hpp" +#include + namespace chatterino { /** @@ -22,6 +24,7 @@ public: */ SubstringPredicate(const QString &search); +protected: /** * @brief Checks whether the message contains the substring passed in the * constructor. @@ -31,7 +34,7 @@ public: * @param message the message to check * @return true if the message contains the substring, false otherwise */ - bool appliesTo(const Message &message); + bool appliesToImpl(const Message &message) override; private: /// Holds the substring to search for in a message's `messageText` diff --git a/src/messages/search/SubtierPredicate.cpp b/src/messages/search/SubtierPredicate.cpp new file mode 100644 index 000000000..70d5b7148 --- /dev/null +++ b/src/messages/search/SubtierPredicate.cpp @@ -0,0 +1,34 @@ +#include "messages/search/SubtierPredicate.hpp" + +#include "messages/Message.hpp" +#include "providers/twitch/TwitchBadge.hpp" + +namespace chatterino { + +SubtierPredicate::SubtierPredicate(const QString &subtiers, bool negate) + : MessagePredicate(negate) +{ + // Check if any comma-seperated values were passed and transform those + for (const auto &subtier : subtiers.split(',', Qt::SkipEmptyParts)) + { + this->subtiers_ << subtier; + } +} + +bool SubtierPredicate::appliesToImpl(const Message &message) +{ + for (const Badge &badge : message.badges) + { + if (badge.key_ == "subscriber") + { + const auto &subTier = + badge.value_.length() > 3 ? badge.value_.at(0) : '1'; + + return subtiers_.contains(subTier); + } + } + + return false; +} + +} // namespace chatterino diff --git a/src/messages/search/SubtierPredicate.hpp b/src/messages/search/SubtierPredicate.hpp new file mode 100644 index 000000000..cf24defc8 --- /dev/null +++ b/src/messages/search/SubtierPredicate.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include "messages/search/MessagePredicate.hpp" + +#include +#include + +namespace chatterino { + +/** + * @brief MessagePredicate checking for the badges of a message. + * + * This predicate will only allow messages that are sent by a subscribed user + * who has a specified subtier (i.e. 1,2,3..) + */ +class SubtierPredicate : public MessagePredicate +{ +public: + /** + * @brief Create an SubtierPredicate with a list of subtiers to search for. + * + * @param subtiers one or more comma-separated subtiers that the message should contain + * @param negate when set, excludes messages containing selected subtiers from results + */ + SubtierPredicate(const QString &subtiers, bool negate); + +protected: + /** + * @brief Checks whether the message contains any of the subtiers passed + * in the constructor. + * + * @param message the message to check + * @return true if the message contains a subtier listed in the specified subtiers, + * false otherwise + */ + bool appliesToImpl(const Message &message) override; + +private: + /// Holds the subtiers that will be searched for + QStringList subtiers_; +}; + +} // namespace chatterino diff --git a/src/providers/IvrApi.cpp b/src/providers/IvrApi.cpp index dff138ef9..02d733b18 100644 --- a/src/providers/IvrApi.cpp +++ b/src/providers/IvrApi.cpp @@ -1,6 +1,6 @@ -#include "IvrApi.hpp" +#include "providers/IvrApi.hpp" -#include "common/Outcome.hpp" +#include "common/network/NetworkResult.hpp" #include "common/QLogging.hpp" #include @@ -17,16 +17,14 @@ void IvrApi::getSubage(QString userName, QString channelName, this->makeRequest( QString("twitch/subage/%1/%2").arg(userName).arg(channelName), {}) - .onSuccess([successCallback, failureCallback](auto result) -> Outcome { + .onSuccess([successCallback, failureCallback](auto result) { auto root = result.parseJson(); successCallback(root); - - return Success; }) .onError([failureCallback](auto result) { qCWarning(chatterinoIvr) - << "Failed IVR API Call!" << result.status() + << "Failed IVR API Call!" << result.formatError() << QString(result.getData()); failureCallback(); }) @@ -40,17 +38,15 @@ void IvrApi::getBulkEmoteSets(QString emoteSetList, QUrlQuery urlQuery; urlQuery.addQueryItem("set_id", emoteSetList); - this->makeRequest("v2/twitch/emotes/sets", urlQuery) - .onSuccess([successCallback, failureCallback](auto result) -> Outcome { + this->makeRequest("twitch/emotes/sets", urlQuery) + .onSuccess([successCallback, failureCallback](auto result) { auto root = result.parseJsonArray(); successCallback(root); - - return Success; }) .onError([failureCallback](auto result) { qCWarning(chatterinoIvr) - << "Failed IVR API Call!" << result.status() + << "Failed IVR API Call!" << result.formatError() << QString(result.getData()); failureCallback(); }) @@ -61,7 +57,7 @@ NetworkRequest IvrApi::makeRequest(QString url, QUrlQuery urlQuery) { assert(!url.startsWith("/")); - const QString baseUrl("https://api.ivr.fi/"); + const QString baseUrl("https://api.ivr.fi/v2/"); QUrl fullUrl(baseUrl + url); fullUrl.setQuery(urlQuery); diff --git a/src/providers/IvrApi.hpp b/src/providers/IvrApi.hpp index 7008b240f..808471897 100644 --- a/src/providers/IvrApi.hpp +++ b/src/providers/IvrApi.hpp @@ -1,10 +1,10 @@ #pragma once -#include "common/NetworkRequest.hpp" -#include "messages/Link.hpp" +#include "common/network/NetworkRequest.hpp" #include "providers/twitch/TwitchEmotes.hpp" -#include +#include +#include #include @@ -21,9 +21,9 @@ struct IvrSubage { const int totalSubMonths; const QString followingSince; - IvrSubage(QJsonObject root) - : isSubHidden(root.value("hidden").toBool()) - , isSubbed(root.value("subscribed").toBool()) + IvrSubage(const QJsonObject &root) + : isSubHidden(root.value("statusHidden").toBool()) + , isSubbed(!root.value("meta").isNull()) , subTier(root.value("meta").toObject().value("tier").toString()) , totalSubMonths( root.value("cumulative").toObject().value("months").toInt()) @@ -40,7 +40,7 @@ struct IvrEmoteSet { const QString tier; const QJsonArray emotes; - IvrEmoteSet(QJsonObject root) + IvrEmoteSet(const QJsonObject &root) : setId(root.value("setID").toString()) , displayName(root.value("channelName").toString()) , login(root.value("channelLogin").toString()) @@ -60,23 +60,21 @@ struct IvrEmote { const QString emoteType; const QString imageType; - explicit IvrEmote(QJsonObject root) + explicit IvrEmote(const QJsonObject &root) : code(root.value("code").toString()) , id(root.value("id").toString()) , setId(root.value("setID").toString()) - , url(QString(TWITCH_EMOTE_TEMPLATE) - .replace("{id}", this->id) - .replace("{scale}", "3.0")) + , url(TWITCH_EMOTE_TEMPLATE.arg(this->id, u"3.0")) , emoteType(root.value("type").toString()) , imageType(root.value("assetType").toString()) { } }; -class IvrApi final : boost::noncopyable +class IvrApi final { public: - // https://api.ivr.fi/docs#tag/Twitch/paths/~1twitch~1subage~1{username}~1{channel}/get + // https://api.ivr.fi/v2/docs/static/index.html#/Twitch/get_twitch_subage__user___channel_ void getSubage(QString userName, QString channelName, ResultCallback resultCallback, IvrFailureCallback failureCallback); @@ -88,6 +86,14 @@ public: static void initialize(); + IvrApi() = default; + + IvrApi(const IvrApi &) = delete; + IvrApi &operator=(const IvrApi &) = delete; + + IvrApi(IvrApi &&) = delete; + IvrApi &operator=(IvrApi &&) = delete; + private: NetworkRequest makeRequest(QString url, QUrlQuery urlQuery); }; diff --git a/src/providers/LinkResolver.cpp b/src/providers/LinkResolver.cpp deleted file mode 100644 index b0d8f5a93..000000000 --- a/src/providers/LinkResolver.cpp +++ /dev/null @@ -1,67 +0,0 @@ -#include "providers/LinkResolver.hpp" - -#include "common/Common.hpp" -#include "common/Env.hpp" -#include "common/NetworkRequest.hpp" -#include "messages/Image.hpp" -#include "messages/Link.hpp" -#include "singletons/Settings.hpp" - -#include - -namespace chatterino { - -void LinkResolver::getLinkInfo( - const QString url, QObject *caller, - std::function successCallback) -{ - if (!getSettings()->linkInfoTooltip) - { - successCallback("No link info loaded", Link(Link::Url, url), nullptr); - return; - } - // Uncomment to test crashes - // QTimer::singleShot(3000, [=]() { - NetworkRequest(Env::get().linkResolverUrl.arg(QString::fromUtf8( - QUrl::toPercentEncoding(url, "", "/:")))) - .caller(caller) - .timeout(30000) - .onSuccess([successCallback, - url](NetworkResult result) mutable -> Outcome { - auto root = result.parseJson(); - auto statusCode = root.value("status").toInt(); - QString response; - QString linkString = url; - ImagePtr thumbnail = nullptr; - if (statusCode == 200) - { - response = root.value("tooltip").toString(); - - if (root.contains("thumbnail")) - { - thumbnail = - Image::fromUrl({root.value("thumbnail").toString()}); - } - if (getSettings()->unshortLinks) - { - linkString = root.value("link").toString(); - } - } - else - { - response = root.value("message").toString(); - } - successCallback(QUrl::fromPercentEncoding(response.toUtf8()), - Link(Link::Url, linkString), thumbnail); - - return Success; - }) - .onError([successCallback, url](auto /*result*/) { - successCallback("No link info found", Link(Link::Url, url), - nullptr); - }) - .execute(); - // }); -} - -} // namespace chatterino diff --git a/src/providers/LinkResolver.hpp b/src/providers/LinkResolver.hpp deleted file mode 100644 index d1b24d690..000000000 --- a/src/providers/LinkResolver.hpp +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include -#include - -#include "messages/Image.hpp" -#include "messages/Link.hpp" - -namespace chatterino { - -class LinkResolver -{ -public: - static void getLinkInfo( - const QString url, QObject *caller, - std::function callback); -}; - -} // namespace chatterino diff --git a/src/providers/NetworkConfigurationProvider.cpp b/src/providers/NetworkConfigurationProvider.cpp new file mode 100644 index 000000000..dca88e8a6 --- /dev/null +++ b/src/providers/NetworkConfigurationProvider.cpp @@ -0,0 +1,76 @@ +#include "providers/NetworkConfigurationProvider.hpp" + +#include "common/Env.hpp" +#include "common/QLogging.hpp" + +#include +#include +#include + +namespace { +/** + * Creates a QNetworkProxy from a given URL. + * + * Creates an HTTP proxy by default, a Socks5 will be created if the scheme is 'socks5'. + */ +QNetworkProxy createProxyFromUrl(const QUrl &url) +{ + QNetworkProxy proxy; + proxy.setHostName(url.host(QUrl::FullyEncoded)); + proxy.setUser(url.userName(QUrl::FullyEncoded)); + proxy.setPassword(url.password(QUrl::FullyEncoded)); + proxy.setPort(url.port(1080)); + + if (url.scheme().compare(QStringLiteral("socks5"), Qt::CaseInsensitive) == + 0) + { + proxy.setType(QNetworkProxy::Socks5Proxy); + } + else + { + proxy.setType(QNetworkProxy::HttpProxy); + if (!proxy.user().isEmpty() || !proxy.password().isEmpty()) + { + // for some reason, Qt doesn't set the Proxy-Authorization header + const auto auth = proxy.user() + ":" + proxy.password(); + const auto base64 = auth.toUtf8().toBase64(); + proxy.setRawHeader("Proxy-Authorization", + QByteArray("Basic ").append(base64)); + } + } + + return proxy; +} + +/** + * Attempts to apply the proxy specified by `url` as the application proxy. + */ +void applyProxy(const QString &url) +{ + auto proxyUrl = QUrl(url); + if (!proxyUrl.isValid() || proxyUrl.isEmpty()) + { + qCDebug(chatterinoNetwork) + << "Invalid or empty proxy url: " << proxyUrl; + return; + } + + const auto proxy = createProxyFromUrl(proxyUrl); + + QNetworkProxy::setApplicationProxy(proxy); + qCDebug(chatterinoNetwork) << "Set application proxy to" << proxy; +} + +} // namespace + +namespace chatterino { + +void NetworkConfigurationProvider::applyFromEnv(const Env &env) +{ + if (env.proxyUrl) + { + applyProxy(*env.proxyUrl); + } +} + +} // namespace chatterino diff --git a/src/providers/NetworkConfigurationProvider.hpp b/src/providers/NetworkConfigurationProvider.hpp new file mode 100644 index 000000000..6e277cfd8 --- /dev/null +++ b/src/providers/NetworkConfigurationProvider.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include "common/QLogging.hpp" + +#include +#include + +#include + +namespace chatterino { + +class Env; + +/** This class manipulates the global network configuration (e.g. proxies). */ +class NetworkConfigurationProvider +{ +public: + /** This class should never be instantiated. */ + NetworkConfigurationProvider() = delete; + + /** + * Applies the configuration requested from the environment variables. + * + * Currently a proxy is applied if configured. + */ + static void applyFromEnv(const Env &env); + + static void applyToWebSocket(const auto &connection) + { + const auto applicationProxy = QNetworkProxy::applicationProxy(); + if (applicationProxy.type() != QNetworkProxy::HttpProxy) + { + return; + } + std::string url = "http://"; + url += applicationProxy.hostName().toStdString(); + url += ":"; + url += std::to_string(applicationProxy.port()); + websocketpp::lib::error_code ec; + connection->set_proxy(url, ec); + if (ec) + { + qCDebug(chatterinoNetwork) + << "Couldn't set websocket proxy:" << ec.value(); + return; + } + + connection->set_proxy_basic_auth( + applicationProxy.user().toStdString(), + applicationProxy.password().toStdString(), ec); + if (ec) + { + qCDebug(chatterinoNetwork) + << "Couldn't set websocket proxy auth:" << ec.value(); + } + } +}; + +} // namespace chatterino diff --git a/src/providers/RecentMessagesApi.cpp b/src/providers/RecentMessagesApi.cpp deleted file mode 100644 index 48c9d7a42..000000000 --- a/src/providers/RecentMessagesApi.cpp +++ /dev/null @@ -1,229 +0,0 @@ -#include "RecentMessagesApi.hpp" - -#include "common/Channel.hpp" -#include "common/Common.hpp" -#include "common/Env.hpp" -#include "common/NetworkRequest.hpp" -#include "common/QLogging.hpp" -#include "providers/twitch/IrcMessageHandler.hpp" -#include "providers/twitch/TwitchChannel.hpp" -#include "providers/twitch/TwitchMessageBuilder.hpp" -#include "singletons/Settings.hpp" -#include "util/FormatTime.hpp" -#include "util/PostToThread.hpp" - -#include -#include -#include -#include - -namespace chatterino { - -namespace { - - // convertClearchatToNotice takes a Communi::IrcMessage that is a CLEARCHAT - // command and converts it to a readable NOTICE message. This has - // historically been done in the Recent Messages API, but this functionality - // has been moved to Chatterino instead. - auto convertClearchatToNotice(Communi::IrcMessage *message) - { - auto channelName = message->parameter(0); - QString noticeMessage{}; - if (message->tags().contains("target-user-id")) - { - auto target = message->parameter(1); - - if (message->tags().contains("ban-duration")) - { - // User was timed out - noticeMessage = - QString("%1 has been timed out for %2.") - .arg(target) - .arg(formatTime( - message->tag("ban-duration").toString())); - } - else - { - // User was permanently banned - noticeMessage = - QString("%1 has been permanently banned.").arg(target); - } - } - else - { - // Chat was cleared - noticeMessage = "Chat has been cleared by a moderator."; - } - - // rebuild the raw IRC message so we can convert it back to an ircmessage again! - // this could probably be done in a smarter way - - auto s = QString(":tmi.twitch.tv NOTICE %1 :%2") - .arg(channelName) - .arg(noticeMessage); - - auto newMessage = Communi::IrcMessage::fromData(s.toUtf8(), nullptr); - newMessage->setTags(message->tags()); - - return newMessage; - } - - // Parse the IRC messages returned in JSON form into Communi messages - std::vector parseRecentMessages( - const QJsonObject &jsonRoot) - { - QJsonArray jsonMessages = jsonRoot.value("messages").toArray(); - std::vector messages; - - if (jsonMessages.empty()) - return messages; - - for (const auto jsonMessage : jsonMessages) - { - auto content = jsonMessage.toString(); - content.replace(COMBINED_FIXER, ZERO_WIDTH_JOINER); - - auto message = - Communi::IrcMessage::fromData(content.toUtf8(), nullptr); - - if (message->command() == "CLEARCHAT") - { - message = convertClearchatToNotice(message); - } - - messages.emplace_back(std::move(message)); - } - - return messages; - } - - // Build Communi messages retrieved from the recent messages API into - // proper chatterino messages. - std::vector buildRecentMessages( - std::vector &messages, Channel *channel) - { - auto &handler = IrcMessageHandler::instance(); - std::vector allBuiltMessages; - - for (auto message : messages) - { - if (message->tags().contains("rm-received-ts")) - { - QDate msgDate = - QDateTime::fromMSecsSinceEpoch( - message->tags().value("rm-received-ts").toLongLong()) - .date(); - - // Check if we need to insert a message stating that a new day began - if (msgDate != channel->lastDate_) - { - channel->lastDate_ = msgDate; - auto msg = makeSystemMessage( - QLocale().toString(msgDate, QLocale::LongFormat), - QTime(0, 0)); - msg->flags.set(MessageFlag::RecentMessage); - allBuiltMessages.emplace_back(msg); - } - } - - auto builtMessages = handler.parseMessageWithReply( - channel, message, allBuiltMessages); - - for (auto builtMessage : builtMessages) - { - builtMessage->flags.set(MessageFlag::RecentMessage); - allBuiltMessages.emplace_back(builtMessage); - } - } - - return allBuiltMessages; - } - - // Returns the URL to be used for querying the Recent Messages API for the - // given channel. - QUrl constructRecentMessagesUrl(const QString &name) - { - QUrl url(Env::get().recentMessagesApiUrl.arg(name)); - QUrlQuery urlQuery(url); - if (!urlQuery.hasQueryItem("limit")) - { - urlQuery.addQueryItem( - "limit", - QString::number(getSettings()->twitchMessageHistoryLimit)); - } - url.setQuery(urlQuery); - return url; - } - -} // namespace - -void RecentMessagesApi::loadRecentMessages(const QString &channelName, - std::weak_ptr channelPtr, - ResultCallback onLoaded, - ErrorCallback onError) -{ - qCDebug(chatterinoRecentMessages) - << "Loading recent messages for" << channelName; - - QUrl url = constructRecentMessagesUrl(channelName); - - NetworkRequest(url) - .onSuccess([channelPtr, onLoaded](NetworkResult result) -> Outcome { - auto shared = channelPtr.lock(); - if (!shared) - return Failure; - - qCDebug(chatterinoRecentMessages) - << "Successfully loaded recent messages for" - << shared->getName(); - - auto root = result.parseJson(); - auto parsedMessages = parseRecentMessages(root); - - // build the Communi messages into chatterino messages - auto builtMessages = - buildRecentMessages(parsedMessages, shared.get()); - - postToThread([shared = std::move(shared), root = std::move(root), - messages = std::move(builtMessages), - onLoaded]() mutable { - // Notify user about a possible gap in logs if it returned some messages - // but isn't currently joined to a channel - if (QString errorCode = root.value("error_code").toString(); - !errorCode.isEmpty()) - { - qCDebug(chatterinoRecentMessages) - << QString("Got error from API: error_code=%1, " - "channel=%2") - .arg(errorCode, shared->getName()); - if (errorCode == "channel_not_joined" && !messages.empty()) - { - shared->addMessage(makeSystemMessage( - "Message history service recovering, there may " - "be gaps in the message history.")); - } - } - - onLoaded(messages); - }); - - return Success; - }) - .onError([channelPtr, onError](NetworkResult result) { - auto shared = channelPtr.lock(); - if (!shared) - return; - - qCDebug(chatterinoRecentMessages) - << "Failed to load recent messages for" << shared->getName(); - - shared->addMessage(makeSystemMessage( - QString("Message history service unavailable (Error %1)") - .arg(result.status()))); - - onError(); - }) - .execute(); -} - -} // namespace chatterino diff --git a/src/providers/RecentMessagesApi.hpp b/src/providers/RecentMessagesApi.hpp deleted file mode 100644 index 30137d5d2..000000000 --- a/src/providers/RecentMessagesApi.hpp +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once - -#include "ForwardDecl.hpp" - -#include - -#include -#include -#include - -namespace chatterino { - -class RecentMessagesApi -{ -public: - using ResultCallback = std::function &)>; - using ErrorCallback = std::function; - - /** - * @brief Loads recent messages for a channel using the Recent Messages API - * - * @param channelName Name of Twitch channel - * @param channelPtr Weak pointer to Channel to use to build messages - * @param onLoaded Callback taking the built messages as a const std::vector & - * @param onError Callback called when the network request fails - */ - static void loadRecentMessages(const QString &channelName, - std::weak_ptr channelPtr, - ResultCallback onLoaded, - ErrorCallback onError); -}; - -} // namespace chatterino diff --git a/src/providers/bttv/BttvEmotes.cpp b/src/providers/bttv/BttvEmotes.cpp index 6785ecede..60bd07b3c 100644 --- a/src/providers/bttv/BttvEmotes.cpp +++ b/src/providers/bttv/BttvEmotes.cpp @@ -1,128 +1,191 @@ #include "providers/bttv/BttvEmotes.hpp" -#include -#include - -#include "common/Common.hpp" -#include "common/NetworkRequest.hpp" +#include "common/network/NetworkRequest.hpp" +#include "common/network/NetworkResult.hpp" +#include "common/Outcome.hpp" #include "common/QLogging.hpp" #include "messages/Emote.hpp" #include "messages/Image.hpp" #include "messages/ImageSet.hpp" #include "messages/MessageBuilder.hpp" +#include "providers/bttv/liveupdates/BttvLiveUpdateMessages.hpp" #include "providers/twitch/TwitchChannel.hpp" #include "singletons/Settings.hpp" -namespace chatterino { +#include +#include + namespace { - const QString CHANNEL_HAS_NO_EMOTES( - "This channel has no BetterTTV channel emotes."); +using namespace chatterino; - QString emoteLinkFormat("https://betterttv.com/emotes/%1"); +const QString CHANNEL_HAS_NO_EMOTES( + "This channel has no BetterTTV channel emotes."); - Url getEmoteLink(QString urlTemplate, const EmoteId &id, - const QString &emoteScale) +/// The emote page template. +/// +/// %1 being the emote ID (e.g. 566ca04265dbbdab32ec054a) +constexpr QStringView EMOTE_LINK_FORMAT = u"https://betterttv.com/emotes/%1"; + +/// The emote CDN link template. +/// +/// %1 being the emote ID (e.g. 566ca04265dbbdab32ec054a) +/// +/// %2 being the emote size (e.g. 3x) +constexpr QStringView EMOTE_CDN_FORMAT = + u"https://cdn.betterttv.net/emote/%1/%2"; + +// BTTV doesn't provide any data on the size, so we assume an emote is 28x28 +constexpr QSize EMOTE_BASE_SIZE(28, 28); + +struct CreateEmoteResult { + EmoteId id; + EmoteName name; + Emote emote; +}; + +Url getEmoteLinkV3(const EmoteId &id, const QString &emoteScale) +{ + return {EMOTE_CDN_FORMAT.arg(id.string, emoteScale)}; +} + +EmotePtr cachedOrMake(Emote &&emote, const EmoteId &id) +{ + static std::unordered_map> cache; + static std::mutex mutex; + + return cachedOrMakeEmotePtr(std::move(emote), cache, mutex, id); +} + +std::pair parseGlobalEmotes(const QJsonArray &jsonEmotes, + const EmoteMap ¤tEmotes) +{ + auto emotes = EmoteMap(); + + for (auto jsonEmote : jsonEmotes) { - urlTemplate.detach(); + auto id = EmoteId{jsonEmote.toObject().value("id").toString()}; + auto name = EmoteName{jsonEmote.toObject().value("code").toString()}; - return {urlTemplate.replace("{{id}}", id.string) - .replace("{{image}}", emoteScale)}; + auto emote = Emote({ + name, + ImageSet{ + Image::fromUrl(getEmoteLinkV3(id, "1x"), 1, EMOTE_BASE_SIZE), + Image::fromUrl(getEmoteLinkV3(id, "2x"), 0.5, + EMOTE_BASE_SIZE * 2), + Image::fromUrl(getEmoteLinkV3(id, "3x"), 0.25, + EMOTE_BASE_SIZE * 4)}, + Tooltip{name.string + "
Global BetterTTV Emote"}, + Url{EMOTE_LINK_FORMAT.arg(id.string)}, + }); + + emotes[name] = cachedOrMakeEmotePtr(std::move(emote), currentEmotes); } - Url getEmoteLinkV3(const EmoteId &id, const QString &emoteScale) + return {Success, std::move(emotes)}; +} + +CreateEmoteResult createChannelEmote(const QString &channelDisplayName, + const QJsonObject &jsonEmote) +{ + auto id = EmoteId{jsonEmote.value("id").toString()}; + auto name = EmoteName{jsonEmote.value("code").toString()}; + auto author = EmoteAuthor{ + jsonEmote.value("user").toObject().value("displayName").toString()}; + + auto emote = Emote({ + name, + ImageSet{ + Image::fromUrl(getEmoteLinkV3(id, "1x"), 1, EMOTE_BASE_SIZE), + Image::fromUrl(getEmoteLinkV3(id, "2x"), 0.5, EMOTE_BASE_SIZE * 2), + Image::fromUrl(getEmoteLinkV3(id, "3x"), 0.25, EMOTE_BASE_SIZE * 4), + }, + Tooltip{ + QString("%1
%2 BetterTTV Emote
By: %3") + .arg(name.string) + // when author is empty, it is a channel emote created by the broadcaster + .arg(author.string.isEmpty() ? "Channel" : "Shared") + .arg(author.string.isEmpty() ? channelDisplayName + : author.string)}, + Url{EMOTE_LINK_FORMAT.arg(id.string)}, + false, + id, + }); + + return {id, name, emote}; +} + +bool updateChannelEmote(Emote &emote, const QString &channelDisplayName, + const QJsonObject &jsonEmote) +{ + bool anyModifications = false; + + if (jsonEmote.contains("code")) { - static const QString urlTemplate( - "https://cdn.betterttv.net/emote/%1/%2"); - - return {urlTemplate.arg(id.string, emoteScale)}; + emote.name = EmoteName{jsonEmote.value("code").toString()}; + anyModifications = true; } - EmotePtr cachedOrMake(Emote &&emote, const EmoteId &id) + if (jsonEmote.contains("user")) { - static std::unordered_map> cache; - static std::mutex mutex; - - return cachedOrMakeEmotePtr(std::move(emote), cache, mutex, id); + emote.author = EmoteAuthor{ + jsonEmote.value("user").toObject().value("displayName").toString()}; + anyModifications = true; } - std::pair parseGlobalEmotes( - const QJsonArray &jsonEmotes, const EmoteMap ¤tEmotes) + + if (anyModifications) { - auto emotes = EmoteMap(); - - for (auto jsonEmote : jsonEmotes) - { - auto id = EmoteId{jsonEmote.toObject().value("id").toString()}; - auto name = - EmoteName{jsonEmote.toObject().value("code").toString()}; - - auto emote = Emote({ - name, - ImageSet{Image::fromUrl(getEmoteLinkV3(id, "1x"), 1), - Image::fromUrl(getEmoteLinkV3(id, "2x"), 0.5), - Image::fromUrl(getEmoteLinkV3(id, "3x"), 0.25)}, - Tooltip{name.string + "
Global BetterTTV Emote"}, - Url{emoteLinkFormat.arg(id.string)}, - }); - - emotes[name] = - cachedOrMakeEmotePtr(std::move(emote), currentEmotes); - } - - return {Success, std::move(emotes)}; + emote.tooltip = Tooltip{ + QString("%1
%2 BetterTTV Emote
By: %3") + .arg(emote.name.string) + // when author is empty, it is a channel emote created by the broadcaster + .arg(emote.author.string.isEmpty() ? "Channel" : "Shared") + .arg(emote.author.string.isEmpty() ? channelDisplayName + : emote.author.string)}; } - std::pair parseChannelEmotes( - const QJsonObject &jsonRoot, const QString &channelDisplayName) - { - auto emotes = EmoteMap(); - auto innerParse = [&jsonRoot, &emotes, - &channelDisplayName](const char *key) { - auto jsonEmotes = jsonRoot.value(key).toArray(); - for (auto jsonEmote_ : jsonEmotes) - { - auto jsonEmote = jsonEmote_.toObject(); + return anyModifications; +} - auto id = EmoteId{jsonEmote.value("id").toString()}; - auto name = EmoteName{jsonEmote.value("code").toString()}; - auto author = EmoteAuthor{jsonEmote.value("user") - .toObject() - .value("displayName") - .toString()}; - - auto emote = Emote({ - name, - ImageSet{ - Image::fromUrl(getEmoteLinkV3(id, "1x"), 1), - Image::fromUrl(getEmoteLinkV3(id, "2x"), 0.5), - Image::fromUrl(getEmoteLinkV3(id, "3x"), 0.25), - }, - Tooltip{ - QString("%1
%2 BetterTTV Emote
By: %3") - .arg(name.string) - // when author is empty, it is a channel emote created by the broadcaster - .arg(author.string.isEmpty() ? "Channel" : "Shared") - .arg(author.string.isEmpty() ? channelDisplayName - : author.string)}, - Url{emoteLinkFormat.arg(id.string)}, - }); - - emotes[name] = cachedOrMake(std::move(emote), id); - } - }; - - innerParse("channelEmotes"); - innerParse("sharedEmotes"); - - return {Success, std::move(emotes)}; - } } // namespace +namespace chatterino { + +using namespace bttv::detail; + +EmoteMap bttv::detail::parseChannelEmotes(const QJsonObject &jsonRoot, + const QString &channelDisplayName) +{ + auto emotes = EmoteMap(); + + auto innerParse = [&jsonRoot, &emotes, + &channelDisplayName](const char *key) { + auto jsonEmotes = jsonRoot.value(key).toArray(); + for (auto jsonEmote_ : jsonEmotes) + { + auto emote = + createChannelEmote(channelDisplayName, jsonEmote_.toObject()); + + emotes[emote.name] = cachedOrMake(std::move(emote.emote), emote.id); + } + }; + + innerParse("channelEmotes"); + innerParse("sharedEmotes"); + + return emotes; +} + // // BttvEmotes // BttvEmotes::BttvEmotes() : global_(std::make_shared()) { + getSettings()->enableBTTVGlobalEmotes.connect( + [this] { + this->loadEmotes(); + }, + this->managedConnections, false); } std::shared_ptr BttvEmotes::emotes() const @@ -130,13 +193,16 @@ std::shared_ptr BttvEmotes::emotes() const return this->global_.get(); } -boost::optional BttvEmotes::emote(const EmoteName &name) const +std::optional BttvEmotes::emote(const EmoteName &name) const { auto emotes = this->global_.get(); auto it = emotes->find(name); if (it == emotes->end()) - return boost::none; + { + return std::nullopt; + } + return it->second; } @@ -144,23 +210,29 @@ void BttvEmotes::loadEmotes() { if (!Settings::instance().enableBTTVGlobalEmotes) { - this->global_.set(EMPTY_EMOTE_MAP); + this->setEmotes(EMPTY_EMOTE_MAP); return; } NetworkRequest(QString(globalEmoteApiUrl)) .timeout(30000) - .onSuccess([this](auto result) -> Outcome { + .onSuccess([this](auto result) { auto emotes = this->global_.get(); auto pair = parseGlobalEmotes(result.parseJsonArray(), *emotes); if (pair.first) - this->global_.set( + { + this->setEmotes( std::make_shared(std::move(pair.second))); - return pair.first; + } }) .execute(); } +void BttvEmotes::setEmotes(std::shared_ptr emotes) +{ + this->global_.set(std::move(emotes)); +} + void BttvEmotes::loadChannel(std::weak_ptr channel, const QString &channelId, const QString &channelDisplayName, @@ -169,74 +241,127 @@ void BttvEmotes::loadChannel(std::weak_ptr channel, { NetworkRequest(QString(bttvChannelEmoteApiUrl) + channelId) .timeout(20000) - .onSuccess([callback = std::move(callback), channel, - &channelDisplayName, - manualRefresh](auto result) -> Outcome { - auto pair = + .onSuccess([callback = std::move(callback), channel, channelDisplayName, + manualRefresh](auto result) { + auto emotes = parseChannelEmotes(result.parseJson(), channelDisplayName); - bool hasEmotes = false; - if (pair.first) - { - hasEmotes = !pair.second.empty(); - callback(std::move(pair.second)); - } + bool hasEmotes = !emotes.empty(); + callback(std::move(emotes)); + if (auto shared = channel.lock(); manualRefresh) { if (hasEmotes) { - shared->addMessage(makeSystemMessage( - "BetterTTV channel emotes reloaded.")); + shared->addSystemMessage( + "BetterTTV channel emotes reloaded."); } else { - shared->addMessage( - makeSystemMessage(CHANNEL_HAS_NO_EMOTES)); + shared->addSystemMessage(CHANNEL_HAS_NO_EMOTES); } } - return pair.first; }) .onError([channelId, channel, manualRefresh](auto result) { auto shared = channel.lock(); if (!shared) + { return; + } + if (result.status() == 404) { // User does not have any BTTV emotes if (manualRefresh) - shared->addMessage( - makeSystemMessage(CHANNEL_HAS_NO_EMOTES)); - } - else if (result.status() == NetworkResult::timedoutStatus) - { - // TODO: Auto retry in case of a timeout, with a delay - qCWarning(chatterinoBttv) - << "Fetching BTTV emotes for channel" << channelId - << "failed due to timeout"; - shared->addMessage(makeSystemMessage( - "Failed to fetch BetterTTV channel emotes. (timed out)")); + { + shared->addSystemMessage(CHANNEL_HAS_NO_EMOTES); + } } else { + // TODO: Auto retry in case of a timeout, with a delay + auto errorString = result.formatError(); qCWarning(chatterinoBttv) << "Error fetching BTTV emotes for channel" << channelId - << ", error" << result.status(); - shared->addMessage( - makeSystemMessage("Failed to fetch BetterTTV channel " - "emotes. (unknown error)")); + << ", error" << errorString; + shared->addSystemMessage( + QStringLiteral("Failed to fetch BetterTTV channel " + "emotes. (Error: %1)") + .arg(errorString)); } }) .execute(); } -/* -static Url getEmoteLink(QString urlTemplate, const EmoteId &id, - const QString &emoteScale) +EmotePtr BttvEmotes::addEmote( + const QString &channelDisplayName, + Atomic> &channelEmoteMap, + const BttvLiveUpdateEmoteUpdateAddMessage &message) { - urlTemplate.detach(); + // This copies the map. + EmoteMap updatedMap = *channelEmoteMap.get(); + auto result = createChannelEmote(channelDisplayName, message.jsonEmote); - return {urlTemplate.replace("{{id}}", id.string) - .replace("{{image}}", emoteScale)}; + auto emote = std::make_shared(std::move(result.emote)); + updatedMap[result.name] = emote; + channelEmoteMap.set(std::make_shared(std::move(updatedMap))); + + return emote; +} + +std::optional> BttvEmotes::updateEmote( + const QString &channelDisplayName, + Atomic> &channelEmoteMap, + const BttvLiveUpdateEmoteUpdateAddMessage &message) +{ + // This copies the map. + EmoteMap updatedMap = *channelEmoteMap.get(); + + // Step 1: remove the existing emote + auto it = updatedMap.findEmote(QString(), message.emoteID); + if (it == updatedMap.end()) + { + // We already copied the map at this point and are now discarding the copy. + // This is fine, because this case should be really rare. + return std::nullopt; + } + auto oldEmotePtr = it->second; + // copy the existing emote, to not change the original one + auto emote = *oldEmotePtr; + updatedMap.erase(it); + + // Step 2: update the emote + if (!updateChannelEmote(emote, channelDisplayName, message.jsonEmote)) + { + // The emote wasn't actually updated + return std::nullopt; + } + + auto name = emote.name; + auto emotePtr = std::make_shared(std::move(emote)); + updatedMap[name] = emotePtr; + channelEmoteMap.set(std::make_shared(std::move(updatedMap))); + + return std::make_pair(oldEmotePtr, emotePtr); +} + +std::optional BttvEmotes::removeEmote( + Atomic> &channelEmoteMap, + const BttvLiveUpdateEmoteRemoveMessage &message) +{ + // This copies the map. + EmoteMap updatedMap = *channelEmoteMap.get(); + auto it = updatedMap.findEmote(QString(), message.emoteID); + if (it == updatedMap.end()) + { + // We already copied the map at this point and are now discarding the copy. + // This is fine, because this case should be really rare. + return std::nullopt; + } + auto emote = it->second; + updatedMap.erase(it); + channelEmoteMap.set(std::make_shared(std::move(updatedMap))); + + return emote; } -*/ } // namespace chatterino diff --git a/src/providers/bttv/BttvEmotes.hpp b/src/providers/bttv/BttvEmotes.hpp index 81a86bca5..788b1e430 100644 --- a/src/providers/bttv/BttvEmotes.hpp +++ b/src/providers/bttv/BttvEmotes.hpp @@ -1,16 +1,33 @@ #pragma once -#include -#include "boost/optional.hpp" #include "common/Aliases.hpp" #include "common/Atomic.hpp" -#include "providers/twitch/TwitchChannel.hpp" + +#include +#include +#include + +#include +#include +#include +#include +#include namespace chatterino { struct Emote; using EmotePtr = std::shared_ptr; class EmoteMap; +class Channel; +struct BttvLiveUpdateEmoteUpdateAddMessage; +struct BttvLiveUpdateEmoteRemoveMessage; + +namespace bttv::detail { + + EmoteMap parseChannelEmotes(const QJsonObject &jsonRoot, + const QString &channelDisplayName); + +} // namespace bttv::detail class BttvEmotes final { @@ -23,16 +40,55 @@ public: BttvEmotes(); std::shared_ptr emotes() const; - boost::optional emote(const EmoteName &name) const; + std::optional emote(const EmoteName &name) const; void loadEmotes(); + void setEmotes(std::shared_ptr emotes); static void loadChannel(std::weak_ptr channel, const QString &channelId, const QString &channelDisplayName, std::function callback, bool manualRefresh); + /** + * Adds an emote to the `channelEmoteMap`. + * This will _copy_ the emote map and + * update the `Atomic`. + * + * @return The added emote. + */ + static EmotePtr addEmote( + const QString &channelDisplayName, + Atomic> &channelEmoteMap, + const BttvLiveUpdateEmoteUpdateAddMessage &message); + + /** + * Updates an emote in this `channelEmoteMap`. + * This will _copy_ the emote map and + * update the `Atomic`. + * + * @return pair if any emote was updated. + */ + static std::optional> updateEmote( + const QString &channelDisplayName, + Atomic> &channelEmoteMap, + const BttvLiveUpdateEmoteUpdateAddMessage &message); + + /** + * Removes an emote from this `channelEmoteMap`. + * This will _copy_ the emote map and + * update the `Atomic`. + * + * @return The removed emote if any emote was removed. + */ + static std::optional removeEmote( + Atomic> &channelEmoteMap, + const BttvLiveUpdateEmoteRemoveMessage &message); + private: Atomic> global_; + + std::vector> + managedConnections; }; } // namespace chatterino diff --git a/src/providers/bttv/BttvLiveUpdates.cpp b/src/providers/bttv/BttvLiveUpdates.cpp new file mode 100644 index 000000000..9794665ad --- /dev/null +++ b/src/providers/bttv/BttvLiveUpdates.cpp @@ -0,0 +1,105 @@ +#include "providers/bttv/BttvLiveUpdates.hpp" + +#include "common/Literals.hpp" + +#include + +#include + +namespace chatterino { + +using namespace chatterino::literals; + +BttvLiveUpdates::BttvLiveUpdates(QString host) + : BasicPubSubManager(std::move(host), u"BTTV"_s) +{ +} + +BttvLiveUpdates::~BttvLiveUpdates() +{ + this->stop(); +} + +void BttvLiveUpdates::joinChannel(const QString &channelID, + const QString &userName) +{ + if (this->joinedChannels_.insert(channelID).second) + { + this->subscribe({BttvLiveUpdateSubscriptionChannel{channelID}}); + this->subscribe({BttvLiveUpdateBroadcastMe{.twitchID = channelID, + .userName = userName}}); + } +} + +void BttvLiveUpdates::partChannel(const QString &id) +{ + if (this->joinedChannels_.erase(id) > 0) + { + this->unsubscribe({BttvLiveUpdateSubscriptionChannel{id}}); + } +} + +void BttvLiveUpdates::onMessage( + websocketpp::connection_hdl /*hdl*/, + BasicPubSubManager::WebsocketMessagePtr msg) +{ + const auto &payload = QString::fromStdString(msg->get_payload()); + QJsonDocument jsonDoc(QJsonDocument::fromJson(payload.toUtf8())); + + if (jsonDoc.isNull()) + { + qCDebug(chatterinoBttv) << "Failed to parse live update JSON"; + return; + } + auto json = jsonDoc.object(); + + auto eventType = json["name"].toString(); + auto eventData = json["data"].toObject(); + + if (eventType == "emote_create") + { + auto message = BttvLiveUpdateEmoteUpdateAddMessage(eventData); + + if (!message.validate()) + { + qCDebug(chatterinoBttv) << "Invalid add message" << json; + return; + } + + this->signals_.emoteAdded.invoke(message); + } + else if (eventType == "emote_update") + { + auto message = BttvLiveUpdateEmoteUpdateAddMessage(eventData); + + if (!message.validate()) + { + qCDebug(chatterinoBttv) << "Invalid update message" << json; + return; + } + + this->signals_.emoteUpdated.invoke(message); + } + else if (eventType == "emote_delete") + { + auto message = BttvLiveUpdateEmoteRemoveMessage(eventData); + + if (!message.validate()) + { + qCDebug(chatterinoBttv) << "Invalid deletion message" << json; + return; + } + + this->signals_.emoteRemoved.invoke(message); + } + else if (eventType == "lookup_user") + { + // ignored + } + else + { + qCDebug(chatterinoBttv) << "Unhandled event:" << json; + } +} + +} // namespace chatterino diff --git a/src/providers/bttv/BttvLiveUpdates.hpp b/src/providers/bttv/BttvLiveUpdates.hpp new file mode 100644 index 000000000..9453e30e6 --- /dev/null +++ b/src/providers/bttv/BttvLiveUpdates.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include "providers/bttv/liveupdates/BttvLiveUpdateMessages.hpp" +#include "providers/bttv/liveupdates/BttvLiveUpdateSubscription.hpp" +#include "providers/liveupdates/BasicPubSubManager.hpp" +#include "util/QStringHash.hpp" + +#include + +#include + +namespace chatterino { + +class BttvLiveUpdates : public BasicPubSubManager +{ + template + using Signal = + pajlada::Signals::Signal; // type-id is vector> + +public: + BttvLiveUpdates(QString host); + ~BttvLiveUpdates() override; + + struct { + Signal emoteAdded; + Signal emoteUpdated; + Signal emoteRemoved; + } signals_; // NOLINT(readability-identifier-naming) + + /** + * Joins a Twitch channel by its id (without any prefix like 'twitch:') + * if it's not already joined. + * + * @param channelID the Twitch channel-id of the broadcaster. + * @param userName the Twitch username of the current user. + */ + void joinChannel(const QString &channelID, const QString &userName); + + /** + * Parts a twitch channel by its id (without any prefix like 'twitch:') + * if it's joined. + * + * @param id the Twitch channel-id of the broadcaster. + */ + void partChannel(const QString &id); + +protected: + void onMessage( + websocketpp::connection_hdl hdl, + BasicPubSubManager::WebsocketMessagePtr msg) + override; + +private: + // Contains all joined Twitch channel-ids + std::unordered_set joinedChannels_; +}; + +} // namespace chatterino diff --git a/src/providers/bttv/LoadBttvChannelEmote.cpp b/src/providers/bttv/LoadBttvChannelEmote.cpp deleted file mode 100644 index f5116cb02..000000000 --- a/src/providers/bttv/LoadBttvChannelEmote.cpp +++ /dev/null @@ -1,14 +0,0 @@ -#include "LoadBttvChannelEmote.hpp" - -#include -#include -#include -#include -#include "common/Common.hpp" -#include "common/NetworkRequest.hpp" -#include "common/UniqueAccess.hpp" -#include "messages/Emote.hpp" - -namespace chatterino { - -} // namespace chatterino diff --git a/src/providers/bttv/LoadBttvChannelEmote.hpp b/src/providers/bttv/LoadBttvChannelEmote.hpp deleted file mode 100644 index d0fa5cc32..000000000 --- a/src/providers/bttv/LoadBttvChannelEmote.hpp +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once - -#include - -class QString; - -namespace chatterino { - -} // namespace chatterino diff --git a/src/providers/bttv/liveupdates/BttvLiveUpdateMessages.cpp b/src/providers/bttv/liveupdates/BttvLiveUpdateMessages.cpp new file mode 100644 index 000000000..a0a5405eb --- /dev/null +++ b/src/providers/bttv/liveupdates/BttvLiveUpdateMessages.cpp @@ -0,0 +1,52 @@ +#include "providers/bttv/liveupdates/BttvLiveUpdateMessages.hpp" + +namespace { + +bool tryParseChannelId(QString &channelId) +{ + if (!channelId.startsWith("twitch:")) + { + return false; + } + + channelId.remove(0, 7); // "twitch:" + return true; +} + +} // namespace + +namespace chatterino { + +BttvLiveUpdateEmoteUpdateAddMessage::BttvLiveUpdateEmoteUpdateAddMessage( + const QJsonObject &json) + : channelID(json["channel"].toString()) + , jsonEmote(json["emote"].toObject()) + , emoteName(this->jsonEmote["code"].toString()) + , emoteID(this->jsonEmote["id"].toString()) + , badChannelID_(!tryParseChannelId(this->channelID)) +{ +} + +bool BttvLiveUpdateEmoteUpdateAddMessage::validate() const +{ + // We don't need to check for jsonEmote["code"]/["id"], + // because these are this->emoteID and this->emoteName. + return !this->badChannelID_ && !this->channelID.isEmpty() && + !this->emoteID.isEmpty() && !this->emoteName.isEmpty(); +} + +BttvLiveUpdateEmoteRemoveMessage::BttvLiveUpdateEmoteRemoveMessage( + const QJsonObject &json) + : channelID(json["channel"].toString()) + , emoteID(json["emoteId"].toString()) + , badChannelID_(!tryParseChannelId(this->channelID)) +{ +} + +bool BttvLiveUpdateEmoteRemoveMessage::validate() const +{ + return !this->badChannelID_ && !this->emoteID.isEmpty() && + !this->channelID.isEmpty(); +} + +} // namespace chatterino diff --git a/src/providers/bttv/liveupdates/BttvLiveUpdateMessages.hpp b/src/providers/bttv/liveupdates/BttvLiveUpdateMessages.hpp new file mode 100644 index 000000000..afa25ff71 --- /dev/null +++ b/src/providers/bttv/liveupdates/BttvLiveUpdateMessages.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include + +namespace chatterino { + +struct BttvLiveUpdateEmoteUpdateAddMessage { + BttvLiveUpdateEmoteUpdateAddMessage(const QJsonObject &json); + + QString channelID; + + QJsonObject jsonEmote; + QString emoteName; + QString emoteID; + + bool validate() const; + +private: + // true if the channel id is malformed + // (e.g. doesn't start with "twitch:") + bool badChannelID_; +}; + +struct BttvLiveUpdateEmoteRemoveMessage { + BttvLiveUpdateEmoteRemoveMessage(const QJsonObject &json); + + QString channelID; + QString emoteID; + + bool validate() const; + +private: + // true if the channel id is malformed + // (e.g. doesn't start with "twitch:") + bool badChannelID_; +}; + +} // namespace chatterino diff --git a/src/providers/bttv/liveupdates/BttvLiveUpdateSubscription.cpp b/src/providers/bttv/liveupdates/BttvLiveUpdateSubscription.cpp new file mode 100644 index 000000000..12b27a349 --- /dev/null +++ b/src/providers/bttv/liveupdates/BttvLiveUpdateSubscription.cpp @@ -0,0 +1,108 @@ +#include "providers/bttv/liveupdates/BttvLiveUpdateSubscription.hpp" + +#include +#include + +namespace chatterino { + +QByteArray BttvLiveUpdateSubscription::encodeSubscribe() const +{ + return QJsonDocument(std::visit( + [](const auto &d) { + return d.encode(true); + }, + this->data)) + .toJson(); +} + +QByteArray BttvLiveUpdateSubscription::encodeUnsubscribe() const +{ + return QJsonDocument(std::visit( + [](const auto &d) { + return d.encode(false); + }, + this->data)) + .toJson(); +} + +QDebug &operator<<(QDebug &dbg, const BttvLiveUpdateSubscription &subscription) +{ + std::visit( + [&](const auto &data) { + dbg << data; + }, + subscription.data); + return dbg; +} + +QJsonObject BttvLiveUpdateSubscriptionChannel::encode(bool isSubscribe) const +{ + QJsonObject root; + if (isSubscribe) + { + root["name"] = "join_channel"; + } + else + { + root["name"] = "part_channel"; + } + + QJsonObject data; + data["name"] = QString("twitch:%1").arg(this->twitchID); + + root["data"] = data; + return root; +} + +bool BttvLiveUpdateSubscriptionChannel::operator==( + const BttvLiveUpdateSubscriptionChannel &rhs) const +{ + return this->twitchID == rhs.twitchID; +} + +bool BttvLiveUpdateSubscriptionChannel::operator!=( + const BttvLiveUpdateSubscriptionChannel &rhs) const +{ + return !(*this == rhs); +} + +QDebug &operator<<(QDebug &dbg, const BttvLiveUpdateSubscriptionChannel &data) +{ + dbg << "BttvLiveUpdateSubscriptionChannel{ twitchID:" << data.twitchID + << '}'; + return dbg; +} + +QJsonObject BttvLiveUpdateBroadcastMe::encode(bool /*isSubscribe*/) const +{ + QJsonObject root; + root["name"] = "broadcast_me"; + + QJsonObject data; + data["name"] = this->userName; + data["channel"] = QString("twitch:%1").arg(this->twitchID); + + root["data"] = data; + return root; +} + +bool BttvLiveUpdateBroadcastMe::operator==( + const BttvLiveUpdateBroadcastMe &rhs) const +{ + return this->twitchID == rhs.twitchID && this->userName == rhs.userName; +} + +bool BttvLiveUpdateBroadcastMe::operator!=( + const BttvLiveUpdateBroadcastMe &rhs) const +{ + return !(*this == rhs); +} + +QDebug &operator<<(QDebug &dbg, const BttvLiveUpdateBroadcastMe &data) +{ + dbg << "BttvLiveUpdateBroadcastMe{ twitchID:" << data.twitchID + << "userName:" << data.userName << '}'; + return dbg; +} + +} // namespace chatterino diff --git a/src/providers/bttv/liveupdates/BttvLiveUpdateSubscription.hpp b/src/providers/bttv/liveupdates/BttvLiveUpdateSubscription.hpp new file mode 100644 index 000000000..45dff37aa --- /dev/null +++ b/src/providers/bttv/liveupdates/BttvLiveUpdateSubscription.hpp @@ -0,0 +1,89 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include + +namespace chatterino { + +struct BttvLiveUpdateSubscriptionChannel { + QString twitchID; + + QJsonObject encode(bool isSubscribe) const; + bool operator==(const BttvLiveUpdateSubscriptionChannel &rhs) const; + bool operator!=(const BttvLiveUpdateSubscriptionChannel &rhs) const; + friend QDebug &operator<<(QDebug &dbg, + const BttvLiveUpdateSubscriptionChannel &data); +}; + +struct BttvLiveUpdateBroadcastMe { + QString twitchID; + QString userName; + + QJsonObject encode(bool isSubscribe) const; + bool operator==(const BttvLiveUpdateBroadcastMe &rhs) const; + bool operator!=(const BttvLiveUpdateBroadcastMe &rhs) const; + friend QDebug &operator<<(QDebug &dbg, + const BttvLiveUpdateBroadcastMe &data); +}; + +using BttvLiveUpdateSubscriptionData = + std::variant; + +struct BttvLiveUpdateSubscription { + BttvLiveUpdateSubscriptionData data; + + QByteArray encodeSubscribe() const; + QByteArray encodeUnsubscribe() const; + + bool operator==(const BttvLiveUpdateSubscription &rhs) const + { + return this->data == rhs.data; + } + bool operator!=(const BttvLiveUpdateSubscription &rhs) const + { + return !(*this == rhs); + } + + friend QDebug &operator<<(QDebug &dbg, + const BttvLiveUpdateSubscription &subscription); +}; + +} // namespace chatterino + +namespace std { + +template <> +struct hash { + size_t operator()( + const chatterino::BttvLiveUpdateSubscriptionChannel &data) const + { + return qHash(data.twitchID); + } +}; + +template <> +struct hash { + size_t operator()(const chatterino::BttvLiveUpdateBroadcastMe &data) const + { + size_t seed = 0; + boost::hash_combine(seed, qHash(data.twitchID)); + boost::hash_combine(seed, qHash(data.userName)); + return seed; + } +}; + +template <> +struct hash { + size_t operator()(const chatterino::BttvLiveUpdateSubscription &sub) const + { + return std::hash{}( + sub.data); + } +}; + +} // namespace std diff --git a/src/providers/chatterino/ChatterinoBadges.cpp b/src/providers/chatterino/ChatterinoBadges.cpp index 446dfb10a..a22d5b994 100644 --- a/src/providers/chatterino/ChatterinoBadges.cpp +++ b/src/providers/chatterino/ChatterinoBadges.cpp @@ -1,25 +1,23 @@ -#include "ChatterinoBadges.hpp" +#include "providers/chatterino/ChatterinoBadges.hpp" + +#include "common/network/NetworkRequest.hpp" +#include "common/network/NetworkResult.hpp" +#include "messages/Emote.hpp" +#include "messages/Image.hpp" #include #include #include -#include #include -#include "common/NetworkRequest.hpp" -#include "common/Outcome.hpp" -#include "messages/Emote.hpp" namespace chatterino { -void ChatterinoBadges::initialize(Settings &settings, Paths &paths) + +ChatterinoBadges::ChatterinoBadges() { this->loadChatterinoBadges(); } -ChatterinoBadges::ChatterinoBadges() -{ -} - -boost::optional ChatterinoBadges::getBadge(const UserId &id) +std::optional ChatterinoBadges::getBadge(const UserId &id) { std::shared_lock lock(this->mutex_); @@ -28,7 +26,7 @@ boost::optional ChatterinoBadges::getBadge(const UserId &id) { return emotes[it->second]; } - return boost::none; + return std::nullopt; } void ChatterinoBadges::loadChatterinoBadges() @@ -37,21 +35,36 @@ void ChatterinoBadges::loadChatterinoBadges() NetworkRequest(url) .concurrent() - .onSuccess([this](auto result) -> Outcome { + .onSuccess([this](auto result) { auto jsonRoot = result.parseJson(); std::unique_lock lock(this->mutex_); int index = 0; - for (const auto &jsonBadge_ : jsonRoot.value("badges").toArray()) + for (const auto &jsonBadgeValue : + jsonRoot.value("badges").toArray()) { - auto jsonBadge = jsonBadge_.toObject(); + auto jsonBadge = jsonBadgeValue.toObject(); + // The sizes for the images are only an estimation, there might + // be badges with different sizes. + constexpr QSize baseSize(18, 18); auto emote = Emote{ - EmoteName{}, - ImageSet{Url{jsonBadge.value("image1").toString()}, - Url{jsonBadge.value("image2").toString()}, - Url{jsonBadge.value("image3").toString()}}, - Tooltip{jsonBadge.value("tooltip").toString()}, Url{}}; + .name = EmoteName{}, + .images = + ImageSet{ + Image::fromUrl( + Url{jsonBadge.value("image1").toString()}, 1.0, + baseSize), + Image::fromUrl( + Url{jsonBadge.value("image2").toString()}, 0.5, + baseSize * 2), + Image::fromUrl( + Url{jsonBadge.value("image3").toString()}, 0.25, + baseSize * 4), + }, + .tooltip = Tooltip{jsonBadge.value("tooltip").toString()}, + .homePage = Url{}, + }; emotes.push_back( std::make_shared(std::move(emote))); @@ -62,8 +75,6 @@ void ChatterinoBadges::loadChatterinoBadges() } ++index; } - - return Success; }) .execute(); } diff --git a/src/providers/chatterino/ChatterinoBadges.hpp b/src/providers/chatterino/ChatterinoBadges.hpp index ec79c056c..b4e26e4f5 100644 --- a/src/providers/chatterino/ChatterinoBadges.hpp +++ b/src/providers/chatterino/ChatterinoBadges.hpp @@ -1,33 +1,60 @@ #pragma once -#include -#include +#include "common/Aliases.hpp" + #include +#include #include #include #include -#include "common/Aliases.hpp" -#include "util/QStringHash.hpp" namespace chatterino { struct Emote; using EmotePtr = std::shared_ptr; -class ChatterinoBadges : public Singleton +class IChatterinoBadges { public: - virtual void initialize(Settings &settings, Paths &paths) override; + IChatterinoBadges() = default; + virtual ~IChatterinoBadges() = default; + + IChatterinoBadges(const IChatterinoBadges &) = delete; + IChatterinoBadges(IChatterinoBadges &&) = delete; + IChatterinoBadges &operator=(const IChatterinoBadges &) = delete; + IChatterinoBadges &operator=(IChatterinoBadges &&) = delete; + + virtual std::optional getBadge(const UserId &id) = 0; +}; + +class ChatterinoBadges : public IChatterinoBadges +{ +public: + /** + * Makes a network request to load Chatterino user badges + */ ChatterinoBadges(); - boost::optional getBadge(const UserId &id); + /** + * Returns the Chatterino badge for the given user + */ + std::optional getBadge(const UserId &id) override; private: void loadChatterinoBadges(); std::shared_mutex mutex_; + /** + * Maps Twitch user IDs to their badge index + * Guarded by mutex_ + */ std::unordered_map badgeMap; + + /** + * Keeps a list of badges. + * Indexes in here are referred to by badgeMap + */ std::vector emotes; }; diff --git a/src/providers/colors/ColorProvider.cpp b/src/providers/colors/ColorProvider.cpp index 350db3536..017dd0d83 100644 --- a/src/providers/colors/ColorProvider.cpp +++ b/src/providers/colors/ColorProvider.cpp @@ -1,6 +1,10 @@ #include "providers/colors/ColorProvider.hpp" -#include "singletons/Theme.hpp" +#include "common/QLogging.hpp" +#include "controllers/highlights/HighlightPhrase.hpp" +#include "singletons/Settings.hpp" + +#include namespace chatterino { @@ -11,24 +15,16 @@ const ColorProvider &ColorProvider::instance() } ColorProvider::ColorProvider() - : typeColorMap_() - , defaultColors_() { this->initTypeColorMap(); this->initDefaultColors(); } -const std::shared_ptr ColorProvider::color(ColorType type) const +std::shared_ptr ColorProvider::color(ColorType type) const { return this->typeColorMap_.at(type); } -void ColorProvider::updateColor(ColorType type, QColor color) -{ - auto colorPtr = this->typeColorMap_.at(type); - *colorPtr = std::move(color); -} - QSet ColorProvider::recentColors() const { QSet retVal; @@ -37,12 +33,12 @@ QSet ColorProvider::recentColors() const * Currently, only colors used in highlight phrases are considered. This * may change at any point in the future. */ - for (auto phrase : getSettings()->highlightedMessages) + for (const auto &phrase : getSettings()->highlightedMessages) { retVal.insert(*phrase.getColor()); } - for (auto userHl : getSettings()->highlightedUsers) + for (const auto &userHl : getSettings()->highlightedUsers) { retVal.insert(*userHl.getColor()); } @@ -64,75 +60,77 @@ void ColorProvider::initTypeColorMap() { // Read settings for custom highlight colors and save them in map. // If no custom values can be found, set up default values instead. + // Set up a signal to the respective setting for updating the color when it's changed + auto initColor = [this](ColorType colorType, QStringSetting &setting, + QColor fallbackColor) { + const auto &colorString = setting.getValue(); + QColor color(colorString); + if (color.isValid()) + { + this->typeColorMap_.insert({ + colorType, + std::make_shared(color), + }); + } + else + { + this->typeColorMap_.insert({ + colorType, + std::make_shared(fallbackColor), + }); + } - QString customColor = getSettings()->selfHighlightColor; - if (QColor(customColor).isValid()) - { - this->typeColorMap_.insert( - {ColorType::SelfHighlight, std::make_shared(customColor)}); - } - else - { - this->typeColorMap_.insert( - {ColorType::SelfHighlight, - std::make_shared( - HighlightPhrase::FALLBACK_HIGHLIGHT_COLOR)}); - } + setting.connect( + [this, colorType](const auto &colorString) { + QColor color(colorString); + if (color.isValid()) + { + // Update color based on the update from the setting + *this->typeColorMap_.at(colorType) = color; + } + else + { + qCWarning(chatterinoCommon) + << "Updated" + << static_cast>( + colorType) + << "to invalid color" << colorString; + } + }, + false); + }; - customColor = getSettings()->subHighlightColor; - if (QColor(customColor).isValid()) - { - this->typeColorMap_.insert( - {ColorType::Subscription, std::make_shared(customColor)}); - } - else - { - this->typeColorMap_.insert( - {ColorType::Subscription, - std::make_shared(HighlightPhrase::FALLBACK_SUB_COLOR)}); - } + initColor(ColorType::SelfHighlight, getSettings()->selfHighlightColor, + HighlightPhrase::FALLBACK_HIGHLIGHT_COLOR); - customColor = getSettings()->whisperHighlightColor; - if (QColor(customColor).isValid()) - { - this->typeColorMap_.insert( - {ColorType::Whisper, std::make_shared(customColor)}); - } - else - { - this->typeColorMap_.insert( - {ColorType::Whisper, - std::make_shared( - HighlightPhrase::FALLBACK_HIGHLIGHT_COLOR)}); - } + initColor(ColorType::SelfMessageHighlight, + getSettings()->selfMessageHighlightColor, + HighlightPhrase::FALLBACK_SELF_MESSAGE_HIGHLIGHT_COLOR); - customColor = getSettings()->redeemedHighlightColor; - if (QColor(customColor).isValid()) - { - this->typeColorMap_.insert({ColorType::RedeemedHighlight, - std::make_shared(customColor)}); - } - else - { - this->typeColorMap_.insert( - {ColorType::RedeemedHighlight, - std::make_shared( - HighlightPhrase::FALLBACK_REDEEMED_HIGHLIGHT_COLOR)}); - } + initColor(ColorType::Subscription, getSettings()->subHighlightColor, + HighlightPhrase::FALLBACK_SUB_COLOR); - customColor = getSettings()->firstMessageHighlightColor; - if (QColor(customColor).isValid()) - { - this->typeColorMap_.insert({ColorType::FirstMessageHighlight, - std::make_shared(customColor)}); - } - else - { - this->typeColorMap_.insert( - {ColorType::FirstMessageHighlight, - std::make_shared( - HighlightPhrase::FALLBACK_FIRST_MESSAGE_HIGHLIGHT_COLOR)}); - } + initColor(ColorType::Whisper, getSettings()->whisperHighlightColor, + HighlightPhrase::FALLBACK_HIGHLIGHT_COLOR); + + initColor(ColorType::RedeemedHighlight, + getSettings()->redeemedHighlightColor, + HighlightPhrase::FALLBACK_REDEEMED_HIGHLIGHT_COLOR); + + initColor(ColorType::FirstMessageHighlight, + getSettings()->firstMessageHighlightColor, + HighlightPhrase::FALLBACK_FIRST_MESSAGE_HIGHLIGHT_COLOR); + + initColor(ColorType::ElevatedMessageHighlight, + getSettings()->elevatedMessageHighlightColor, + HighlightPhrase::FALLBACK_ELEVATED_MESSAGE_HIGHLIGHT_COLOR); + + initColor(ColorType::ThreadMessageHighlight, + getSettings()->threadHighlightColor, + HighlightPhrase::FALLBACK_THREAD_HIGHLIGHT_COLOR); + + initColor(ColorType::AutomodHighlight, getSettings()->automodHighlightColor, + HighlightPhrase::FALLBACK_AUTOMOD_HIGHLIGHT_COLOR); } void ColorProvider::initDefaultColors() diff --git a/src/providers/colors/ColorProvider.hpp b/src/providers/colors/ColorProvider.hpp index 4540caaf3..c35da0543 100644 --- a/src/providers/colors/ColorProvider.hpp +++ b/src/providers/colors/ColorProvider.hpp @@ -1,9 +1,10 @@ #pragma once +#include + #include #include - -#include +#include namespace chatterino { @@ -13,6 +14,11 @@ enum class ColorType { Whisper, RedeemedHighlight, FirstMessageHighlight, + ElevatedMessageHighlight, + ThreadMessageHighlight, + // Used in automatic highlights of your own messages + SelfMessageHighlight, + AutomodHighlight, }; class ColorProvider @@ -31,9 +37,7 @@ public: * of already parsed predefined (self highlights, subscriptions, * and whispers) highlights. */ - const std::shared_ptr color(ColorType type) const; - - void updateColor(ColorType type, QColor color); + std::shared_ptr color(ColorType type) const; /** * @brief Return a set of recently used colors used anywhere in Chatterino. diff --git a/src/providers/emoji/Emojis.cpp b/src/providers/emoji/Emojis.cpp index 4979c2cfb..eefdd827e 100644 --- a/src/providers/emoji/Emojis.cpp +++ b/src/providers/emoji/Emojis.cpp @@ -1,133 +1,164 @@ #include "providers/emoji/Emojis.hpp" -#include "Application.hpp" +#include "common/QLogging.hpp" #include "messages/Emote.hpp" +#include "messages/Image.hpp" #include "singletons/Settings.hpp" +#include "util/RapidjsonHelpers.hpp" +#include +#include #include #include #include -#include -#include -#include -#include "common/QLogging.hpp" -namespace chatterino { +#include +#include + namespace { - auto toneNames = std::map{ - {"1F3FB", "tone1"}, {"1F3FC", "tone2"}, {"1F3FD", "tone3"}, - {"1F3FE", "tone4"}, {"1F3FF", "tone5"}, - }; +using namespace chatterino; - void parseEmoji(const std::shared_ptr &emojiData, - const rapidjson::Value &unparsedEmoji, - QString shortCode = QString()) +const std::map TONE_NAMES{ + {"1F3FB", "tone1"}, {"1F3FC", "tone2"}, {"1F3FD", "tone3"}, + {"1F3FE", "tone4"}, {"1F3FF", "tone5"}, +}; + +void parseEmoji(const std::shared_ptr &emojiData, + const rapidjson::Value &unparsedEmoji, + const QString &shortCode = {}) +{ + std::vector unicodeBytes{}; + + struct { + bool apple; + bool google; + bool twitter; + bool facebook; + } capabilities{}; + + if (!shortCode.isEmpty()) { - std::array unicodeBytes; + emojiData->shortCodes.push_back(shortCode); + } + else + { + // Load short codes from the suggested short_names + const auto &shortNames = unparsedEmoji["short_names"]; + for (const auto &shortName : shortNames.GetArray()) + { + emojiData->shortCodes.emplace_back(shortName.GetString()); + } + } - struct { - bool apple; - bool google; - bool twitter; - bool facebook; - } capabilities; + rj::getSafe(unparsedEmoji, "non_qualified", emojiData->nonQualifiedCode); + rj::getSafe(unparsedEmoji, "unified", emojiData->unifiedCode); + assert(!emojiData->unifiedCode.isEmpty()); - if (!shortCode.isEmpty()) - { - emojiData->shortCodes.push_back(shortCode); - } - else - { - const auto &shortCodes = unparsedEmoji["short_names"]; - for (const auto &_shortCode : shortCodes.GetArray()) - { - emojiData->shortCodes.emplace_back(_shortCode.GetString()); - } - } + rj::getSafe(unparsedEmoji, "has_img_apple", capabilities.apple); + rj::getSafe(unparsedEmoji, "has_img_google", capabilities.google); + rj::getSafe(unparsedEmoji, "has_img_twitter", capabilities.twitter); + rj::getSafe(unparsedEmoji, "has_img_facebook", capabilities.facebook); - rj::getSafe(unparsedEmoji, "non_qualified", - emojiData->nonQualifiedCode); - rj::getSafe(unparsedEmoji, "unified", emojiData->unifiedCode); + if (capabilities.apple) + { + emojiData->capabilities.insert("Apple"); + } + if (capabilities.google) + { + emojiData->capabilities.insert("Google"); + } + if (capabilities.twitter) + { + emojiData->capabilities.insert("Twitter"); + } + if (capabilities.facebook) + { + emojiData->capabilities.insert("Facebook"); + } - rj::getSafe(unparsedEmoji, "has_img_apple", capabilities.apple); - rj::getSafe(unparsedEmoji, "has_img_google", capabilities.google); - rj::getSafe(unparsedEmoji, "has_img_twitter", capabilities.twitter); - rj::getSafe(unparsedEmoji, "has_img_facebook", capabilities.facebook); + QStringList unicodeCharacters = emojiData->unifiedCode.toLower().split('-'); - if (capabilities.apple) - { - emojiData->capabilities.insert("Apple"); - } - if (capabilities.google) - { - emojiData->capabilities.insert("Google"); - } - if (capabilities.twitter) - { - emojiData->capabilities.insert("Twitter"); - } - if (capabilities.facebook) - { - emojiData->capabilities.insert("Facebook"); - } - - QStringList unicodeCharacters; - if (!emojiData->nonQualifiedCode.isEmpty()) - { - unicodeCharacters = - emojiData->nonQualifiedCode.toLower().split('-'); - } - else - { - unicodeCharacters = emojiData->unifiedCode.toLower().split('-'); - } - if (unicodeCharacters.length() < 1) + for (const QString &unicodeCharacter : unicodeCharacters) + { + bool ok{false}; + unicodeBytes.push_back(unicodeCharacter.toUInt(&ok, 16)); + if (!ok) { + qCWarning(chatterinoEmoji) + << "Failed to parse emoji" << emojiData->shortCodes; return; } - - int numUnicodeBytes = 0; - - for (const QString &unicodeCharacter : unicodeCharacters) - { - unicodeBytes.at(numUnicodeBytes++) = - QString(unicodeCharacter).toUInt(nullptr, 16); - } - - emojiData->value = - QString::fromUcs4(unicodeBytes.data(), numUnicodeBytes); } - // getToneNames takes a tones and returns their names in the same order - // The format of the tones is: "1F3FB-1F3FB" or "1F3FB" - // The output of the tone names is: "tone1-tone1" or "tone1" - QString getToneNames(const QString &tones) + // We can safely do a narrowing static cast since unicodeBytes will never be a large number + emojiData->value = + QString::fromUcs4(unicodeBytes.data(), + static_cast(unicodeBytes.size())); + + if (!emojiData->nonQualifiedCode.isEmpty()) { - auto toneParts = tones.split('-'); - QStringList toneNameResults; - for (const auto &tonePart : toneParts) + QStringList nonQualifiedCharacters = + emojiData->nonQualifiedCode.toLower().split('-'); + std::vector nonQualifiedBytes{}; + for (const QString &unicodeCharacter : nonQualifiedCharacters) { - auto toneNameIt = toneNames.find(tonePart); - if (toneNameIt == toneNames.end()) + bool ok{false}; + nonQualifiedBytes.push_back( + QString(unicodeCharacter).toUInt(&ok, 16)); + if (!ok) { - qDebug() << "Tone with key" << tonePart - << "does not exist in tone names map"; - continue; + qCWarning(chatterinoEmoji) + << "Failed to parse emoji nonQualified" + << emojiData->shortCodes; + return; } - - toneNameResults.append(toneNameIt->second); } - assert(!toneNameResults.isEmpty()); - - return toneNameResults.join('-'); + // We can safely do a narrowing static cast since unicodeBytes will never be a large number + emojiData->nonQualified = QString::fromUcs4( + nonQualifiedBytes.data(), + static_cast(nonQualifiedBytes.size())); } +} + +// getToneNames takes a tones and returns their names in the same order +// The format of the tones is: "1F3FB-1F3FB" or "1F3FB" +// The output of the tone names is: "tone1-tone1" or "tone1" +QString getToneNames(const QString &tones) +{ + auto toneParts = tones.split('-'); + QStringList toneNameResults; + for (const auto &tonePart : toneParts) + { + auto toneNameIt = TONE_NAMES.find(tonePart); + if (toneNameIt == TONE_NAMES.end()) + { + qDebug() << "Tone with key" << tonePart + << "does not exist in tone names map"; + continue; + } + + toneNameResults.append(toneNameIt->second); + } + + assert(!toneNameResults.isEmpty()); + + return toneNameResults.join('-'); +} } // namespace +namespace chatterino { + void Emojis::load() { + if (this->loaded_) + { + return; + } + this->loaded_ = true; + this->loadEmojis(); this->sortEmojis(); @@ -137,7 +168,7 @@ void Emojis::load() void Emojis::loadEmojis() { - // Current version: https://github.com/iamcal/emoji-data/blob/v14.0.0/emoji.json (Emoji version 14.0 (2022)) + // Current version: https://github.com/iamcal/emoji-data/blob/v15.1.1/emoji.json (Emoji version 15.1 (2023)) QFile file(":/emoji.json"); file.open(QFile::ReadOnly); QTextStream s1(&file); @@ -166,7 +197,7 @@ void Emojis::loadEmojis() this->emojiFirstByte_[emojiData->value.at(0)].append(emojiData); - this->emojis.insert(emojiData->unifiedCode, emojiData); + this->emojis.push_back(emojiData); if (unparsedEmoji.HasMember("skin_variations")) { @@ -188,8 +219,7 @@ void Emojis::loadEmojis() this->emojiFirstByte_[variationEmojiData->value.at(0)].append( variationEmojiData); - this->emojis.insert(variationEmojiData->unifiedCode, - variationEmojiData); + this->emojis.push_back(variationEmojiData); } } } @@ -213,13 +243,9 @@ void Emojis::sortEmojis() void Emojis::loadEmojiSet() { -#ifndef CHATTERINO_TEST - getSettings()->emojiSet.connect([=](const auto &emojiSet) { -#else - const QString emojiSet = "twitter"; -#endif - this->emojis.each([=](const auto &name, - std::shared_ptr &emoji) { + getSettings()->emojiSet.connect([this](const auto &emojiSet) { + for (const auto &emoji : this->emojis) + { QString emojiSetToUse = emojiSet; // clang-format off static std::map emojiSets = { @@ -244,14 +270,15 @@ void Emojis::loadEmojiSet() }; // clang-format on - if (emoji->capabilities.count(emojiSetToUse) == 0) + // As of emoji-data v15.1.1, google is the only source missing no images. + if (!emoji->capabilities.contains(emojiSetToUse)) { - emojiSetToUse = "Twitter"; + emojiSetToUse = "Google"; } QString code = emoji->unifiedCode.toLower(); QString urlPrefix = - "https://pajbot.com/static/emoji-v2/img/twitter/64/"; + "https://pajbot.com/static/emoji-v2/img/google/64/"; auto it = emojiSets.find(emojiSetToUse); if (it != emojiSets.end()) { @@ -259,21 +286,20 @@ void Emojis::loadEmojiSet() } QString url = urlPrefix + code + ".png"; emoji->emote = std::make_shared(Emote{ - EmoteName{emoji->value}, ImageSet{Image::fromUrl({url}, 0.35)}, + EmoteName{emoji->value}, + ImageSet{Image::fromUrl({url}, 0.35, {64, 64})}, Tooltip{":" + emoji->shortCodes[0] + ":
Emoji"}, Url{}}); - }); -#ifndef CHATTERINO_TEST + } }); -#endif } std::vector> Emojis::parse( - const QString &text) + const QString &text) const { auto result = std::vector>(); - int lastParsedEmojiEndIndex = 0; + QString::size_type lastParsedEmojiEndIndex = 0; - for (auto i = 0; i < text.length(); ++i) + for (qsizetype i = 0; i < text.length(); ++i) { const QChar character = text.at(i); @@ -291,39 +317,47 @@ std::vector> Emojis::parse( const auto &possibleEmojis = it.value(); - int remainingCharacters = text.length() - i - 1; + auto remainingCharacters = text.length() - i - 1; std::shared_ptr matchedEmoji; - int matchedEmojiLength = 0; + QString::size_type matchedEmojiLength = 0; for (const std::shared_ptr &emoji : possibleEmojis) { - int emojiExtraCharacters = emoji->value.length() - 1; - if (emojiExtraCharacters > remainingCharacters) + auto emojiNonQualifiedExtraCharacters = + emoji->nonQualified.length() - 1; + auto emojiExtraCharacters = emoji->value.length() - 1; + if (remainingCharacters >= emojiExtraCharacters) { - // It cannot be this emoji, there's not enough space for it - continue; - } + // look in emoji->value + bool match = QStringView{emoji->value}.mid(1) == + QStringView{text}.mid(i + 1, emojiExtraCharacters); - bool match = true; - - for (int j = 1; j < emoji->value.length(); ++j) - { - if (text.at(i + j) != emoji->value.at(j)) + if (match) { - match = false; + matchedEmoji = emoji; + matchedEmojiLength = emoji->value.length(); break; } } - - if (match) + if (!emoji->nonQualified.isNull() && + remainingCharacters >= emojiNonQualifiedExtraCharacters) { - matchedEmoji = emoji; - matchedEmojiLength = emoji->value.length(); + // This checking here relies on the fact that the nonQualified string + // always starts with the same byte as value (the unified string) + bool match = QStringView{emoji->nonQualified}.mid(1) == + QStringView{text}.mid( + i + 1, emojiNonQualifiedExtraCharacters); - break; + if (match) + { + matchedEmoji = emoji; + matchedEmojiLength = emoji->nonQualified.length(); + + break; + } } } @@ -332,10 +366,10 @@ std::vector> Emojis::parse( continue; } - int currentParsedEmojiFirstIndex = i; - int currentParsedEmojiEndIndex = i + (matchedEmojiLength); + auto currentParsedEmojiFirstIndex = i; + auto currentParsedEmojiEndIndex = i + (matchedEmojiLength); - int charactersFromLastParsedEmoji = + auto charactersFromLastParsedEmoji = currentParsedEmojiFirstIndex - lastParsedEmojiEndIndex; if (charactersFromLastParsedEmoji > 0) @@ -362,12 +396,12 @@ std::vector> Emojis::parse( return result; } -QString Emojis::replaceShortCodes(const QString &text) +QString Emojis::replaceShortCodes(const QString &text) const { QString ret(text); auto it = this->findShortCodesRegex_.globalMatch(text); - int32_t offset = 0; + qsizetype offset = 0; while (it.hasNext()) { @@ -385,7 +419,7 @@ QString Emojis::replaceShortCodes(const QString &text) continue; } - auto emojiData = emojiIt.value(); + const auto &emojiData = emojiIt.value(); ret.replace(offset + match.capturedStart(), match.capturedLength(), emojiData->value); @@ -396,4 +430,14 @@ QString Emojis::replaceShortCodes(const QString &text) return ret; } +const std::vector &Emojis::getEmojis() const +{ + return this->emojis; +} + +const std::vector &Emojis::getShortCodes() const +{ + return this->shortCodes; +} + } // namespace chatterino diff --git a/src/providers/emoji/Emojis.hpp b/src/providers/emoji/Emojis.hpp index e010c4686..d6d783fc5 100644 --- a/src/providers/emoji/Emojis.hpp +++ b/src/providers/emoji/Emojis.hpp @@ -1,11 +1,11 @@ #pragma once -#include "util/ConcurrentMap.hpp" - +#include #include #include -#include -#include +#include + +#include #include #include @@ -19,6 +19,9 @@ struct EmojiData { // :male:) QString value; + // actual byte-representation of the non qualified emoji + QString nonQualified; + // i.e. 204e-50a2 QString unifiedCode; QString nonQualifiedCode; @@ -33,24 +36,41 @@ struct EmojiData { EmotePtr emote; }; -using EmojiMap = ConcurrentMap>; +using EmojiPtr = std::shared_ptr; -class Emojis +class IEmojis +{ +public: + virtual ~IEmojis() = default; + + virtual std::vector> parse( + const QString &text) const = 0; + virtual const std::vector &getEmojis() const = 0; + virtual const std::vector &getShortCodes() const = 0; + virtual QString replaceShortCodes(const QString &text) const = 0; +}; + +class Emojis : public IEmojis { public: void initialize(); void load(); - std::vector> parse(const QString &text); + std::vector> parse( + const QString &text) const override; - EmojiMap emojis; std::vector shortCodes; - QString replaceShortCodes(const QString &text); + QString replaceShortCodes(const QString &text) const override; + + const std::vector &getEmojis() const override; + const std::vector &getShortCodes() const override; private: void loadEmojis(); void sortEmojis(); void loadEmojiSet(); + std::vector emojis; + /// Emojis QRegularExpression findShortCodesRegex_{":([-+\\w]+):"}; @@ -60,6 +80,8 @@ private: // Maps the first character of the emoji unicode string to a vector of // possible emojis QMap>> emojiFirstByte_; + + bool loaded_ = false; }; } // namespace chatterino diff --git a/src/providers/ffz/FfzBadges.cpp b/src/providers/ffz/FfzBadges.cpp index bff84cac2..fb4358ab9 100644 --- a/src/providers/ffz/FfzBadges.cpp +++ b/src/providers/ffz/FfzBadges.cpp @@ -1,23 +1,19 @@ -#include "FfzBadges.hpp" +#include "providers/ffz/FfzBadges.hpp" + +#include "common/network/NetworkRequest.hpp" +#include "common/network/NetworkResult.hpp" +#include "messages/Emote.hpp" +#include "messages/Image.hpp" +#include "providers/ffz/FfzUtil.hpp" #include #include #include #include #include -#include -#include -#include "common/NetworkRequest.hpp" -#include "common/Outcome.hpp" -#include "messages/Emote.hpp" namespace chatterino { -void FfzBadges::initialize(Settings &settings, Paths &paths) -{ - this->load(); -} - std::vector FfzBadges::getUserBadges(const UserId &id) { std::vector badges; @@ -39,15 +35,16 @@ std::vector FfzBadges::getUserBadges(const UserId &id) return badges; } -boost::optional FfzBadges::getBadge(const int badgeID) +std::optional FfzBadges::getBadge(const int badgeID) const { + this->tgBadges.guard(); auto it = this->badges.find(badgeID); if (it != this->badges.end()) { return it->second; } - return boost::none; + return std::nullopt; } void FfzBadges::load() @@ -55,22 +52,29 @@ void FfzBadges::load() static QUrl url("https://api.frankerfacez.com/v1/badges/ids"); NetworkRequest(url) - .onSuccess([this](auto result) -> Outcome { + .onSuccess([this](auto result) { std::unique_lock lock(this->mutex_); auto jsonRoot = result.parseJson(); + this->tgBadges.guard(); for (const auto &jsonBadge_ : jsonRoot.value("badges").toArray()) { auto jsonBadge = jsonBadge_.toObject(); auto jsonUrls = jsonBadge.value("urls").toObject(); + QSize baseSize(jsonBadge["width"].toInt(18), + jsonBadge["height"].toInt(18)); auto emote = Emote{ EmoteName{}, - ImageSet{ - Url{QString("https:") + jsonUrls.value("1").toString()}, - Url{QString("https:") + jsonUrls.value("2").toString()}, - Url{QString("https:") + - jsonUrls.value("4").toString()}}, + ImageSet{Image::fromUrl( + parseFfzUrl(jsonUrls.value("1").toString()), + 1.0, baseSize), + Image::fromUrl( + parseFfzUrl(jsonUrls.value("2").toString()), + 0.5, baseSize * 2), + Image::fromUrl( + parseFfzUrl(jsonUrls.value("4").toString()), + 0.25, baseSize * 4)}, Tooltip{jsonBadge.value("title").toString()}, Url{}}; Badge badge; @@ -92,17 +96,15 @@ void FfzBadges::load() auto userIDString = QString::number(user.toInt()); auto [userBadges, created] = this->userBadges.emplace( - std::make_pair>( + std::make_pair>( std::move(userIDString), {badgeID})); if (!created) { // User already had a badge assigned - userBadges->second.push_back(badgeID); + userBadges->second.emplace(badgeID); } } } - - return Success; }) .execute(); } diff --git a/src/providers/ffz/FfzBadges.hpp b/src/providers/ffz/FfzBadges.hpp index 4e08a3428..761111831 100644 --- a/src/providers/ffz/FfzBadges.hpp +++ b/src/providers/ffz/FfzBadges.hpp @@ -1,29 +1,25 @@ #pragma once -#include -#include - #include "common/Aliases.hpp" -#include "common/UniqueAccess.hpp" -#include "util/QStringHash.hpp" +#include "util/ThreadGuard.hpp" + +#include -#include #include +#include +#include #include #include #include -#include - namespace chatterino { struct Emote; using EmotePtr = std::shared_ptr; -class FfzBadges : public Singleton +class FfzBadges { public: - virtual void initialize(Settings &settings, Paths &paths) override; FfzBadges() = default; struct Badge { @@ -32,19 +28,19 @@ public: }; std::vector getUserBadges(const UserId &id); - -private: - boost::optional getBadge(int badgeID); + std::optional getBadge(int badgeID) const; void load(); +private: std::shared_mutex mutex_; // userBadges points a user ID to the list of badges they have - std::unordered_map> userBadges; + std::unordered_map> userBadges; // badges points a badge ID to the information about the badge std::unordered_map badges; + ThreadGuard tgBadges; }; } // namespace chatterino diff --git a/src/providers/ffz/FfzEmotes.cpp b/src/providers/ffz/FfzEmotes.cpp index 25cba7d15..84887186c 100644 --- a/src/providers/ffz/FfzEmotes.cpp +++ b/src/providers/ffz/FfzEmotes.cpp @@ -1,169 +1,216 @@ #include "providers/ffz/FfzEmotes.hpp" -#include - -#include "common/NetworkRequest.hpp" -#include "common/Outcome.hpp" +#include "common/network/NetworkRequest.hpp" +#include "common/network/NetworkResult.hpp" #include "common/QLogging.hpp" #include "messages/Emote.hpp" #include "messages/Image.hpp" #include "messages/MessageBuilder.hpp" +#include "providers/ffz/FfzUtil.hpp" #include "providers/twitch/TwitchChannel.hpp" #include "singletons/Settings.hpp" -namespace chatterino { namespace { - const QString CHANNEL_HAS_NO_EMOTES( - "This channel has no FrankerFaceZ channel emotes."); +using namespace chatterino; - Url getEmoteLink(const QJsonObject &urls, const QString &emoteScale) +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +const auto &LOG = chatterinoFfzemotes; + +const QString CHANNEL_HAS_NO_EMOTES( + "This channel has no FrankerFaceZ channel emotes."); + +// FFZ doesn't provide any data on the size for room badges, +// so we assume 18x18 (same as a Twitch badge) +constexpr QSize BASE_BADGE_SIZE(18, 18); + +Url getEmoteLink(const QJsonObject &urls, const QString &emoteScale) +{ + auto emote = urls[emoteScale]; + if (emote.isUndefined() || emote.isNull()) { - auto emote = urls.value(emoteScale); - if (emote.isUndefined() || emote.isNull()) + return {""}; + } + + assert(emote.isString()); + + return parseFfzUrl(emote.toString()); +} + +void fillInEmoteData(const QJsonObject &emote, const QJsonObject &urls, + const EmoteName &name, const QString &tooltip, + Emote &emoteData) +{ + auto url1x = getEmoteLink(urls, "1"); + auto url2x = getEmoteLink(urls, "2"); + auto url3x = getEmoteLink(urls, "4"); + QSize baseSize(emote["width"].toInt(28), emote["height"].toInt(28)); + + //, code, tooltip + emoteData.name = name; + emoteData.images = ImageSet{ + Image::fromUrl(url1x, 1, baseSize), + url2x.string.isEmpty() ? Image::getEmpty() + : Image::fromUrl(url2x, 0.5, baseSize * 2), + url3x.string.isEmpty() ? Image::getEmpty() + : Image::fromUrl(url3x, 0.25, baseSize * 4)}; + emoteData.tooltip = {tooltip}; +} + +EmotePtr cachedOrMake(Emote &&emote, const EmoteId &id) +{ + static std::unordered_map> cache; + static std::mutex mutex; + + return cachedOrMakeEmotePtr(std::move(emote), cache, mutex, id); +} + +void parseEmoteSetInto(const QJsonObject &emoteSet, const QString &kind, + EmoteMap &map) +{ + for (const auto emoteRef : emoteSet["emoticons"].toArray()) + { + const auto emoteJson = emoteRef.toObject(); + + // margins + auto id = EmoteId{QString::number(emoteJson["id"].toInt())}; + auto name = EmoteName{emoteJson["name"].toString()}; + auto author = + EmoteAuthor{emoteJson["owner"]["display_name"].toString()}; + auto urls = emoteJson["urls"].toObject(); + if (emoteJson["animated"].isObject()) { - return {""}; + // prefer animated images if available + urls = emoteJson["animated"].toObject(); } - assert(emote.isString()); + Emote emote; + fillInEmoteData(emoteJson, urls, name, + QString("%1
%2 FFZ Emote
By: %3") + .arg(name.string, kind, author.string), + emote); + emote.homePage = + Url{QString("https://www.frankerfacez.com/emoticon/%1-%2") + .arg(id.string) + .arg(name.string)}; - return {"https:" + emote.toString()}; + map[name] = cachedOrMake(std::move(emote), id); } - void fillInEmoteData(const QJsonObject &urls, const EmoteName &name, - const QString &tooltip, Emote &emoteData) - { - auto url1x = getEmoteLink(urls, "1"); - auto url2x = getEmoteLink(urls, "2"); - auto url3x = getEmoteLink(urls, "4"); +} - //, code, tooltip - emoteData.name = name; - emoteData.images = - ImageSet{Image::fromUrl(url1x, 1), - url2x.string.isEmpty() ? Image::getEmpty() - : Image::fromUrl(url2x, 0.5), - url3x.string.isEmpty() ? Image::getEmpty() - : Image::fromUrl(url3x, 0.25)}; - emoteData.tooltip = {tooltip}; +EmoteMap parseGlobalEmotes(const QJsonObject &jsonRoot) +{ + // Load default sets from the `default_sets` object + std::unordered_set defaultSets{}; + auto jsonDefaultSets = jsonRoot["default_sets"].toArray(); + for (auto jsonDefaultSet : jsonDefaultSets) + { + defaultSets.insert(jsonDefaultSet.toInt()); } - EmotePtr cachedOrMake(Emote &&emote, const EmoteId &id) - { - static std::unordered_map> cache; - static std::mutex mutex; - return cachedOrMakeEmotePtr(std::move(emote), cache, mutex, id); - } - std::pair parseGlobalEmotes( - const QJsonObject &jsonRoot, const EmoteMap ¤tEmotes) - { - auto jsonSets = jsonRoot.value("sets").toObject(); - auto emotes = EmoteMap(); + auto emotes = EmoteMap(); - for (auto jsonSet : jsonSets) + for (const auto emoteSetRef : jsonRoot["sets"].toObject()) + { + const auto emoteSet = emoteSetRef.toObject(); + auto emoteSetID = emoteSet["id"].toInt(); + if (!defaultSets.contains(emoteSetID)) { - auto jsonEmotes = jsonSet.toObject().value("emoticons").toArray(); - - for (auto jsonEmoteValue : jsonEmotes) - { - auto jsonEmote = jsonEmoteValue.toObject(); - - auto name = EmoteName{jsonEmote.value("name").toString()}; - auto id = - EmoteId{QString::number(jsonEmote.value("id").toInt())}; - auto urls = jsonEmote.value("urls").toObject(); - - auto emote = Emote(); - fillInEmoteData(urls, name, - name.string + "
Global FFZ Emote", emote); - emote.homePage = - Url{QString("https://www.frankerfacez.com/emoticon/%1-%2") - .arg(id.string) - .arg(name.string)}; - - emotes[name] = - cachedOrMakeEmotePtr(std::move(emote), currentEmotes); - } + qCDebug(LOG) << "Skipping global emote set" << emoteSetID + << "as it's not part of the default sets"; + continue; } - return {Success, std::move(emotes)}; + parseEmoteSetInto(emoteSet, "Global", emotes); } - boost::optional parseAuthorityBadge(const QJsonObject &badgeUrls, - const QString tooltip) + return emotes; +} + +std::optional parseAuthorityBadge(const QJsonObject &badgeUrls, + const QString &tooltip) +{ + std::optional authorityBadge; + + if (!badgeUrls.isEmpty()) { - boost::optional authorityBadge; + auto authorityBadge1x = getEmoteLink(badgeUrls, "1"); + auto authorityBadge2x = getEmoteLink(badgeUrls, "2"); + auto authorityBadge3x = getEmoteLink(badgeUrls, "4"); - if (!badgeUrls.isEmpty()) - { - auto authorityBadge1x = getEmoteLink(badgeUrls, "1"); - auto authorityBadge2x = getEmoteLink(badgeUrls, "2"); - auto authorityBadge3x = getEmoteLink(badgeUrls, "4"); + auto authorityBadgeImageSet = ImageSet{ + Image::fromUrl(authorityBadge1x, 1, BASE_BADGE_SIZE), + authorityBadge2x.string.isEmpty() + ? Image::getEmpty() + : Image::fromUrl(authorityBadge2x, 0.5, BASE_BADGE_SIZE * 2), + authorityBadge3x.string.isEmpty() + ? Image::getEmpty() + : Image::fromUrl(authorityBadge3x, 0.25, BASE_BADGE_SIZE * 4), + }; - auto authorityBadgeImageSet = ImageSet{ - Image::fromUrl(authorityBadge1x, 1), - authorityBadge2x.string.isEmpty() - ? Image::getEmpty() - : Image::fromUrl(authorityBadge2x, 0.5), - authorityBadge3x.string.isEmpty() - ? Image::getEmpty() - : Image::fromUrl(authorityBadge3x, 0.25), - }; - - authorityBadge = std::make_shared(Emote{ - {""}, - authorityBadgeImageSet, - Tooltip{tooltip}, - authorityBadge1x, - }); - } - return authorityBadge; + authorityBadge = std::make_shared(Emote{ + .name = {""}, + .images = authorityBadgeImageSet, + .tooltip = Tooltip{tooltip}, + .homePage = authorityBadge1x, + }); } + return authorityBadge; +} - EmoteMap parseChannelEmotes(const QJsonObject &jsonRoot) - { - auto jsonSets = jsonRoot.value("sets").toObject(); - auto emotes = EmoteMap(); - - for (auto jsonSet : jsonSets) - { - auto jsonEmotes = jsonSet.toObject().value("emoticons").toArray(); - - for (auto _jsonEmote : jsonEmotes) - { - auto jsonEmote = _jsonEmote.toObject(); - - // margins - auto id = - EmoteId{QString::number(jsonEmote.value("id").toInt())}; - auto name = EmoteName{jsonEmote.value("name").toString()}; - auto author = EmoteAuthor{jsonEmote.value("owner") - .toObject() - .value("display_name") - .toString()}; - auto urls = jsonEmote.value("urls").toObject(); - - Emote emote; - fillInEmoteData(urls, name, - QString("%1
Channel FFZ Emote
By: %2") - .arg(name.string) - .arg(author.string), - emote); - emote.homePage = - Url{QString("https://www.frankerfacez.com/emoticon/%1-%2") - .arg(id.string) - .arg(name.string)}; - - emotes[name] = cachedOrMake(std::move(emote), id); - } - } - - return emotes; - } } // namespace +namespace chatterino { + +using namespace ffz::detail; + +EmoteMap ffz::detail::parseChannelEmotes(const QJsonObject &jsonRoot) +{ + auto emotes = EmoteMap(); + + for (const auto emoteSetRef : jsonRoot["sets"].toObject()) + { + parseEmoteSetInto(emoteSetRef.toObject(), "Channel", emotes); + } + + return emotes; +} + +FfzChannelBadgeMap ffz::detail::parseChannelBadges(const QJsonObject &badgeRoot) +{ + FfzChannelBadgeMap channelBadges; + + for (auto it = badgeRoot.begin(); it != badgeRoot.end(); ++it) + { + const auto badgeID = it.key().toInt(); + const auto &jsonUserIDs = it.value().toArray(); + for (const auto &jsonUserID : jsonUserIDs) + { + // NOTE: The Twitch User IDs come through as ints right now, the code below + // tries to parse them as strings first since that's how we treat them anyway. + if (jsonUserID.isString()) + { + channelBadges[jsonUserID.toString()].emplace_back(badgeID); + } + else + { + channelBadges[QString::number(jsonUserID.toInt())].emplace_back( + badgeID); + } + } + } + + return channelBadges; +} + FfzEmotes::FfzEmotes() : global_(std::make_shared()) { + getSettings()->enableFFZGlobalEmotes.connect( + [this] { + this->loadEmotes(); + }, + this->managedConnections, false); } std::shared_ptr FfzEmotes::emotes() const @@ -171,20 +218,22 @@ std::shared_ptr FfzEmotes::emotes() const return this->global_.get(); } -boost::optional FfzEmotes::emote(const EmoteName &name) const +std::optional FfzEmotes::emote(const EmoteName &name) const { auto emotes = this->global_.get(); auto it = emotes->find(name); if (it != emotes->end()) + { return it->second; - return boost::none; + } + return std::nullopt; } void FfzEmotes::loadEmotes() { if (!Settings::instance().enableFFZGlobalEmotes) { - this->global_.set(EMPTY_EMOTE_MAP); + this->setEmotes(EMPTY_EMOTE_MAP); return; } @@ -193,93 +242,90 @@ void FfzEmotes::loadEmotes() NetworkRequest(url) .timeout(30000) - .onSuccess([this](auto result) -> Outcome { - auto emotes = this->emotes(); - auto pair = parseGlobalEmotes(result.parseJson(), *emotes); - if (pair.first) - this->global_.set( - std::make_shared(std::move(pair.second))); - return pair.first; + .onSuccess([this](auto result) { + auto parsedSet = parseGlobalEmotes(result.parseJson()); + this->setEmotes(std::make_shared(std::move(parsedSet))); }) .execute(); } +void FfzEmotes::setEmotes(std::shared_ptr emotes) +{ + this->global_.set(std::move(emotes)); +} + void FfzEmotes::loadChannel( - std::weak_ptr channel, const QString &channelId, + std::weak_ptr channel, const QString &channelID, std::function emoteCallback, - std::function)> modBadgeCallback, - std::function)> vipBadgeCallback, + std::function)> modBadgeCallback, + std::function)> vipBadgeCallback, + std::function channelBadgesCallback, bool manualRefresh) { - qCDebug(chatterinoFfzemotes) - << "[FFZEmotes] Reload FFZ Channel Emotes for channel" << channelId; + qCDebug(LOG) << "Reload FFZ Channel Emotes for channel" << channelID; - NetworkRequest("https://api.frankerfacez.com/v1/room/id/" + channelId) + NetworkRequest("https://api.frankerfacez.com/v1/room/id/" + channelID) .timeout(20000) .onSuccess([emoteCallback = std::move(emoteCallback), modBadgeCallback = std::move(modBadgeCallback), - vipBadgeCallback = std::move(vipBadgeCallback), channel, - manualRefresh](auto result) -> Outcome { - auto json = result.parseJson(); + vipBadgeCallback = std::move(vipBadgeCallback), + channelBadgesCallback = std::move(channelBadgesCallback), + channel, manualRefresh](const auto &result) { + const auto json = result.parseJson(); + auto emoteMap = parseChannelEmotes(json); auto modBadge = parseAuthorityBadge( - json.value("room").toObject().value("mod_urls").toObject(), - "Moderator"); + json["room"]["mod_urls"].toObject(), "Moderator"); auto vipBadge = parseAuthorityBadge( - json.value("room").toObject().value("vip_badge").toObject(), - "VIP"); + json["room"]["vip_badge"].toObject(), "VIP"); + auto channelBadges = + parseChannelBadges(json["room"]["user_badge_ids"].toObject()); bool hasEmotes = !emoteMap.empty(); emoteCallback(std::move(emoteMap)); modBadgeCallback(std::move(modBadge)); vipBadgeCallback(std::move(vipBadge)); + channelBadgesCallback(std::move(channelBadges)); if (auto shared = channel.lock(); manualRefresh) { if (hasEmotes) { - shared->addMessage(makeSystemMessage( - "FrankerFaceZ channel emotes reloaded.")); + shared->addSystemMessage( + "FrankerFaceZ channel emotes reloaded."); } else { - shared->addMessage( - makeSystemMessage(CHANNEL_HAS_NO_EMOTES)); + shared->addSystemMessage(CHANNEL_HAS_NO_EMOTES); } } - - return Success; }) - .onError([channelId, channel, manualRefresh](NetworkResult result) { + .onError([channelID, channel, manualRefresh](const auto &result) { auto shared = channel.lock(); if (!shared) + { return; + } + if (result.status() == 404) { // User does not have any FFZ emotes if (manualRefresh) - shared->addMessage( - makeSystemMessage(CHANNEL_HAS_NO_EMOTES)); - } - else if (result.status() == NetworkResult::timedoutStatus) - { - // TODO: Auto retry in case of a timeout, with a delay - qCWarning(chatterinoFfzemotes) - << "Fetching FFZ emotes for channel" << channelId - << "failed due to timeout"; - shared->addMessage( - makeSystemMessage("Failed to fetch FrankerFaceZ channel " - "emotes. (timed out)")); + { + shared->addSystemMessage(CHANNEL_HAS_NO_EMOTES); + } } else { - qCWarning(chatterinoFfzemotes) - << "Error fetching FFZ emotes for channel" << channelId - << ", error" << result.status(); - shared->addMessage( - makeSystemMessage("Failed to fetch FrankerFaceZ channel " - "emotes. (unknown error)")); + // TODO: Auto retry in case of a timeout, with a delay + auto errorString = result.formatError(); + qCWarning(LOG) << "Error fetching FFZ emotes for channel" + << channelID << ", error" << errorString; + shared->addSystemMessage( + QStringLiteral("Failed to fetch FrankerFaceZ channel " + "emotes. (Error: %1)") + .arg(errorString)); } }) .execute(); diff --git a/src/providers/ffz/FfzEmotes.hpp b/src/providers/ffz/FfzEmotes.hpp index 123777670..4c42ecbe4 100644 --- a/src/providers/ffz/FfzEmotes.hpp +++ b/src/providers/ffz/FfzEmotes.hpp @@ -1,37 +1,63 @@ #pragma once -#include -#include "boost/optional.hpp" #include "common/Aliases.hpp" #include "common/Atomic.hpp" -#include "providers/twitch/TwitchChannel.hpp" +#include "util/QStringHash.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include namespace chatterino { struct Emote; using EmotePtr = std::shared_ptr; class EmoteMap; +class Channel; + +/// Maps a Twitch User ID to a list of badge IDs +using FfzChannelBadgeMap = + boost::unordered::unordered_flat_map>; + +namespace ffz::detail { + + EmoteMap parseChannelEmotes(const QJsonObject &jsonRoot); + + /** + * Parse the `user_badge_ids` into a map of User IDs -> Badge IDs + */ + FfzChannelBadgeMap parseChannelBadges(const QJsonObject &badgeRoot); + +} // namespace ffz::detail class FfzEmotes final { - static constexpr const char *globalEmoteApiUrl = - "https://api.frankerfacez.com/v1/set/global"; - public: FfzEmotes(); std::shared_ptr emotes() const; - boost::optional emote(const EmoteName &name) const; + std::optional emote(const EmoteName &name) const; void loadEmotes(); + void setEmotes(std::shared_ptr emotes); static void loadChannel( std::weak_ptr channel, const QString &channelId, std::function emoteCallback, - std::function)> modBadgeCallback, - std::function)> vipBadgeCallback, + std::function)> modBadgeCallback, + std::function)> vipBadgeCallback, + std::function channelBadgesCallback, bool manualRefresh); private: Atomic> global_; + + std::vector> + managedConnections; }; } // namespace chatterino diff --git a/src/providers/ffz/FfzUtil.cpp b/src/providers/ffz/FfzUtil.cpp new file mode 100644 index 000000000..b621edbaf --- /dev/null +++ b/src/providers/ffz/FfzUtil.cpp @@ -0,0 +1,14 @@ +#include "providers/ffz/FfzUtil.hpp" + +#include + +namespace chatterino { + +Url parseFfzUrl(const QString &ffzUrl) +{ + QUrl asURL(ffzUrl); + asURL.setScheme("https"); + return {asURL.toString()}; +} + +} // namespace chatterino diff --git a/src/providers/ffz/FfzUtil.hpp b/src/providers/ffz/FfzUtil.hpp new file mode 100644 index 000000000..4afd6c818 --- /dev/null +++ b/src/providers/ffz/FfzUtil.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "common/Aliases.hpp" + +#include + +namespace chatterino { + +Url parseFfzUrl(const QString &ffzUrl); + +} // namespace chatterino diff --git a/src/providers/irc/AbstractIrcServer.cpp b/src/providers/irc/AbstractIrcServer.cpp deleted file mode 100644 index 91f4decee..000000000 --- a/src/providers/irc/AbstractIrcServer.cpp +++ /dev/null @@ -1,425 +0,0 @@ -#include "AbstractIrcServer.hpp" - -#include "common/Channel.hpp" -#include "common/Common.hpp" -#include "common/QLogging.hpp" -#include "messages/LimitedQueueSnapshot.hpp" -#include "messages/Message.hpp" -#include "messages/MessageBuilder.hpp" - -#include - -namespace chatterino { - -const int RECONNECT_BASE_INTERVAL = 2000; -// 60 falloff counter means it will try to reconnect at most every 60*2 seconds -const int MAX_FALLOFF_COUNTER = 60; - -// Ratelimits for joinBucket_ -const int JOIN_RATELIMIT_BUDGET = 18; -const int JOIN_RATELIMIT_COOLDOWN = 12500; - -AbstractIrcServer::AbstractIrcServer() -{ - // Initialize the connections - // XXX: don't create write connection if there is no separate write connection. - this->writeConnection_.reset(new IrcConnection); - this->writeConnection_->moveToThread( - QCoreApplication::instance()->thread()); - - // Apply a leaky bucket rate limiting to JOIN messages - auto actuallyJoin = [&](QString message) { - if (!this->channels.contains(message)) - { - return; - } - this->readConnection_->sendRaw("JOIN #" + message); - }; - this->joinBucket_.reset(new RatelimitBucket( - JOIN_RATELIMIT_BUDGET, JOIN_RATELIMIT_COOLDOWN, actuallyJoin, this)); - - QObject::connect(this->writeConnection_.get(), - &Communi::IrcConnection::messageReceived, this, - [this](auto msg) { - this->writeConnectionMessageReceived(msg); - }); - QObject::connect(this->writeConnection_.get(), - &Communi::IrcConnection::connected, this, [this] { - this->onWriteConnected(this->writeConnection_.get()); - }); - this->connections_.managedConnect( - this->writeConnection_->connectionLost, [this](bool timeout) { - qCDebug(chatterinoIrc) - << "Write connection reconnect requested. Timeout:" << timeout; - this->writeConnection_->smartReconnect.invoke(); - }); - - // Listen to read connection message signals - this->readConnection_.reset(new IrcConnection); - this->readConnection_->moveToThread(QCoreApplication::instance()->thread()); - - QObject::connect(this->readConnection_.get(), - &Communi::IrcConnection::messageReceived, this, - [this](auto msg) { - this->readConnectionMessageReceived(msg); - }); - QObject::connect(this->readConnection_.get(), - &Communi::IrcConnection::privateMessageReceived, this, - [this](auto msg) { - this->privateMessageReceived(msg); - }); - QObject::connect(this->readConnection_.get(), - &Communi::IrcConnection::connected, this, [this] { - this->onReadConnected(this->readConnection_.get()); - }); - QObject::connect(this->readConnection_.get(), - &Communi::IrcConnection::disconnected, this, [this] { - this->onDisconnected(); - }); - this->connections_.managedConnect( - this->readConnection_->connectionLost, [this](bool timeout) { - qCDebug(chatterinoIrc) - << "Read connection reconnect requested. Timeout:" << timeout; - if (timeout) - { - // Show additional message since this is going to interrupt a - // connection that is still "connected" - this->addGlobalSystemMessage( - "Server connection timed out, reconnecting"); - } - this->readConnection_->smartReconnect.invoke(); - }); -} - -void AbstractIrcServer::initializeIrc() -{ - assert(!this->initialized_); - - if (this->hasSeparateWriteConnection()) - { - this->initializeConnectionSignals(this->writeConnection_.get(), - ConnectionType::Write); - this->initializeConnectionSignals(this->readConnection_.get(), - ConnectionType::Read); - } - else - { - this->initializeConnectionSignals(this->readConnection_.get(), - ConnectionType::Both); - } - - this->initialized_ = true; -} - -void AbstractIrcServer::connect() -{ - assert(this->initialized_); - - this->disconnect(); - - if (this->hasSeparateWriteConnection()) - { - this->initializeConnection(this->writeConnection_.get(), Write); - this->initializeConnection(this->readConnection_.get(), Read); - } - else - { - this->initializeConnection(this->readConnection_.get(), Both); - } -} - -void AbstractIrcServer::open(ConnectionType type) -{ - std::lock_guard lock(this->connectionMutex_); - - if (type == Write) - { - this->writeConnection_->open(); - } - if (type & Read) - { - this->readConnection_->open(); - } -} - -void AbstractIrcServer::addGlobalSystemMessage(const QString &messageText) -{ - std::lock_guard lock(this->channelMutex); - - MessageBuilder b(systemMessage, messageText); - auto message = b.release(); - - for (std::weak_ptr &weak : this->channels.values()) - { - std::shared_ptr chan = weak.lock(); - if (!chan) - { - continue; - } - - chan->addMessage(message); - } -} - -void AbstractIrcServer::disconnect() -{ - std::lock_guard locker(this->connectionMutex_); - - this->readConnection_->close(); - if (this->hasSeparateWriteConnection()) - { - this->writeConnection_->close(); - } -} - -void AbstractIrcServer::sendMessage(const QString &channelName, - const QString &message) -{ - this->sendRawMessage("PRIVMSG #" + channelName + " :" + message); -} - -void AbstractIrcServer::sendRawMessage(const QString &rawMessage) -{ - std::lock_guard locker(this->connectionMutex_); - - if (this->hasSeparateWriteConnection()) - { - this->writeConnection_->sendRaw(rawMessage); - } - else - { - this->readConnection_->sendRaw(rawMessage); - } -} - -void AbstractIrcServer::writeConnectionMessageReceived( - Communi::IrcMessage *message) -{ - (void)message; -} - -ChannelPtr AbstractIrcServer::getOrAddChannel(const QString &dirtyChannelName) -{ - auto channelName = this->cleanChannelName(dirtyChannelName); - - // try get channel - ChannelPtr chan = this->getChannelOrEmpty(channelName); - if (chan != Channel::getEmpty()) - { - return chan; - } - - std::lock_guard lock(this->channelMutex); - - // value doesn't exist - chan = this->createChannel(channelName); - if (!chan) - { - return Channel::getEmpty(); - } - - this->channels.insert(channelName, chan); - this->connections_.managedConnect(chan->destroyed, [this, channelName] { - // fourtf: issues when the server itself is destroyed - - qCDebug(chatterinoIrc) << "[AbstractIrcServer::addChannel]" - << channelName << "was destroyed"; - this->channels.remove(channelName); - - if (this->readConnection_) - { - this->readConnection_->sendRaw("PART #" + channelName); - } - }); - - // join IRC channel - { - std::lock_guard lock2(this->connectionMutex_); - - if (this->readConnection_) - { - if (this->readConnection_->isConnected()) - { - this->joinBucket_->send(channelName); - } - } - } - - return chan; -} - -ChannelPtr AbstractIrcServer::getChannelOrEmpty(const QString &dirtyChannelName) -{ - auto channelName = this->cleanChannelName(dirtyChannelName); - - std::lock_guard lock(this->channelMutex); - - // try get special channel - ChannelPtr chan = this->getCustomChannel(channelName); - if (chan) - { - return chan; - } - - // value exists - auto it = this->channels.find(channelName); - if (it != this->channels.end()) - { - chan = it.value().lock(); - - if (chan) - { - return chan; - } - } - - return Channel::getEmpty(); -} - -std::vector> AbstractIrcServer::getChannels() -{ - std::lock_guard lock(this->channelMutex); - std::vector> channels; - - for (auto &&weak : this->channels.values()) - { - channels.push_back(weak); - } - - return channels; -} - -void AbstractIrcServer::onReadConnected(IrcConnection *connection) -{ - (void)connection; - - std::lock_guard lock(this->channelMutex); - - // join channels - for (auto &&weak : this->channels) - { - if (auto channel = weak.lock()) - { - this->joinBucket_->send(channel->getName()); - } - } - - // connected/disconnected message - auto connectedMsg = makeSystemMessage("connected"); - connectedMsg->flags.set(MessageFlag::ConnectedMessage); - auto reconnected = makeSystemMessage("reconnected"); - reconnected->flags.set(MessageFlag::ConnectedMessage); - - for (std::weak_ptr &weak : this->channels.values()) - { - std::shared_ptr chan = weak.lock(); - if (!chan) - { - continue; - } - - LimitedQueueSnapshot snapshot = chan->getMessageSnapshot(); - - bool replaceMessage = - snapshot.size() > 0 && snapshot[snapshot.size() - 1]->flags.has( - MessageFlag::DisconnectedMessage); - - if (replaceMessage) - { - chan->replaceMessage(snapshot[snapshot.size() - 1], reconnected); - } - else - { - chan->addMessage(connectedMsg); - } - - chan->connected.invoke(); - } - - this->falloffCounter_ = 1; -} - -void AbstractIrcServer::onWriteConnected(IrcConnection *connection) -{ - (void)connection; -} - -void AbstractIrcServer::onDisconnected() -{ - std::lock_guard lock(this->channelMutex); - - MessageBuilder b(systemMessage, "disconnected"); - b->flags.set(MessageFlag::DisconnectedMessage); - auto disconnectedMsg = b.release(); - - for (std::weak_ptr &weak : this->channels.values()) - { - std::shared_ptr chan = weak.lock(); - if (!chan) - { - continue; - } - - chan->addMessage(disconnectedMsg); - } -} - -std::shared_ptr AbstractIrcServer::getCustomChannel( - const QString &channelName) -{ - (void)channelName; - return nullptr; -} - -QString AbstractIrcServer::cleanChannelName(const QString &dirtyChannelName) -{ - if (dirtyChannelName.startsWith('#')) - return dirtyChannelName.mid(1); - else - return dirtyChannelName; -} - -void AbstractIrcServer::addFakeMessage(const QString &data) -{ - auto fakeMessage = Communi::IrcMessage::fromData( - data.toUtf8(), this->readConnection_.get()); - - if (fakeMessage->command() == "PRIVMSG") - { - this->privateMessageReceived( - static_cast(fakeMessage)); - } - else - { - this->readConnectionMessageReceived(fakeMessage); - } -} - -void AbstractIrcServer::privateMessageReceived( - Communi::IrcPrivateMessage *message) -{ - (void)message; -} - -void AbstractIrcServer::readConnectionMessageReceived( - Communi::IrcMessage *message) -{ -} - -void AbstractIrcServer::forEachChannel(std::function func) -{ - std::lock_guard lock(this->channelMutex); - - for (std::weak_ptr &weak : this->channels.values()) - { - ChannelPtr chan = weak.lock(); - if (!chan) - { - continue; - } - - func(chan); - } -} - -} // namespace chatterino diff --git a/src/providers/irc/AbstractIrcServer.hpp b/src/providers/irc/AbstractIrcServer.hpp deleted file mode 100644 index 796bfa809..000000000 --- a/src/providers/irc/AbstractIrcServer.hpp +++ /dev/null @@ -1,107 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -#include "common/Common.hpp" -#include "providers/irc/IrcConnection2.hpp" -#include "util/RatelimitBucket.hpp" - -namespace chatterino { - -class Channel; -using ChannelPtr = std::shared_ptr; - -class AbstractIrcServer : public QObject -{ -public: - enum ConnectionType { Read = 1, Write = 2, Both = 3 }; - - virtual ~AbstractIrcServer() = default; - - // initializeIrc must be called from the derived class - // this allows us to initialize the abstract IRC server based on the derived class's parameters - void initializeIrc(); - - // connection - void connect(); - void disconnect(); - - void sendMessage(const QString &channelName, const QString &message); - void sendRawMessage(const QString &rawMessage); - - // channels - ChannelPtr getOrAddChannel(const QString &dirtyChannelName); - ChannelPtr getChannelOrEmpty(const QString &dirtyChannelName); - std::vector> getChannels(); - - // signals - pajlada::Signals::NoArgSignal connected; - pajlada::Signals::NoArgSignal disconnected; - - void addFakeMessage(const QString &data); - - void addGlobalSystemMessage(const QString &messageText); - - // iteration - void forEachChannel(std::function func); - -protected: - AbstractIrcServer(); - - // initializeConnectionSignals is called on a connection once in its lifetime. - // it can be used to connect signals to your class - virtual void initializeConnectionSignals(IrcConnection *connection, - ConnectionType type){}; - - // initializeConnection is called every time before we try to connect to the IRC server - virtual void initializeConnection(IrcConnection *connection, - ConnectionType type) = 0; - - virtual std::shared_ptr createChannel( - const QString &channelName) = 0; - - virtual void privateMessageReceived(Communi::IrcPrivateMessage *message); - virtual void readConnectionMessageReceived(Communi::IrcMessage *message); - virtual void writeConnectionMessageReceived(Communi::IrcMessage *message); - - virtual void onReadConnected(IrcConnection *connection); - virtual void onWriteConnected(IrcConnection *connection); - virtual void onDisconnected(); - - virtual std::shared_ptr getCustomChannel( - const QString &channelName); - - virtual bool hasSeparateWriteConnection() const = 0; - virtual QString cleanChannelName(const QString &dirtyChannelName); - - void open(ConnectionType type); - - QMap> channels; - std::mutex channelMutex; - -private: - void initConnection(); - - QObjectPtr writeConnection_ = nullptr; - QObjectPtr readConnection_ = nullptr; - - // Our rate limiting bucket for the Twitch join rate limits - // https://dev.twitch.tv/docs/irc/guide#rate-limits - QObjectPtr joinBucket_; - - QTimer reconnectTimer_; - int falloffCounter_ = 1; - - std::mutex connectionMutex_; - - // bool autoReconnect_ = false; - pajlada::Signals::SignalHolder connections_; - - bool initialized_{false}; -}; - -} // namespace chatterino diff --git a/src/providers/irc/Irc2.cpp b/src/providers/irc/Irc2.cpp deleted file mode 100644 index 61a8a2cdd..000000000 --- a/src/providers/irc/Irc2.cpp +++ /dev/null @@ -1,264 +0,0 @@ -#include "Irc2.hpp" - -#include -#include "common/Credentials.hpp" -#include "common/SignalVectorModel.hpp" -#include "singletons/Paths.hpp" -#include "util/CombinePath.hpp" -#include "util/RapidjsonHelpers.hpp" -#include "util/StandardItemHelper.hpp" - -#include -#include - -#include - -namespace chatterino { - -namespace { - QString configPath() - { - return combinePath(getPaths()->settingsDirectory, "irc.json"); - } - - class Model : public SignalVectorModel - { - public: - Model(QObject *parent) - : SignalVectorModel(6, parent) - { - } - - // turn a vector item into a model row - IrcServerData getItemFromRow(std::vector &row, - const IrcServerData &original) - { - return IrcServerData{ - row[0]->data(Qt::EditRole).toString(), // host - row[1]->data(Qt::EditRole).toInt(), // port - row[2]->data(Qt::CheckStateRole).toBool(), // ssl - row[3]->data(Qt::EditRole).toString(), // user - row[4]->data(Qt::EditRole).toString(), // nick - row[5]->data(Qt::EditRole).toString(), // real - original.authType, // authType - original.connectCommands, // connectCommands - original.id, // id - }; - } - - // turns a row in the model into a vector item - void getRowFromItem(const IrcServerData &item, - std::vector &row) - { - setStringItem(row[0], item.host, false); - setStringItem(row[1], QString::number(item.port)); - setBoolItem(row[2], item.ssl); - setStringItem(row[3], item.user); - setStringItem(row[4], item.nick); - setStringItem(row[5], item.real); - } - }; -} // namespace - -inline QString escape(QString str) -{ - return str.replace(":", "::"); -} - -// This returns a unique id for every server which is understandeable in the systems credential manager. -inline QString getCredentialName(const IrcServerData &data) -{ - return escape(QString::number(data.id)) + ":" + escape(data.user) + "@" + - escape(data.host); -} - -void IrcServerData::getPassword( - QObject *receiver, std::function &&onLoaded) const -{ - Credentials::instance().get("irc", getCredentialName(*this), receiver, - std::move(onLoaded)); -} - -void IrcServerData::setPassword(const QString &password) -{ - Credentials::instance().set("irc", getCredentialName(*this), password); -} - -Irc::Irc() -{ - this->connections.itemInserted.connect([this](auto &&args) { - // make sure only one id can only exist for one server - assert(this->servers_.find(args.item.id) == this->servers_.end()); - - // add new server - if (auto ab = this->abandonedChannels_.find(args.item.id); - ab != this->abandonedChannels_.end()) - { - auto server = std::make_unique(args.item, ab->second); - - // set server of abandoned channels - for (auto weak : ab->second) - if (auto shared = weak.lock()) - if (auto ircChannel = - dynamic_cast(shared.get())) - ircChannel->setServer(server.get()); - - // add new server with abandoned channels - this->servers_.emplace(args.item.id, std::move(server)); - this->abandonedChannels_.erase(ab); - } - else - { - // add new server - this->servers_.emplace(args.item.id, - std::make_unique(args.item)); - } - }); - - this->connections.itemRemoved.connect([this](auto &&args) { - // restore - if (auto server = this->servers_.find(args.item.id); - server != this->servers_.end()) - { - auto abandoned = server->second->getChannels(); - - // set server of abandoned servers to nullptr - for (auto weak : abandoned) - if (auto shared = weak.lock()) - if (auto ircChannel = - dynamic_cast(shared.get())) - ircChannel->setServer(nullptr); - - this->abandonedChannels_[args.item.id] = abandoned; - this->servers_.erase(server); - } - - if (args.caller != Irc::noEraseCredentialCaller) - { - Credentials::instance().erase("irc", getCredentialName(args.item)); - } - }); - - this->connections.delayedItemsChanged.connect([this] { - this->save(); - }); -} - -QAbstractTableModel *Irc::newConnectionModel(QObject *parent) -{ - auto model = new Model(parent); - model->initialize(&this->connections); - return model; -} - -ChannelPtr Irc::getOrAddChannel(int id, QString name) -{ - if (auto server = this->servers_.find(id); server != this->servers_.end()) - { - return server->second->getOrAddChannel(name); - } - else - { - auto channel = std::make_shared(name, nullptr); - - this->abandonedChannels_[id].push_back(channel); - - return std::move(channel); - } -} - -Irc &Irc::instance() -{ - static Irc irc; - return irc; -} - -int Irc::uniqueId() -{ - int i = this->currentId_ + 1; - auto it = this->servers_.find(i); - auto it2 = this->abandonedChannels_.find(i); - - while (it != this->servers_.end() || it2 != this->abandonedChannels_.end()) - { - i++; - it = this->servers_.find(i); - it2 = this->abandonedChannels_.find(i); - } - - return (this->currentId_ = i); -} - -void Irc::save() -{ - QJsonDocument doc; - QJsonObject root; - QJsonArray servers; - - for (auto &&conn : this->connections) - { - QJsonObject obj; - obj.insert("host", conn.host); - obj.insert("port", conn.port); - obj.insert("ssl", conn.ssl); - obj.insert("username", conn.user); - obj.insert("nickname", conn.nick); - obj.insert("realname", conn.real); - obj.insert("connectCommands", - QJsonArray::fromStringList(conn.connectCommands)); - obj.insert("id", conn.id); - obj.insert("authType", int(conn.authType)); - - servers.append(obj); - } - - root.insert("servers", servers); - doc.setObject(root); - - QSaveFile file(configPath()); - file.open(QIODevice::WriteOnly); - file.write(doc.toJson()); - file.commit(); -} - -void Irc::load() -{ - if (this->loaded_) - return; - this->loaded_ = true; - - QString config = configPath(); - QFile file(configPath()); - file.open(QIODevice::ReadOnly); - auto object = QJsonDocument::fromJson(file.readAll()).object(); - - std::unordered_set ids; - - // load servers - for (auto server : object.value("servers").toArray()) - { - auto obj = server.toObject(); - IrcServerData data; - data.host = obj.value("host").toString(data.host); - data.port = obj.value("port").toInt(data.port); - data.ssl = obj.value("ssl").toBool(data.ssl); - data.user = obj.value("username").toString(data.user); - data.nick = obj.value("nickname").toString(data.nick); - data.real = obj.value("realname").toString(data.real); - data.connectCommands = - obj.value("connectCommands").toVariant().toStringList(); - data.id = obj.value("id").toInt(data.id); - data.authType = - IrcAuthType(obj.value("authType").toInt(int(data.authType))); - - // duplicate id's are not allowed :( - if (ids.find(data.id) == ids.end()) - { - ids.insert(data.id); - - this->connections.append(data); - } - } -} - -} // namespace chatterino diff --git a/src/providers/irc/Irc2.hpp b/src/providers/irc/Irc2.hpp deleted file mode 100644 index c4ae8c733..000000000 --- a/src/providers/irc/Irc2.hpp +++ /dev/null @@ -1,67 +0,0 @@ -#pragma once - -#include -#include - -#include "providers/irc/IrcChannel2.hpp" -#include "providers/irc/IrcServer.hpp" - -class QAbstractTableModel; - -namespace chatterino { - -enum class IrcAuthType { Anonymous, Custom, Pass, Sasl }; - -struct IrcServerData { - QString host; - int port = 6697; - bool ssl = true; - - QString user; - QString nick; - QString real; - - IrcAuthType authType = IrcAuthType::Anonymous; - void getPassword(QObject *receiver, - std::function &&onLoaded) const; - void setPassword(const QString &password); - - QStringList connectCommands; - - int id; -}; - -class Irc -{ -public: - Irc(); - - static Irc &instance(); - - static inline void *const noEraseCredentialCaller = - reinterpret_cast(1); - - SignalVector connections; - QAbstractTableModel *newConnectionModel(QObject *parent); - - ChannelPtr getOrAddChannel(int serverId, QString name); - - void save(); - void load(); - - int uniqueId(); - -private: - int currentId_{}; - bool loaded_{}; - - // Servers have a unique id. - // When a server gets changed it gets removed and then added again. - // So we store the channels of that server in abandonedChannels_ temporarily. - // Or if the server got removed permanently then it's still stored there. - std::unordered_map> servers_; - std::unordered_map>> - abandonedChannels_; -}; - -} // namespace chatterino diff --git a/src/providers/irc/IrcAccount.cpp b/src/providers/irc/IrcAccount.cpp deleted file mode 100644 index 96486b7ea..000000000 --- a/src/providers/irc/IrcAccount.cpp +++ /dev/null @@ -1,36 +0,0 @@ -#include "IrcAccount.hpp" - -// namespace chatterino { -// -// IrcAccount::IrcAccount(const QString &_userName, const QString &_nickName, -// const QString -// &_realName, -// const QString &_password) -// : userName(_userName) -// , nickName(_nickName) -// , realName(_realName) -// , password(_password) -//{ -//} - -// const QString &IrcAccount::getUserName() const -//{ -// return this->userName; -//} - -// const QString &IrcAccount::getNickName() const -//{ -// return this->nickName; -//} - -// const QString &IrcAccount::getRealName() const -//{ -// return this->realName; -//} - -// const QString &IrcAccount::getPassword() const -//{ -// return this->password; -//} -// -//} // namespace chatterino diff --git a/src/providers/irc/IrcAccount.hpp b/src/providers/irc/IrcAccount.hpp deleted file mode 100644 index 2c4345b10..000000000 --- a/src/providers/irc/IrcAccount.hpp +++ /dev/null @@ -1,26 +0,0 @@ -#pragma once - -#include - -// namespace chatterino { -// -// class IrcAccount -//{ -// public: -// IrcAccount(const QString &userName, const QString &nickName, const QString -// &realName, -// const QString &password); - -// const QString &getUserName() const; -// const QString &getNickName() const; -// const QString &getRealName() const; -// const QString &getPassword() const; - -// private: -// QString userName; -// QString nickName; -// QString realName; -// QString password; -//}; -// -//} // namespace chatterino diff --git a/src/providers/irc/IrcChannel2.cpp b/src/providers/irc/IrcChannel2.cpp deleted file mode 100644 index 123ae738d..000000000 --- a/src/providers/irc/IrcChannel2.cpp +++ /dev/null @@ -1,78 +0,0 @@ -#include "IrcChannel2.hpp" - -#include "debug/AssertInGuiThread.hpp" -#include "messages/Message.hpp" -#include "messages/MessageBuilder.hpp" -#include "providers/irc/IrcCommands.hpp" -#include "providers/irc/IrcServer.hpp" - -namespace chatterino { - -IrcChannel::IrcChannel(const QString &name, IrcServer *server) - : Channel(name, Channel::Type::Irc) - , ChannelChatters(*static_cast(this)) - , server_(server) -{ -} - -void IrcChannel::sendMessage(const QString &message) -{ - assertInGuiThread(); - if (message.isEmpty()) - { - return; - } - - if (message.startsWith("/")) - { - int index = message.indexOf(' ', 1); - QString command = message.mid(1, index - 1); - QString params = index == -1 ? "" : message.mid(index + 1); - - invokeIrcCommand(command, params, *this); - } - else - { - if (this->server()) - this->server()->sendMessage(this->getName(), message); - - MessageBuilder builder; - builder.emplace(); - const auto &nick = this->server()->nick(); - builder.emplace(nick + ":", MessageElementFlag::Username) - ->setLink({Link::UserInfo, nick}); - builder.emplace(message, MessageElementFlag::Text); - builder.message().messageText = message; - builder.message().searchText = nick + ": " + message; - builder.message().loginName = nick; - builder.message().displayName = nick; - this->addMessage(builder.release()); - } -} - -IrcServer *IrcChannel::server() -{ - assertInGuiThread(); - - return this->server_; -} - -void IrcChannel::setServer(IrcServer *server) -{ - assertInGuiThread(); - - this->server_ = server; -} - -bool IrcChannel::canReconnect() const -{ - return true; -} - -void IrcChannel::reconnect() -{ - if (this->server()) - this->server()->connect(); -} - -} // namespace chatterino diff --git a/src/providers/irc/IrcChannel2.hpp b/src/providers/irc/IrcChannel2.hpp deleted file mode 100644 index 81b85ce18..000000000 --- a/src/providers/irc/IrcChannel2.hpp +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once - -#include "common/Channel.hpp" -#include "common/ChannelChatters.hpp" - -namespace chatterino { - -class Irc; -class IrcServer; - -class IrcChannel : public Channel, public ChannelChatters -{ -public: - explicit IrcChannel(const QString &name, IrcServer *server); - - void sendMessage(const QString &message) override; - - // server may be nullptr - IrcServer *server(); - - // Channel methods - virtual bool canReconnect() const override; - virtual void reconnect() override; - -private: - void setServer(IrcServer *server); - - IrcServer *server_; - - friend class Irc; -}; - -} // namespace chatterino diff --git a/src/providers/irc/IrcCommands.cpp b/src/providers/irc/IrcCommands.cpp deleted file mode 100644 index 99bd5cb8c..000000000 --- a/src/providers/irc/IrcCommands.cpp +++ /dev/null @@ -1,93 +0,0 @@ -#include "IrcCommands.hpp" - -#include "messages/MessageBuilder.hpp" -#include "providers/irc/IrcChannel2.hpp" -#include "providers/irc/IrcServer.hpp" -#include "util/Overloaded.hpp" -#include "util/QStringHash.hpp" - -namespace chatterino { - -Outcome invokeIrcCommand(const QString &commandName, const QString &allParams, - IrcChannel &channel) -{ - if (!channel.server()) - { - return Failure; - } - - // STATIC MESSAGES - static auto staticMessages = std::unordered_map{ - {"join", "/join is not supported. Press ctrl+r to change the " - "channel. If required use /raw JOIN #channel."}, - {"part", "/part is not supported. Press ctrl+r to change the " - "channel. If required use /raw PART #channel."}, - }; - auto cmd = commandName.toLower(); - - if (auto it = staticMessages.find(cmd); it != staticMessages.end()) - { - channel.addMessage(makeSystemMessage(it->second)); - return Success; - } - - // CUSTOM COMMANDS - auto params = allParams.split(' '); - auto paramsAfter = [&](int i) { - return params.mid(i + 1).join(' '); - }; - - auto sendRaw = [&](QString str) { - channel.server()->sendRawMessage(str); - }; - - if (cmd == "msg") - { - sendRaw("PRIVMSG " + params[0] + " :" + paramsAfter(0)); - } - else if (cmd == "away") - { - sendRaw("AWAY " + params[0] + " :" + paramsAfter(0)); - } - else if (cmd == "knock") - { - sendRaw("KNOCK #" + params[0] + " " + paramsAfter(0)); - } - else if (cmd == "kick") - { - if (params.size() < 2) - { - channel.addMessage( - makeSystemMessage("Usage: /kick [message]")); - return Failure; - } - const auto &channelParam = params[0]; - const auto &clientParam = params[1]; - const auto &messageParam = paramsAfter(1); - if (messageParam.isEmpty()) - { - sendRaw("KICK " + channelParam + " " + clientParam); - } - else - { - sendRaw("KICK " + channelParam + " " + clientParam + " :" + - messageParam); - } - } - else if (cmd == "wallops") - { - sendRaw("WALLOPS :" + allParams); - } - else if (cmd == "raw") - { - sendRaw(allParams); - } - else - { - sendRaw(cmd.toUpper() + " " + allParams); - } - - return Success; -} - -} // namespace chatterino diff --git a/src/providers/irc/IrcCommands.hpp b/src/providers/irc/IrcCommands.hpp deleted file mode 100644 index 32679e599..000000000 --- a/src/providers/irc/IrcCommands.hpp +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once - -#include -#include "common/Outcome.hpp" - -namespace chatterino { - -class IrcChannel; - -Outcome invokeIrcCommand(const QString &command, const QString ¶ms, - IrcChannel &channel); - -} // namespace chatterino diff --git a/src/providers/irc/IrcConnection2.cpp b/src/providers/irc/IrcConnection2.cpp index 7e4c45379..3f9d57c97 100644 --- a/src/providers/irc/IrcConnection2.cpp +++ b/src/providers/irc/IrcConnection2.cpp @@ -1,13 +1,17 @@ -#include "IrcConnection2.hpp" +#include "providers/irc/IrcConnection2.hpp" #include "common/QLogging.hpp" #include "common/Version.hpp" +#include + +using namespace std::chrono_literals; + namespace chatterino { namespace { - const auto payload = QString("chatterino/" CHATTERINO_VERSION); + const auto payload = "chatterino/" + CHATTERINO_VERSION; } // namespace @@ -16,7 +20,7 @@ IrcConnection::IrcConnection(QObject *parent) { // Log connection errors for ease-of-debugging QObject::connect(this, &Communi::IrcConnection::socketError, this, - [this](QAbstractSocket::SocketError error) { + [](QAbstractSocket::SocketError error) { qCDebug(chatterinoIrc) << "Connection error:" << error; }); @@ -38,18 +42,6 @@ IrcConnection::IrcConnection(QObject *parent) } }); - // Schedule a reconnect that won't violate RECONNECT_MIN_INTERVAL - this->smartReconnect.connect([this] { - if (this->reconnectTimer_.isActive()) - { - return; - } - - auto delay = this->reconnectBackoff_.next(); - qCDebug(chatterinoIrc) << "Reconnecting in" << delay.count() << "ms"; - this->reconnectTimer_.start(delay); - }); - this->reconnectTimer_.setSingleShot(true); QObject::connect(&this->reconnectTimer_, &QTimer::timeout, [this] { if (this->isConnected()) @@ -68,6 +60,7 @@ IrcConnection::IrcConnection(QObject *parent) // Send ping every x seconds this->pingTimer_.setInterval(5000); this->pingTimer_.start(); + this->lastPing_ = std::chrono::system_clock::now(); QObject::connect(&this->pingTimer_, &QTimer::timeout, [this] { if (this->isConnected()) { @@ -76,6 +69,25 @@ IrcConnection::IrcConnection(QObject *parent) // If we're still receiving messages, all is well this->recentlyReceivedMessage_ = false; this->waitingForPong_ = false; + + // Check if we got invoked too late (e.g. due to a sleep) + auto now = std::chrono::system_clock::now(); + auto elapsed = now - this->lastPing_; + if (elapsed < 3 * 5000ms) + { + this->heartbeat.invoke(); + } + else + { + qCDebug(chatterinoIrc).nospace() + << "Got late ping (skipping heartbeat): " + << std::chrono::duration_cast< + std::chrono::milliseconds>(elapsed) + .count() + << "ms"; + } + this->lastPing_ = now; + return; } @@ -123,6 +135,19 @@ IrcConnection::~IrcConnection() this->disconnect(); } +void IrcConnection::smartReconnect() +{ + if (this->reconnectTimer_.isActive()) + { + // Ignore this reconnect request, we already have a reconnect request queued up + return; + } + + auto delay = this->reconnectBackoff_.next(); + qCDebug(chatterinoIrc) << "Reconnecting in" << delay.count() << "ms"; + this->reconnectTimer_.start(delay); +} + void IrcConnection::open() { this->expectConnectionLoss_ = false; diff --git a/src/providers/irc/IrcConnection2.hpp b/src/providers/irc/IrcConnection2.hpp index 1149bbebd..0e269397c 100644 --- a/src/providers/irc/IrcConnection2.hpp +++ b/src/providers/irc/IrcConnection2.hpp @@ -2,11 +2,12 @@ #include "util/ExponentialBackoff.hpp" -#include - #include +#include #include +#include + namespace chatterino { class IrcConnection : public Communi::IrcConnection @@ -20,8 +21,12 @@ public: // receiver to trigger a reconnect, if desired pajlada::Signals::Signal connectionLost; + // Signal to indicate the connection is still healthy + pajlada::Signals::NoArgSignal heartbeat; + // Request a reconnect with a minimum interval between attempts. - pajlada::Signals::NoArgSignal smartReconnect; + // This won't violate RECONNECT_MIN_INTERVAL + void smartReconnect(); virtual void open(); virtual void close(); @@ -30,6 +35,7 @@ private: QTimer pingTimer_; QTimer reconnectTimer_; std::atomic recentlyReceivedMessage_{true}; + std::chrono::time_point lastPing_; // Reconnect with a base delay of 1 second and max out at 1 second * (2^(5-1)) (i.e. 16 seconds) ExponentialBackoff<5> reconnectBackoff_{std::chrono::milliseconds{1000}}; diff --git a/src/providers/irc/IrcMessageBuilder.cpp b/src/providers/irc/IrcMessageBuilder.cpp deleted file mode 100644 index 77d36fc5a..000000000 --- a/src/providers/irc/IrcMessageBuilder.cpp +++ /dev/null @@ -1,239 +0,0 @@ -#include "providers/irc/IrcMessageBuilder.hpp" - -#include "Application.hpp" -#include "common/IrcColors.hpp" -#include "controllers/accounts/AccountController.hpp" -#include "controllers/ignores/IgnoreController.hpp" -#include "controllers/ignores/IgnorePhrase.hpp" -#include "messages/Message.hpp" -#include "providers/chatterino/ChatterinoBadges.hpp" -#include "singletons/Emotes.hpp" -#include "singletons/Resources.hpp" -#include "singletons/Settings.hpp" -#include "singletons/Theme.hpp" -#include "singletons/WindowManager.hpp" -#include "util/Helpers.hpp" -#include "util/IrcHelpers.hpp" -#include "widgets/Window.hpp" - -namespace { - -QRegularExpression IRC_COLOR_PARSE_REGEX( - "(\u0003(\\d{1,2})?(,(\\d{1,2}))?|\u000f)", - QRegularExpression::UseUnicodePropertiesOption); - -} // namespace - -namespace chatterino { - -IrcMessageBuilder::IrcMessageBuilder( - Channel *_channel, const Communi::IrcPrivateMessage *_ircMessage, - const MessageParseArgs &_args) - : SharedMessageBuilder(_channel, _ircMessage, _args) -{ -} - -IrcMessageBuilder::IrcMessageBuilder(Channel *_channel, - const Communi::IrcMessage *_ircMessage, - const MessageParseArgs &_args, - QString content, bool isAction) - : SharedMessageBuilder(_channel, _ircMessage, _args, content, isAction) -{ - assert(false); -} - -IrcMessageBuilder::IrcMessageBuilder( - const Communi::IrcNoticeMessage *_ircMessage, const MessageParseArgs &_args) - : SharedMessageBuilder(Channel::getEmpty().get(), _ircMessage, _args, - _ircMessage->content(), false) -{ -} - -MessagePtr IrcMessageBuilder::build() -{ - // PARSE - this->parse(); - this->usernameColor_ = getRandomColor(this->ircMessage->nick()); - - // PUSH ELEMENTS - this->appendChannelName(); - - this->message().serverReceivedTime = calculateMessageTime(this->ircMessage); - this->emplace(this->message().serverReceivedTime.time()); - - this->appendUsername(); - - // words - this->addWords(this->originalMessage_.split(' ')); - - this->message().messageText = this->originalMessage_; - this->message().searchText = this->message().localizedName + " " + - this->userName + ": " + this->originalMessage_; - - // highlights - this->parseHighlights(); - - // highlighting incoming whispers if requested per setting - if (this->args.isReceivedWhisper && getSettings()->highlightInlineWhispers) - { - this->message().flags.set(MessageFlag::HighlightedWhisper, true); - } - - return this->release(); -} - -void IrcMessageBuilder::addWords(const QStringList &words) -{ - MessageColor defaultColorType = this->textColor_; - auto defaultColor = defaultColorType.getColor(*getApp()->themes); - QColor textColor = defaultColor; - int fg = -1; - int bg = -1; - - for (auto word : words) - { - if (word.isEmpty()) - { - continue; - } - - auto string = QString(word); - - // Actually just text - auto linkString = this->matchLink(string); - auto link = Link(); - - if (!linkString.isEmpty()) - { - this->addLink(string, linkString); - continue; - } - - // Does the word contain a color changer? If so, split on it. - // Add color indicators, then combine into the same word with the color being changed - - auto i = IRC_COLOR_PARSE_REGEX.globalMatch(string); - - if (!i.hasNext()) - { - this->addText(string, textColor); - continue; - } - - int lastPos = 0; - - while (i.hasNext()) - { - auto match = i.next(); - - if (lastPos != match.capturedStart() && match.capturedStart() != 0) - { - if (fg >= 0 && fg <= 98) - { - textColor = IRC_COLORS[fg]; - getApp()->themes->normalizeColor(textColor); - } - else - { - textColor = defaultColor; - } - this->addText( - string.mid(lastPos, match.capturedStart() - lastPos), - textColor, false); - lastPos = match.capturedStart() + match.capturedLength(); - } - if (!match.captured(1).isEmpty()) - { - fg = -1; - bg = -1; - } - - if (!match.captured(2).isEmpty()) - { - fg = match.captured(2).toInt(nullptr); - } - else - { - fg = -1; - } - if (!match.captured(4).isEmpty()) - { - bg = match.captured(4).toInt(nullptr); - } - else if (fg == -1) - { - bg = -1; - } - - lastPos = match.capturedStart() + match.capturedLength(); - } - - if (fg >= 0 && fg <= 98) - { - textColor = IRC_COLORS[fg]; - getApp()->themes->normalizeColor(textColor); - } - else - { - textColor = defaultColor; - } - this->addText(string.mid(lastPos), textColor); - } - - this->message().elements.back()->setTrailingSpace(false); -} - -void IrcMessageBuilder::addText(const QString &text, const QColor &color, - bool addSpace) -{ - this->textColor_ = color; - for (auto &variant : getApp()->emotes->emojis.parse(text)) - { - boost::apply_visitor( - [&](auto &&arg) { - this->addTextOrEmoji(arg); - }, - variant); - if (!addSpace) - { - this->message().elements.back()->setTrailingSpace(false); - } - } -} - -void IrcMessageBuilder::appendUsername() -{ - QString username = this->userName; - this->message().loginName = username; - this->message().displayName = username; - - // The full string that will be rendered in the chat widget - QString usernameText = username; - - if (this->args.isReceivedWhisper) - { - this->emplace(usernameText, MessageElementFlag::Username, - this->usernameColor_, - FontStyle::ChatMediumBold) - ->setLink({Link::UserWhisper, this->message().displayName}); - - // Separator - this->emplace("->", MessageElementFlag::Username, - MessageColor::System, FontStyle::ChatMedium); - - this->emplace("you:", MessageElementFlag::Username); - } - else - { - if (!this->action_) - { - usernameText += ":"; - } - this->emplace(usernameText, MessageElementFlag::Username, - this->usernameColor_, - FontStyle::ChatMediumBold) - ->setLink({Link::UserInfo, this->message().loginName}); - } -} - -} // namespace chatterino diff --git a/src/providers/irc/IrcMessageBuilder.hpp b/src/providers/irc/IrcMessageBuilder.hpp deleted file mode 100644 index 82f295b32..000000000 --- a/src/providers/irc/IrcMessageBuilder.hpp +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once - -#include "common/Aliases.hpp" -#include "common/Outcome.hpp" -#include "messages/SharedMessageBuilder.hpp" -#include "providers/twitch/TwitchBadge.hpp" - -#include -#include -#include - -namespace chatterino { - -struct Emote; -using EmotePtr = std::shared_ptr; - -class Channel; -class TwitchChannel; - -class IrcMessageBuilder : public SharedMessageBuilder -{ -public: - IrcMessageBuilder() = delete; - - explicit IrcMessageBuilder(Channel *_channel, - const Communi::IrcPrivateMessage *_ircMessage, - const MessageParseArgs &_args); - explicit IrcMessageBuilder(Channel *_channel, - const Communi::IrcMessage *_ircMessage, - const MessageParseArgs &_args, QString content, - bool isAction); - - /** - * @brief used for global notice messages (i.e. notice messages without a channel as its target) - **/ - explicit IrcMessageBuilder(const Communi::IrcNoticeMessage *_ircMessage, - const MessageParseArgs &_args); - - MessagePtr build() override; - -private: - void appendUsername(); - - void addWords(const QStringList &words); - void addText(const QString &text, const QColor &color, - bool addSpace = true); -}; - -} // namespace chatterino diff --git a/src/providers/irc/IrcServer.cpp b/src/providers/irc/IrcServer.cpp deleted file mode 100644 index a6fc8efde..000000000 --- a/src/providers/irc/IrcServer.cpp +++ /dev/null @@ -1,294 +0,0 @@ -#include "IrcServer.hpp" - -#include -#include - -#include "common/QLogging.hpp" -#include "messages/Message.hpp" -#include "providers/irc/Irc2.hpp" -#include "providers/irc/IrcChannel2.hpp" -#include "providers/irc/IrcMessageBuilder.hpp" -#include "providers/twitch/TwitchIrcServer.hpp" // NOTE: Included to access the mentions channel -#include "singletons/Settings.hpp" -#include "util/IrcHelpers.hpp" -#include "util/QObjectRef.hpp" - -#include - -namespace chatterino { - -IrcServer::IrcServer(const IrcServerData &data) - : data_(new IrcServerData(data)) -{ - this->initializeIrc(); - - this->connect(); -} - -IrcServer::IrcServer(const IrcServerData &data, - const std::vector> &restoreChannels) - : IrcServer(data) -{ - for (auto &&weak : restoreChannels) - { - if (auto shared = weak.lock()) - { - this->channels[shared->getName()] = weak; - } - } -} - -IrcServer::~IrcServer() -{ - delete this->data_; -} - -int IrcServer::id() -{ - return this->data_->id; -} - -const QString &IrcServer::user() -{ - return this->data_->user; -} - -const QString &IrcServer::nick() -{ - return this->data_->nick.isEmpty() ? this->data_->user : this->data_->nick; -} - -const QString &IrcServer::userFriendlyIdentifier() -{ - return this->data_->host; -} - -void IrcServer::initializeConnectionSignals(IrcConnection *connection, - ConnectionType type) -{ - assert(type == Both); - - QObject::connect( - connection, &Communi::IrcConnection::socketError, this, - [this](QAbstractSocket::SocketError error) { - static int index = - QAbstractSocket::staticMetaObject.indexOfEnumerator( - "SocketError"); - - std::lock_guard lock(this->channelMutex); - - for (auto &&weak : this->channels) - { - if (auto shared = weak.lock()) - { - shared->addMessage(makeSystemMessage( - QStringLiteral("Socket error: ") + - QAbstractSocket::staticMetaObject.enumerator(index) - .valueToKey(error))); - } - } - }); - - QObject::connect(connection, &Communi::IrcConnection::nickNameRequired, - this, [](const QString &reserved, QString *result) { - *result = reserved + (std::rand() % 100); - }); - - QObject::connect(connection, &Communi::IrcConnection::noticeMessageReceived, - this, [this](Communi::IrcNoticeMessage *message) { - MessageParseArgs args; - args.isReceivedWhisper = true; - - IrcMessageBuilder builder(message, args); - - auto msg = builder.build(); - - for (auto &&weak : this->channels) - { - if (auto shared = weak.lock()) - { - shared->addMessage(msg); - } - } - }); -} - -void IrcServer::initializeConnection(IrcConnection *connection, - ConnectionType type) -{ - assert(type == Both); - - connection->setSecure(this->data_->ssl); - connection->setHost(this->data_->host); - connection->setPort(this->data_->port); - - connection->setUserName(this->data_->user); - connection->setNickName(this->data_->nick.isEmpty() ? this->data_->user - : this->data_->nick); - connection->setRealName(this->data_->real.isEmpty() ? this->data_->user - : this->data_->nick); - - if (getSettings()->enableExperimentalIrc) - { - switch (this->data_->authType) - { - case IrcAuthType::Sasl: - connection->setSaslMechanism("PLAIN"); - [[fallthrough]]; - case IrcAuthType::Pass: - this->data_->getPassword( - this, [conn = new QObjectRef(connection) /* can't copy */, - this](const QString &password) mutable { - if (*conn) - { - (*conn)->setPassword(password); - this->open(Both); - } - - delete conn; - }); - break; - default: - this->open(Both); - } - } -} - -std::shared_ptr IrcServer::createChannel(const QString &channelName) -{ - return std::make_shared(channelName, this); -} - -bool IrcServer::hasSeparateWriteConnection() const -{ - return false; -} - -void IrcServer::onReadConnected(IrcConnection *connection) -{ - { - std::lock_guard lock(this->channelMutex); - - for (auto &&command : this->data_->connectCommands) - { - connection->sendRaw(command + "\r\n"); - } - } - - AbstractIrcServer::onReadConnected(connection); -} - -void IrcServer::privateMessageReceived(Communi::IrcPrivateMessage *message) -{ - auto target = message->target(); - target = target.startsWith('#') ? target.mid(1) : target; - - if (auto channel = this->getChannelOrEmpty(target); !channel->isEmpty()) - { - MessageParseArgs args; - IrcMessageBuilder builder(channel.get(), message, args); - - if (!builder.isIgnored()) - { - auto msg = builder.build(); - - channel->addMessage(msg); - builder.triggerHighlights(); - const auto highlighted = msg->flags.has(MessageFlag::Highlighted); - const auto showInMentions = - msg->flags.has(MessageFlag::ShowInMentions); - - if (highlighted && showInMentions) - { - getApp()->twitch->mentionsChannel->addMessage(msg); - } - } - else - { - qCDebug(chatterinoIrc) << "message ignored :rage:"; - } - } -} - -void IrcServer::readConnectionMessageReceived(Communi::IrcMessage *message) -{ - AbstractIrcServer::readConnectionMessageReceived(message); - - switch (message->type()) - { - case Communi::IrcMessage::Join: { - auto x = static_cast(message); - - if (auto it = - this->channels.find(this->cleanChannelName(x->channel())); - it != this->channels.end()) - { - if (auto shared = it->lock()) - { - if (message->nick() == this->data_->nick) - { - shared->addMessage(makeSystemMessage("joined")); - } - else - { - if (auto c = - dynamic_cast(shared.get())) - c->addJoinedUser(x->nick()); - } - } - } - return; - } - - case Communi::IrcMessage::Part: { - auto x = static_cast(message); - - if (auto it = - this->channels.find(this->cleanChannelName(x->channel())); - it != this->channels.end()) - { - if (auto shared = it->lock()) - { - if (message->nick() == this->data_->nick) - { - shared->addMessage(makeSystemMessage("parted")); - } - else - { - if (auto c = - dynamic_cast(shared.get())) - c->addPartedUser(x->nick()); - } - } - } - return; - } - - case Communi::IrcMessage::Pong: - case Communi::IrcMessage::Notice: - case Communi::IrcMessage::Private: - return; - - default: - if (getSettings()->showUnhandledIrcMessages) - { - MessageBuilder builder; - - builder.emplace( - calculateMessageTime(message).time()); - builder.emplace(message->toData(), - MessageElementFlag::Text); - builder->flags.set(MessageFlag::Debug); - - auto msg = builder.release(); - - for (auto &&weak : this->channels) - { - if (auto shared = weak.lock()) - shared->addMessage(msg); - } - }; - } -} - -} // namespace chatterino diff --git a/src/providers/irc/IrcServer.hpp b/src/providers/irc/IrcServer.hpp deleted file mode 100644 index 9ebc8b902..000000000 --- a/src/providers/irc/IrcServer.hpp +++ /dev/null @@ -1,41 +0,0 @@ -#pragma once - -#include "providers/irc/AbstractIrcServer.hpp" -#include "providers/irc/IrcAccount.hpp" - -namespace chatterino { - -struct IrcServerData; - -class IrcServer : public AbstractIrcServer -{ -public: - explicit IrcServer(const IrcServerData &data); - IrcServer(const IrcServerData &data, - const std::vector> &restoreChannels); - ~IrcServer() override; - - int id(); - const QString &user(); - const QString &nick(); - const QString &userFriendlyIdentifier(); - - // AbstractIrcServer interface -protected: - void initializeConnectionSignals(IrcConnection *connection, - ConnectionType type) override; - void initializeConnection(IrcConnection *connection, - ConnectionType type) override; - std::shared_ptr createChannel(const QString &channelName) override; - bool hasSeparateWriteConnection() const override; - - void onReadConnected(IrcConnection *connection) override; - void privateMessageReceived(Communi::IrcPrivateMessage *message) override; - void readConnectionMessageReceived(Communi::IrcMessage *message) override; - -private: - // pointer so we don't have to circle include Irc2.hpp - IrcServerData *data_; -}; - -} // namespace chatterino diff --git a/src/providers/links/LinkInfo.cpp b/src/providers/links/LinkInfo.cpp new file mode 100644 index 000000000..ce03b9746 --- /dev/null +++ b/src/providers/links/LinkInfo.cpp @@ -0,0 +1,106 @@ +#include "providers/links/LinkInfo.hpp" + +#include "debug/AssertInGuiThread.hpp" + +#include + +namespace chatterino { + +LinkInfo::LinkInfo(QString url) + : QObject(nullptr) + , originalUrl_(url) + , url_(std::move(url)) + , tooltip_(this->url_) +{ +} + +LinkInfo::~LinkInfo() = default; + +LinkInfo::State LinkInfo::state() const +{ + return this->state_; +} + +QString LinkInfo::url() const +{ + return this->url_; +} + +QString LinkInfo::originalUrl() const +{ + return this->originalUrl_; +} + +bool LinkInfo::isPending() const +{ + return this->state_ == State::Created; +} + +bool LinkInfo::isLoading() const +{ + return this->state_ == State::Loading; +} + +bool LinkInfo::isLoaded() const +{ + return this->state_ > State::Loading; +} + +bool LinkInfo::isResolved() const +{ + return this->state_ == State::Resolved; +} + +bool LinkInfo::hasError() const +{ + return this->state_ == State::Errored; +} + +bool LinkInfo::hasThumbnail() const +{ + return this->thumbnail_ && !this->thumbnail_->url().string.isEmpty(); +} + +QString LinkInfo::tooltip() const +{ + return this->tooltip_; +} + +ImagePtr LinkInfo::thumbnail() const +{ + return this->thumbnail_; +} + +void LinkInfo::setState(State state) +{ + assertInGuiThread(); + assert(state >= this->state_); + + if (this->state_ == state) + { + return; + } + + this->state_ = state; + this->stateChanged(state); +} + +void LinkInfo::setResolvedUrl(QString resolvedUrl) +{ + assertInGuiThread(); + this->url_ = std::move(resolvedUrl); +} + +void LinkInfo::setTooltip(QString tooltip) +{ + assertInGuiThread(); + this->tooltip_ = std::move(tooltip); +} + +void LinkInfo::setThumbnail(ImagePtr thumbnail) +{ + assertInGuiThread(); + this->thumbnail_ = std::move(thumbnail); +} + +} // namespace chatterino diff --git a/src/providers/links/LinkInfo.hpp b/src/providers/links/LinkInfo.hpp new file mode 100644 index 000000000..1d1c24b44 --- /dev/null +++ b/src/providers/links/LinkInfo.hpp @@ -0,0 +1,135 @@ +#pragma once + +#include "messages/Image.hpp" + +namespace chatterino { + +/// @brief Rich info about a URL with tooltip and thumbnail +/// +/// This is only a data class - it doesn't do the resolving. +/// It can only be used from the GUI thread. +class LinkInfo : public QObject +{ + Q_OBJECT + +public: + /// @brief the state of a link info + /// + /// The state of a link can only increase. For example, it's not possible + /// for the link to change from "Resolved" to "Loading". + enum class State { + /// @brief The object was created, no info is resolved + /// + /// This is the initial state + Created, + /// Info is currently loading + Loading, + /// Info has been resolved and the properties have been updated + Resolved, + /// There has been an error resolving the link info (e.g. timeout) + Errored, + }; + + /// @brief Constructs a new link info for a URL + /// + /// This doesn't load any link info. + /// @see #ensureLoadingStarted() + [[nodiscard]] explicit LinkInfo(QString url); + + ~LinkInfo() override; + + LinkInfo(const LinkInfo &) = delete; + LinkInfo(LinkInfo &&) = delete; + LinkInfo &operator=(const LinkInfo &) = delete; + LinkInfo &operator=(LinkInfo &&) = delete; + + /// @brief The URL of this link + /// + /// If the "unshortLinks" setting is enabled, this can change after the + /// link is resolved. + [[nodiscard]] QString url() const; + + /// @brief The URL of this link as seen in the message + /// + /// If the "unshortLinks" setting doesn't affect this URL. + [[nodiscard]] QString originalUrl() const; + + /// Returns the current state + [[nodiscard]] State state() const; + + /// Returns true if this link has not yet been resolved (it's "Created") + [[nodiscard]] bool isPending() const; + + /// Returns true if the info is loading + [[nodiscard]] bool isLoading() const; + + /// Returns true if the info is loaded (resolved or errored) + [[nodiscard]] bool isLoaded() const; + + /// Returns true if this link has been resolved + [[nodiscard]] bool isResolved() const; + + /// Returns true if the info failed to resolve + [[nodiscard]] bool hasError() const; + + /// Returns true if this link has a thumbnail + [[nodiscard]] bool hasThumbnail() const; + + /// @brief Returns the tooltip of this link + /// + /// The tooltip contains the URL of the link and any info added by the + /// resolver. Resolvers must include the URL. + [[nodiscard]] QString tooltip() const; + + /// @brief Returns the thumbnail of this link + /// + /// The thumbnail is provided by the resolver and might not have been + /// loaded yet. + /// + /// @pre The caller must check #hasThumbnail() before calling this method + [[nodiscard]] ImagePtr thumbnail() const; + + /// @brief Updates the state and emits #stateChanged accordingly + /// + /// @pre The caller must be in the GUI thread. + /// @pre @a state must be greater or equal to the current state. + /// @see #state(), #stateChanged + void setState(State state); + + /// @brief Updates the resolved url of this link + /// + /// @pre The caller must be in the GUI thread. + /// @see #url() + void setResolvedUrl(QString resolvedUrl); + + /// @brief Updates the tooltip of this link + /// + /// @pre The caller must be in the GUI thread. + /// @see #tooltip() + void setTooltip(QString tooltip); + + /// @brief Updates the thumbnail of this link + /// + /// The thumbnail is allowed to be empty or nullptr. + /// + /// @pre The caller must be in the GUI thread. + /// @see #hasThumbnail(), #thumbnail() + void setThumbnail(ImagePtr thumbnail); + +signals: + /// @brief Emitted when this link's state changes + /// + /// @param state The new state + void stateChanged(State state); + +private: + const QString originalUrl_; + QString url_; + + QString tooltip_; + ImagePtr thumbnail_; + + State state_ = State::Created; +}; + +} // namespace chatterino diff --git a/src/providers/links/LinkResolver.cpp b/src/providers/links/LinkResolver.cpp new file mode 100644 index 000000000..6026aa6a5 --- /dev/null +++ b/src/providers/links/LinkResolver.cpp @@ -0,0 +1,72 @@ +#include "providers/links/LinkResolver.hpp" + +#include "common/Env.hpp" +#include "common/network/NetworkRequest.hpp" +#include "common/network/NetworkResult.hpp" +#include "providers/links/LinkInfo.hpp" +#include "singletons/Settings.hpp" + +#include + +namespace chatterino { + +void LinkResolver::resolve(LinkInfo *info) +{ + using State = LinkInfo::State; + + assert(info); + + if (info->state() != State::Created) + { + // The link is already resolved or is currently loading + return; + } + + if (!getSettings()->linkInfoTooltip) + { + return; + } + + info->setTooltip("Loading..."); + info->setState(State::Loading); + + NetworkRequest(Env::get().linkResolverUrl.arg(QString::fromUtf8( + QUrl::toPercentEncoding(info->originalUrl(), {}, "/:")))) + .caller(info) + .timeout(30000) + .onSuccess([info](const NetworkResult &result) { + const auto root = result.parseJson(); + QString response; + QString url; + ImagePtr thumbnail = nullptr; + if (root["status"].toInt() == 200) + { + response = root["tooltip"].toString(); + + if (root.contains("thumbnail")) + { + info->setThumbnail( + Image::fromUrl({root["thumbnail"].toString()})); + } + if (getSettings()->unshortLinks && root.contains("link")) + { + info->setResolvedUrl(root["link"].toString()); + } + } + else + { + response = root["message"].toString(); + } + + info->setTooltip(QUrl::fromPercentEncoding(response.toUtf8())); + info->setState(State::Resolved); + }) + .onError([info](const auto &result) { + info->setTooltip(u"No link info found (" % result.formatError() % + u')'); + info->setState(State::Errored); + }) + .execute(); +} + +} // namespace chatterino diff --git a/src/providers/links/LinkResolver.hpp b/src/providers/links/LinkResolver.hpp new file mode 100644 index 000000000..c5cee79ef --- /dev/null +++ b/src/providers/links/LinkResolver.hpp @@ -0,0 +1,36 @@ +#pragma once + +namespace chatterino { + +class LinkInfo; + +class ILinkResolver +{ +public: + ILinkResolver() = default; + virtual ~ILinkResolver() = default; + ILinkResolver(const ILinkResolver &) = delete; + ILinkResolver(ILinkResolver &&) = delete; + ILinkResolver &operator=(const ILinkResolver &) = delete; + ILinkResolver &operator=(ILinkResolver &&) = delete; + + virtual void resolve(LinkInfo *info) = 0; +}; + +class LinkResolver : public ILinkResolver +{ +public: + LinkResolver() = default; + + /// @brief Loads and updates the link info + /// + /// Calling this with an already resolved or currently loading info is a + /// no-op. Loading can be blocked by disabling the "linkInfoTooltip" + /// setting. URLs will be unshortened if the "unshortLinks" setting is + /// enabled. The resolver is set through Env::linkResolverUrl. + /// + /// @pre @a info must not be nullptr + void resolve(LinkInfo *info) override; +}; + +} // namespace chatterino diff --git a/src/providers/liveupdates/BasicPubSubClient.hpp b/src/providers/liveupdates/BasicPubSubClient.hpp new file mode 100644 index 000000000..aa65e619a --- /dev/null +++ b/src/providers/liveupdates/BasicPubSubClient.hpp @@ -0,0 +1,189 @@ +#pragma once + +#include "common/QLogging.hpp" +#include "providers/liveupdates/BasicPubSubWebsocket.hpp" +#include "singletons/Settings.hpp" +#include "util/DebugCount.hpp" +#include "util/Helpers.hpp" + +#include + +#include +#include +#include + +namespace chatterino { + +/** + * This class manages a single connection + * that has at most #maxSubscriptions subscriptions. + * + * You can safely overload the #onConnectionEstablished method + * and e.g. add additional heartbeat logic. + * + * You can use shared_from_this to get a shared_ptr of this client. + * + * @tparam Subscription see BasicPubSubManager + */ +template +class BasicPubSubClient + : public std::enable_shared_from_this> +{ +public: + // The maximum amount of subscriptions this connections can handle + const size_t maxSubscriptions; + + BasicPubSubClient(liveupdates::WebsocketClient &websocketClient, + liveupdates::WebsocketHandle handle, + size_t maxSubscriptions = 100) + : maxSubscriptions(maxSubscriptions) + , websocketClient_(websocketClient) + , handle_(std::move(handle)) + { + } + + virtual ~BasicPubSubClient() = default; + + BasicPubSubClient(const BasicPubSubClient &) = delete; + BasicPubSubClient(const BasicPubSubClient &&) = delete; + BasicPubSubClient &operator=(const BasicPubSubClient &) = delete; + BasicPubSubClient &operator=(const BasicPubSubClient &&) = delete; + +protected: + virtual void onConnectionEstablished() + { + } + + bool send(const char *payload) + { + liveupdates::WebsocketErrorCode ec; + this->websocketClient_.send(this->handle_, payload, + websocketpp::frame::opcode::text, ec); + + if (ec) + { + qCDebug(chatterinoLiveupdates) << "Error sending message" << payload + << ":" << ec.message().c_str(); + return false; + } + + return true; + } + + /** + * @return true if this client subscribed to this subscription + * and the current subscriptions don't exceed the maximum + * amount. + * It won't subscribe twice to the same subscription. + * Don't use this in place of subscription management + * in the BasicPubSubManager. + */ + bool subscribe(const Subscription &subscription) + { + if (this->subscriptions_.size() >= this->maxSubscriptions) + { + return false; + } + + if (!this->subscriptions_.emplace(subscription).second) + { + qCWarning(chatterinoLiveupdates) + << "Tried subscribing to" << subscription + << "but we're already subscribed!"; + return true; // true because the subscription already exists + } + + qCDebug(chatterinoLiveupdates) << "Subscribing to" << subscription; + DebugCount::increase("LiveUpdates subscriptions"); + + QByteArray encoded = subscription.encodeSubscribe(); + this->send(encoded); + + return true; + } + + /** + * @return true if this client previously subscribed + * and now unsubscribed from this subscription. + */ + bool unsubscribe(const Subscription &subscription) + { + if (this->subscriptions_.erase(subscription) <= 0) + { + return false; + } + + qCDebug(chatterinoLiveupdates) << "Unsubscribing from" << subscription; + DebugCount::decrease("LiveUpdates subscriptions"); + + QByteArray encoded = subscription.encodeUnsubscribe(); + this->send(encoded); + + return true; + } + + void close(const std::string &reason, + websocketpp::close::status::value code = + websocketpp::close::status::normal) + { + liveupdates::WebsocketErrorCode ec; + + auto conn = this->websocketClient_.get_con_from_hdl(this->handle_, ec); + if (ec) + { + qCDebug(chatterinoLiveupdates) + << "Error getting connection:" << ec.message().c_str(); + return; + } + + conn->close(code, reason, ec); + if (ec) + { + qCDebug(chatterinoLiveupdates) + << "Error closing:" << ec.message().c_str(); + return; + } + } + + bool isStarted() const + { + return this->started_.load(std::memory_order_acquire); + } + + /** + * @brief Will be called when the clients has been requested to stop + * + * Derived classes can override this to implement their own shutdown behaviour + */ + virtual void stopImpl() + { + } + + liveupdates::WebsocketClient &websocketClient_; + +private: + void start() + { + assert(!this->isStarted()); + this->started_.store(true, std::memory_order_release); + this->onConnectionEstablished(); + } + + void stop() + { + assert(this->isStarted()); + this->started_.store(false, std::memory_order_release); + + this->stopImpl(); + } + + liveupdates::WebsocketHandle handle_; + std::unordered_set subscriptions_; + + std::atomic started_{false}; + + template + friend class BasicPubSubManager; +}; + +} // namespace chatterino diff --git a/src/providers/liveupdates/BasicPubSubManager.hpp b/src/providers/liveupdates/BasicPubSubManager.hpp new file mode 100644 index 000000000..e8d95b528 --- /dev/null +++ b/src/providers/liveupdates/BasicPubSubManager.hpp @@ -0,0 +1,406 @@ +#pragma once + +#include "common/QLogging.hpp" +#include "common/Version.hpp" +#include "providers/liveupdates/BasicPubSubClient.hpp" +#include "providers/liveupdates/BasicPubSubWebsocket.hpp" +#include "providers/NetworkConfigurationProvider.hpp" +#include "providers/twitch/PubSubHelpers.hpp" +#include "util/DebugCount.hpp" +#include "util/ExponentialBackoff.hpp" +#include "util/RenameThread.hpp" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace chatterino { + +/** + * This class is the basis for connecting and interacting with + * simple PubSub servers over the Websocket protocol. + * It acts as a pool for connections (see BasicPubSubClient). + * + * You can customize the clients, by creating your custom + * client in ::createClient. + * + * You **must** implement #onMessage. The method gets called for every + * received message on every connection. + * If you want to get the connection this message was received on, + * use #findClient. + * + * You must expose your own subscribe and unsubscribe methods + * (e.g. [un-]subscribeTopic). + * This manager does not keep track of the subscriptions. + * + * @tparam Subscription + * The subscription has the following requirements: + * It must have the methods QByteArray encodeSubscribe(), + * and QByteArray encodeUnsubscribe(). + * It must have an overload for + * QDebug &operator<< (see tests/src/BasicPubSub.cpp), + * a specialization for std::hash, + * and and overload for operator== and operator!=. + * + * @see BasicPubSubClient + */ +template +class BasicPubSubManager +{ +public: + BasicPubSubManager(QString host, QString shortName) + : host_(std::move(host)) + , shortName_(std::move(shortName)) + { + this->websocketClient_.set_access_channels( + websocketpp::log::alevel::all); + this->websocketClient_.clear_access_channels( + websocketpp::log::alevel::frame_payload | + websocketpp::log::alevel::frame_header); + + this->websocketClient_.init_asio(); + + // SSL Handshake + this->websocketClient_.set_tls_init_handler([this](auto hdl) { + return this->onTLSInit(hdl); + }); + + this->websocketClient_.set_message_handler([this](auto hdl, auto msg) { + this->onMessage(hdl, msg); + }); + this->websocketClient_.set_open_handler([this](auto hdl) { + this->onConnectionOpen(hdl); + }); + this->websocketClient_.set_close_handler([this](auto hdl) { + this->onConnectionClose(hdl); + }); + this->websocketClient_.set_fail_handler([this](auto hdl) { + this->onConnectionFail(hdl); + }); + this->websocketClient_.set_user_agent( + QStringLiteral("Chatterino/%1 (%2)") + .arg(Version::instance().version(), + Version::instance().commitHash()) + .toStdString()); + } + + virtual ~BasicPubSubManager() + { + // The derived class must call stop in its destructor + assert(this->stopping_); + } + + BasicPubSubManager(const BasicPubSubManager &) = delete; + BasicPubSubManager(const BasicPubSubManager &&) = delete; + BasicPubSubManager &operator=(const BasicPubSubManager &) = delete; + BasicPubSubManager &operator=(const BasicPubSubManager &&) = delete; + + /** This is only used for testing. */ + struct { + std::atomic connectionsClosed{0}; + std::atomic connectionsOpened{0}; + std::atomic connectionsFailed{0}; + } diag; + + void start() + { + this->work_ = std::make_shared( + this->websocketClient_.get_io_service()); + this->mainThread_.reset(new std::thread([this] { + runThread(); + })); + + renameThread(*this->mainThread_.get(), "BPSM-" % this->shortName_); + } + + void stop() + { + if (this->stopping_) + { + return; + } + + this->stopping_ = true; + + for (const auto &client : this->clients_) + { + client.second->close("Shutting down"); + } + + this->work_.reset(); + + if (this->mainThread_->joinable()) + { + // NOTE: We spawn a new thread to join the websocket thread. + // There is a case where a new client was initiated but not added to the clients list. + // We just don't join the thread & let the operating system nuke the thread if joining fails + // within 1s. + auto joiner = std::async(std::launch::async, &std::thread::join, + this->mainThread_.get()); + if (joiner.wait_for(std::chrono::seconds(1)) == + std::future_status::timeout) + { + qCWarning(chatterinoLiveupdates) + << "Thread didn't join within 1 second, rip it out"; + this->websocketClient_.stop(); + } + } + } + +protected: + using WebsocketMessagePtr = + websocketpp::config::asio_tls_client::message_type::ptr; + using WebsocketContextPtr = + websocketpp::lib::shared_ptr; + + virtual void onMessage(websocketpp::connection_hdl hdl, + WebsocketMessagePtr msg) = 0; + + virtual std::shared_ptr> createClient( + liveupdates::WebsocketClient &client, websocketpp::connection_hdl hdl) + { + return std::make_shared>(client, hdl); + } + + /** + * @param hdl The handle of the client. + * @return The client managing this connection, empty shared_ptr otherwise. + */ + std::shared_ptr> findClient( + websocketpp::connection_hdl hdl) + { + auto clientIt = this->clients_.find(hdl); + + if (clientIt == this->clients_.end()) + { + return {}; + } + + return clientIt->second; + } + + void unsubscribe(const Subscription &subscription) + { + for (auto &client : this->clients_) + { + if (client.second->unsubscribe(subscription)) + { + return; + } + } + } + + void subscribe(const Subscription &subscription) + { + if (this->trySubscribe(subscription)) + { + return; + } + + this->addClient(); + this->pendingSubscriptions_.emplace_back(subscription); + DebugCount::increase("LiveUpdates subscription backlog"); + } + +private: + void onConnectionOpen(websocketpp::connection_hdl hdl) + { + DebugCount::increase("LiveUpdates connections"); + this->addingClient_ = false; + this->diag.connectionsOpened.fetch_add(1, std::memory_order_acq_rel); + + this->connectBackoff_.reset(); + + auto client = this->createClient(this->websocketClient_, hdl); + + // We separate the starting from the constructor because we will want to use + // shared_from_this + client->start(); + + this->clients_.emplace(hdl, client); + + auto pendingSubsToTake = std::min(this->pendingSubscriptions_.size(), + client->maxSubscriptions); + + qCDebug(chatterinoLiveupdates) + << "LiveUpdate connection opened, subscribing to" + << pendingSubsToTake << "subscriptions!"; + + while (pendingSubsToTake > 0 && !this->pendingSubscriptions_.empty()) + { + const auto last = std::move(this->pendingSubscriptions_.back()); + this->pendingSubscriptions_.pop_back(); + if (!client->subscribe(last)) + { + qCDebug(chatterinoLiveupdates) + << "Failed to subscribe to" << last << "on new client."; + // TODO: should we try to add a new client here? + return; + } + DebugCount::decrease("LiveUpdates subscription backlog"); + pendingSubsToTake--; + } + + if (!this->pendingSubscriptions_.empty()) + { + this->addClient(); + } + } + + void onConnectionFail(websocketpp::connection_hdl hdl) + { + DebugCount::increase("LiveUpdates failed connections"); + this->diag.connectionsFailed.fetch_add(1, std::memory_order_acq_rel); + + if (auto conn = this->websocketClient_.get_con_from_hdl(std::move(hdl))) + { + qCDebug(chatterinoLiveupdates) + << "LiveUpdates connection attempt failed (error: " + << conn->get_ec().message().c_str() << ")"; + } + else + { + qCDebug(chatterinoLiveupdates) + << "LiveUpdates connection attempt failed but we can't get the " + "connection from a handle."; + } + this->addingClient_ = false; + if (!this->pendingSubscriptions_.empty()) + { + runAfter(this->websocketClient_.get_io_service(), + this->connectBackoff_.next(), [this](auto /*timer*/) { + this->addClient(); + }); + } + } + + void onConnectionClose(websocketpp::connection_hdl hdl) + { + qCDebug(chatterinoLiveupdates) << "Connection closed"; + DebugCount::decrease("LiveUpdates connections"); + this->diag.connectionsClosed.fetch_add(1, std::memory_order_acq_rel); + + auto clientIt = this->clients_.find(hdl); + + // If this assert goes off, there's something wrong with the connection + // creation/preserving code KKona + assert(clientIt != this->clients_.end()); + + auto client = clientIt->second; + + this->clients_.erase(clientIt); + + client->stop(); + + if (!this->stopping_) + { + for (const auto &sub : client->subscriptions_) + { + this->subscribe(sub); + } + } + } + + WebsocketContextPtr onTLSInit(const websocketpp::connection_hdl & /*hdl*/) + { + WebsocketContextPtr ctx( + new boost::asio::ssl::context(boost::asio::ssl::context::tlsv12)); + + try + { + ctx->set_options(boost::asio::ssl::context::default_workarounds | + boost::asio::ssl::context::no_sslv2 | + boost::asio::ssl::context::single_dh_use); + } + catch (const std::exception &e) + { + qCDebug(chatterinoLiveupdates) + << "Exception caught in OnTLSInit:" << e.what(); + } + + return ctx; + } + + void runThread() + { + qCDebug(chatterinoLiveupdates) << "Start LiveUpdates manager thread"; + this->websocketClient_.run(); + qCDebug(chatterinoLiveupdates) + << "Done with LiveUpdates manager thread"; + } + + void addClient() + { + if (this->addingClient_) + { + return; + } + + qCDebug(chatterinoLiveupdates) << "Adding an additional client"; + + this->addingClient_ = true; + + websocketpp::lib::error_code ec; + auto con = this->websocketClient_.get_connection( + this->host_.toStdString(), ec); + + if (ec) + { + qCDebug(chatterinoLiveupdates) + << "Unable to establish connection:" << ec.message().c_str(); + return; + } + + NetworkConfigurationProvider::applyToWebSocket(con); + + this->websocketClient_.connect(con); + } + + bool trySubscribe(const Subscription &subscription) + { + for (auto &client : this->clients_) + { + if (client.second->subscribe(subscription)) + { + return true; + } + } + return false; + } + + std::map>, + std::owner_less> + clients_; + + std::vector pendingSubscriptions_; + std::atomic addingClient_{false}; + ExponentialBackoff<5> connectBackoff_{std::chrono::milliseconds(1000)}; + + std::shared_ptr work_{nullptr}; + + liveupdates::WebsocketClient websocketClient_; + std::unique_ptr mainThread_; + + const QString host_; + + /// Short name of the service (e.g. "7TV" or "BTTV") + const QString shortName_; + + bool stopping_{false}; +}; + +} // namespace chatterino diff --git a/src/providers/liveupdates/BasicPubSubWebsocket.hpp b/src/providers/liveupdates/BasicPubSubWebsocket.hpp new file mode 100644 index 000000000..bf30c30ca --- /dev/null +++ b/src/providers/liveupdates/BasicPubSubWebsocket.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include "providers/twitch/ChatterinoWebSocketppLogger.hpp" + +#include +#include +#include +#include + +namespace chatterino { + +struct BasicPubSubConfig : public websocketpp::config::asio_tls_client { + // NOLINTBEGIN(modernize-use-using) + typedef websocketpp::log::chatterinowebsocketpplogger< + concurrency_type, websocketpp::log::elevel> + elog_type; + typedef websocketpp::log::chatterinowebsocketpplogger< + concurrency_type, websocketpp::log::alevel> + alog_type; + + struct PerMessageDeflateConfig { + }; + + typedef websocketpp::extensions::permessage_deflate::disabled< + PerMessageDeflateConfig> + permessage_deflate_type; + // NOLINTEND(modernize-use-using) +}; + +namespace liveupdates { + using WebsocketClient = websocketpp::client; + using WebsocketHandle = websocketpp::connection_hdl; + using WebsocketErrorCode = websocketpp::lib::error_code; +} // namespace liveupdates + +} // namespace chatterino diff --git a/src/providers/recentmessages/Api.cpp b/src/providers/recentmessages/Api.cpp new file mode 100644 index 000000000..855e31f47 --- /dev/null +++ b/src/providers/recentmessages/Api.cpp @@ -0,0 +1,100 @@ +#include "providers/recentmessages/Api.hpp" + +#include "common/network/NetworkRequest.hpp" +#include "common/network/NetworkResult.hpp" +#include "common/QLogging.hpp" +#include "providers/recentmessages/Impl.hpp" +#include "util/PostToThread.hpp" + +namespace { + +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +const auto &LOG = chatterinoRecentMessages; + +} // namespace + +namespace chatterino::recentmessages { + +using namespace recentmessages::detail; + +void load( + const QString &channelName, std::weak_ptr channelPtr, + ResultCallback onLoaded, ErrorCallback onError, const int limit, + const std::optional> + after, + const std::optional> + before, + const bool jitter) +{ + qCDebug(LOG) << "Loading recent messages for" << channelName; + + const auto url = + constructRecentMessagesUrl(channelName, limit, after, before); + + const long delayMs = jitter ? std::rand() % 100 : 0; + QTimer::singleShot(delayMs, [=] { + NetworkRequest(url) + .onSuccess([channelPtr, onLoaded](const auto &result) { + auto shared = channelPtr.lock(); + if (!shared) + { + return; + } + + qCDebug(LOG) << "Successfully loaded recent messages for" + << shared->getName(); + + auto root = result.parseJson(); + auto parsedMessages = parseRecentMessages(root); + + // build the Communi messages into chatterino messages + auto builtMessages = + buildRecentMessages(parsedMessages, shared.get()); + + postToThread([shared = std::move(shared), + root = std::move(root), + messages = std::move(builtMessages), + onLoaded]() mutable { + // Notify user about a possible gap in logs if it returned some messages + // but isn't currently joined to a channel + const auto errorCode = root.value("error_code").toString(); + if (!errorCode.isEmpty()) + { + qCDebug(LOG) + << QString("Got error from API: error_code=%1, " + "channel=%2") + .arg(errorCode, shared->getName()); + if (errorCode == "channel_not_joined" && + !messages.empty()) + { + shared->addSystemMessage( + "Message history service recovering, there may " + "be gaps in the message history."); + } + } + + onLoaded(messages); + }); + }) + .onError([channelPtr, onError](const NetworkResult &result) { + auto shared = channelPtr.lock(); + if (!shared) + { + return; + } + + qCDebug(LOG) << "Failed to load recent messages for" + << shared->getName(); + + shared->addSystemMessage( + QStringLiteral( + "Message history service unavailable (Error: %1)") + .arg(result.formatError())); + + onError(); + }) + .execute(); + }); +} + +} // namespace chatterino::recentmessages diff --git a/src/providers/recentmessages/Api.hpp b/src/providers/recentmessages/Api.hpp new file mode 100644 index 000000000..57193be1f --- /dev/null +++ b/src/providers/recentmessages/Api.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include + +namespace chatterino { + +class Channel; +using ChannelPtr = std::shared_ptr; + +struct Message; +using MessagePtr = std::shared_ptr; + +} // namespace chatterino + +namespace chatterino::recentmessages { + +using ResultCallback = std::function &)>; +using ErrorCallback = std::function; + +/** + * @brief Loads recent messages for a channel using the Recent Messages API + * + * @param channelName Name of Twitch channel + * @param channelPtr Weak pointer to Channel to use to build messages + * @param onLoaded Callback taking the built messages as a const std::vector & + * @param onError Callback called when the network request fails + * @param limit Maximum number of messages to query + * @param after Only return messages that were received after this timestamp; ignored if `std::nullopt` + * @param before Only return messages that were received before this timestamp; ignored if `std::nullopt` + * @param jitter Whether to delay the request by a small random duration + */ +void load( + const QString &channelName, std::weak_ptr channelPtr, + ResultCallback onLoaded, ErrorCallback onError, int limit, + std::optional> after, + std::optional> before, + bool jitter); + +} // namespace chatterino::recentmessages diff --git a/src/providers/recentmessages/Impl.cpp b/src/providers/recentmessages/Impl.cpp new file mode 100644 index 000000000..6971b5547 --- /dev/null +++ b/src/providers/recentmessages/Impl.cpp @@ -0,0 +1,129 @@ +#include "providers/recentmessages/Impl.hpp" + +#include "common/Env.hpp" +#include "common/QLogging.hpp" +#include "messages/MessageBuilder.hpp" +#include "providers/twitch/IrcMessageHandler.hpp" +#include "providers/twitch/TwitchChannel.hpp" +#include "util/FormatTime.hpp" + +#include +#include + +namespace { + +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +const auto &LOG = chatterinoRecentMessages; + +} // namespace + +namespace chatterino::recentmessages::detail { + +// Parse the IRC messages returned in JSON form into Communi messages +std::vector parseRecentMessages( + const QJsonObject &jsonRoot) +{ + const auto jsonMessages = jsonRoot.value("messages").toArray(); + std::vector messages; + + if (jsonMessages.empty()) + { + return messages; + } + + for (const auto &jsonMessage : jsonMessages) + { + auto content = jsonMessage.toString(); + + // For explanation of why this exists, see src/providers/twitch/TwitchChannel.hpp, + // where these constants are defined + content.replace(COMBINED_FIXER, ZERO_WIDTH_JOINER); + + auto *message = + Communi::IrcMessage::fromData(content.toUtf8(), nullptr); + + messages.emplace_back(message); + } + + return messages; +} + +// Build Communi messages retrieved from the recent messages API into +// proper chatterino messages. +std::vector buildRecentMessages( + std::vector &messages, Channel *channel) +{ + std::vector allBuiltMessages; + + for (auto *message : messages) + { + if (message->tags().contains("rm-received-ts")) + { + const auto msgDate = + QDateTime::fromMSecsSinceEpoch( + message->tags().value("rm-received-ts").toLongLong()) + .date(); + + // Check if we need to insert a message stating that a new day began + if (msgDate != channel->lastDate_) + { + channel->lastDate_ = msgDate; + auto msg = makeSystemMessage( + QLocale().toString(msgDate, QLocale::LongFormat), + QTime(0, 0)); + msg->flags.set(MessageFlag::RecentMessage); + allBuiltMessages.emplace_back(msg); + } + } + + auto builtMessages = IrcMessageHandler::parseMessageWithReply( + channel, message, allBuiltMessages); + + for (const auto &builtMessage : builtMessages) + { + builtMessage->flags.set(MessageFlag::RecentMessage); + allBuiltMessages.emplace_back(builtMessage); + } + + message->deleteLater(); + } + + return allBuiltMessages; +} + +// Returns the URL to be used for querying the Recent Messages API for the +// given channel. +QUrl constructRecentMessagesUrl( + const QString &name, const int limit, + const std::optional> + after, + const std::optional> + before) +{ + QUrl url(Env::get().recentMessagesApiUrl.arg(name)); + QUrlQuery urlQuery(url); + if (!urlQuery.hasQueryItem("limit")) + { + urlQuery.addQueryItem("limit", QString::number(limit)); + } + if (after.has_value()) + { + urlQuery.addQueryItem( + "after", QString::number( + std::chrono::duration_cast( + after->time_since_epoch()) + .count())); + } + if (before.has_value()) + { + urlQuery.addQueryItem( + "before", QString::number( + std::chrono::duration_cast( + before->time_since_epoch()) + .count())); + } + url.setQuery(urlQuery); + return url; +} + +} // namespace chatterino::recentmessages::detail diff --git a/src/providers/recentmessages/Impl.hpp b/src/providers/recentmessages/Impl.hpp new file mode 100644 index 000000000..3c60b6c3d --- /dev/null +++ b/src/providers/recentmessages/Impl.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include "common/Channel.hpp" +#include "messages/Message.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace chatterino::recentmessages::detail { + +// Parse the IRC messages returned in JSON form into Communi messages +std::vector parseRecentMessages( + const QJsonObject &jsonRoot); + +// Build Communi messages retrieved from the recent messages API into +// proper chatterino messages. +std::vector buildRecentMessages( + std::vector &messages, Channel *channel); + +// Returns the URL to be used for querying the Recent Messages API for the +// given channel. +QUrl constructRecentMessagesUrl( + const QString &name, int limit, + std::optional> after, + std::optional> before); + +} // namespace chatterino::recentmessages::detail diff --git a/src/providers/seventv/SeventvAPI.cpp b/src/providers/seventv/SeventvAPI.cpp new file mode 100644 index 000000000..dd7a1375f --- /dev/null +++ b/src/providers/seventv/SeventvAPI.cpp @@ -0,0 +1,82 @@ +#include "providers/seventv/SeventvAPI.hpp" + +#include "common/Literals.hpp" +#include "common/network/NetworkRequest.hpp" +#include "common/network/NetworkResult.hpp" + +namespace { + +using namespace chatterino::literals; + +const QString API_URL_USER = u"https://7tv.io/v3/users/twitch/%1"_s; +const QString API_URL_EMOTE_SET = u"https://7tv.io/v3/emote-sets/%1"_s; +const QString API_URL_PRESENCES = u"https://7tv.io/v3/users/%1/presences"_s; + +} // namespace + +// NOLINTBEGIN(readability-convert-member-functions-to-static) +namespace chatterino { + +void SeventvAPI::getUserByTwitchID( + const QString &twitchID, SuccessCallback &&onSuccess, + ErrorCallback &&onError) +{ + NetworkRequest(API_URL_USER.arg(twitchID), NetworkRequestType::Get) + .timeout(20000) + .onSuccess( + [callback = std::move(onSuccess)](const NetworkResult &result) { + auto json = result.parseJson(); + callback(json); + }) + .onError([callback = std::move(onError)](const NetworkResult &result) { + callback(result); + }) + .execute(); +} + +void SeventvAPI::getEmoteSet(const QString &emoteSet, + SuccessCallback &&onSuccess, + ErrorCallback &&onError) +{ + NetworkRequest(API_URL_EMOTE_SET.arg(emoteSet), NetworkRequestType::Get) + .timeout(25000) + .onSuccess( + [callback = std::move(onSuccess)](const NetworkResult &result) { + auto json = result.parseJson(); + callback(json); + }) + .onError([callback = std::move(onError)](const NetworkResult &result) { + callback(result); + }) + .execute(); +} + +void SeventvAPI::updatePresence(const QString &twitchChannelID, + const QString &seventvUserID, + SuccessCallback<> &&onSuccess, + ErrorCallback &&onError) +{ + QJsonObject payload{ + {u"kind"_s, 1}, // UserPresenceKindChannel + {u"data"_s, + QJsonObject{ + {u"id"_s, twitchChannelID}, + {u"platform"_s, u"TWITCH"_s}, + }}, + }; + + NetworkRequest(API_URL_PRESENCES.arg(seventvUserID), + NetworkRequestType::Post) + .json(payload) + .timeout(10000) + .onSuccess([callback = std::move(onSuccess)](const auto &) { + callback(); + }) + .onError([callback = std::move(onError)](const NetworkResult &result) { + callback(result); + }) + .execute(); +} + +} // namespace chatterino +// NOLINTEND(readability-convert-member-functions-to-static) diff --git a/src/providers/seventv/SeventvAPI.hpp b/src/providers/seventv/SeventvAPI.hpp new file mode 100644 index 000000000..5ee5d9b61 --- /dev/null +++ b/src/providers/seventv/SeventvAPI.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include + +class QString; +class QJsonObject; + +namespace chatterino { + +class NetworkResult; + +class SeventvAPI final +{ + using ErrorCallback = std::function; + template + using SuccessCallback = std::function; + +public: + SeventvAPI() = default; + ~SeventvAPI() = default; + + SeventvAPI(const SeventvAPI &) = delete; + SeventvAPI(SeventvAPI &&) = delete; + SeventvAPI &operator=(const SeventvAPI &) = delete; + SeventvAPI &operator=(SeventvAPI &&) = delete; + + virtual void getUserByTwitchID( + const QString &twitchID, + SuccessCallback &&onSuccess, + ErrorCallback &&onError); + virtual void getEmoteSet(const QString &emoteSet, + SuccessCallback &&onSuccess, + ErrorCallback &&onError); + + virtual void updatePresence(const QString &twitchChannelID, + const QString &seventvUserID, + SuccessCallback<> &&onSuccess, + ErrorCallback &&onError); +}; + +} // namespace chatterino diff --git a/src/providers/seventv/SeventvBadges.cpp b/src/providers/seventv/SeventvBadges.cpp new file mode 100644 index 000000000..2f09bd087 --- /dev/null +++ b/src/providers/seventv/SeventvBadges.cpp @@ -0,0 +1,77 @@ +#include "providers/seventv/SeventvBadges.hpp" + +#include "messages/Emote.hpp" +#include "messages/Image.hpp" +#include "providers/seventv/SeventvEmotes.hpp" + +#include +#include +#include + +namespace chatterino { + +std::optional SeventvBadges::getBadge(const UserId &id) const +{ + std::shared_lock lock(this->mutex_); + + auto it = this->badgeMap_.find(id.string); + if (it != this->badgeMap_.end()) + { + return it->second; + } + return std::nullopt; +} + +void SeventvBadges::assignBadgeToUser(const QString &badgeID, + const UserId &userID) +{ + const std::unique_lock lock(this->mutex_); + + const auto badgeIt = this->knownBadges_.find(badgeID); + if (badgeIt != this->knownBadges_.end()) + { + this->badgeMap_[userID.string] = badgeIt->second; + } +} + +void SeventvBadges::clearBadgeFromUser(const QString &badgeID, + const UserId &userID) +{ + const std::unique_lock lock(this->mutex_); + + const auto it = this->badgeMap_.find(userID.string); + if (it != this->badgeMap_.end() && it->second->id.string == badgeID) + { + this->badgeMap_.erase(userID.string); + } +} + +void SeventvBadges::registerBadge(const QJsonObject &badgeJson) +{ + const auto badgeID = badgeJson["id"].toString(); + + const std::unique_lock lock(this->mutex_); + + if (this->knownBadges_.find(badgeID) != this->knownBadges_.end()) + { + return; + } + + auto emote = Emote{ + .name = EmoteName{}, + .images = SeventvEmotes::createImageSet(badgeJson), + .tooltip = Tooltip{badgeJson["tooltip"].toString()}, + .homePage = Url{}, + .id = EmoteId{badgeID}, + }; + + if (emote.images.getImage1()->isEmpty()) + { + return; // Bad images + } + + this->knownBadges_[badgeID] = + std::make_shared(std::move(emote)); +} + +} // namespace chatterino diff --git a/src/providers/seventv/SeventvBadges.hpp b/src/providers/seventv/SeventvBadges.hpp new file mode 100644 index 000000000..c9d0efad5 --- /dev/null +++ b/src/providers/seventv/SeventvBadges.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include "common/Aliases.hpp" +#include "util/QStringHash.hpp" + +#include + +#include +#include +#include +#include + +namespace chatterino { + +struct Emote; +using EmotePtr = std::shared_ptr; + +class SeventvBadges +{ +public: + // Return the badge, if any, that is assigned to the user + std::optional getBadge(const UserId &id) const; + + // Assign the given badge to the user + void assignBadgeToUser(const QString &badgeID, const UserId &userID); + + // Remove the given badge from the user + void clearBadgeFromUser(const QString &badgeID, const UserId &userID); + + // Register a new known badge + // The json object will contain all information about the badge, like its ID & its images + void registerBadge(const QJsonObject &badgeJson); + +private: + // Mutex for both `badgeMap_` and `knownBadges_` + mutable std::shared_mutex mutex_; + + // user-id => badge + std::unordered_map badgeMap_; + // badge-id => badge + std::unordered_map knownBadges_; +}; + +} // namespace chatterino diff --git a/src/providers/seventv/SeventvCosmetics.hpp b/src/providers/seventv/SeventvCosmetics.hpp new file mode 100644 index 000000000..6302c51f4 --- /dev/null +++ b/src/providers/seventv/SeventvCosmetics.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include + +namespace chatterino::seventv { + +enum class CosmeticKind { + Badge, + Paint, + EmoteSet, + + INVALID, +}; + +} // namespace chatterino::seventv + +template <> +constexpr magic_enum::customize::customize_t + magic_enum::customize::enum_name( + chatterino::seventv::CosmeticKind value) noexcept +{ + using chatterino::seventv::CosmeticKind; + switch (value) + { + case CosmeticKind::Badge: + return "BADGE"; + case CosmeticKind::Paint: + return "PAINT"; + case CosmeticKind::EmoteSet: + return "EMOTE_SET"; + + default: + return default_tag; + } +} diff --git a/src/providers/seventv/SeventvEmotes.cpp b/src/providers/seventv/SeventvEmotes.cpp new file mode 100644 index 000000000..969b0e5f5 --- /dev/null +++ b/src/providers/seventv/SeventvEmotes.cpp @@ -0,0 +1,505 @@ +#include "providers/seventv/SeventvEmotes.hpp" + +#include "Application.hpp" +#include "common/Literals.hpp" +#include "common/network/NetworkResult.hpp" +#include "common/QLogging.hpp" +#include "messages/Emote.hpp" +#include "messages/Image.hpp" +#include "messages/ImageSet.hpp" +#include "messages/MessageBuilder.hpp" +#include "providers/seventv/eventapi/Dispatch.hpp" +#include "providers/seventv/SeventvAPI.hpp" +#include "providers/twitch/TwitchChannel.hpp" +#include "singletons/Settings.hpp" +#include "util/Helpers.hpp" + +#include +#include +#include + +#include +#include + +/** + * # References + * + * - EmoteSet: https://github.com/SevenTV/API/blob/a84e884b5590dbb5d91a5c6b3548afabb228f385/data/model/emote-set.model.go#L8-L18 + * - ActiveEmote: https://github.com/SevenTV/API/blob/a84e884b5590dbb5d91a5c6b3548afabb228f385/data/model/emote-set.model.go#L20-L27 + * - EmotePartial (emoteData): https://github.com/SevenTV/API/blob/a84e884b5590dbb5d91a5c6b3548afabb228f385/data/model/emote.model.go#L24-L34 + * - ImageHost: https://github.com/SevenTV/API/blob/a84e884b5590dbb5d91a5c6b3548afabb228f385/data/model/model.go#L36-L39 + * - ImageFile: https://github.com/SevenTV/API/blob/a84e884b5590dbb5d91a5c6b3548afabb228f385/data/model/model.go#L41-L48 + */ +namespace { + +using namespace chatterino; +using namespace seventv::eventapi; + +// These declarations won't throw an exception. +const QString CHANNEL_HAS_NO_EMOTES("This channel has no 7TV channel emotes."); +const QString EMOTE_LINK_FORMAT("https://7tv.app/emotes/%1"); + +struct CreateEmoteResult { + Emote emote; + EmoteId id; + EmoteName name; + bool hasImages{}; +}; + +EmotePtr cachedOrMake(Emote &&emote, const EmoteId &id) +{ + static std::unordered_map> cache; + static std::mutex mutex; + + return cachedOrMakeEmotePtr(std::move(emote), cache, mutex, id); +} + +/** + * This decides whether an emote should be displayed + * as zero-width + */ +bool isZeroWidthActive(const QJsonObject &activeEmote) +{ + auto flags = SeventvActiveEmoteFlags( + SeventvActiveEmoteFlag(activeEmote.value("flags").toInt())); + return flags.has(SeventvActiveEmoteFlag::ZeroWidth); +} + +/** + * This is only an indicator if an emote should be added + * as zero-width or not. The user can still overwrite this. + */ +bool isZeroWidthRecommended(const QJsonObject &emoteData) +{ + auto flags = + SeventvEmoteFlags(SeventvEmoteFlag(emoteData.value("flags").toInt())); + return flags.has(SeventvEmoteFlag::ZeroWidth); +} + +Tooltip createTooltip(const QString &name, const QString &author, bool isGlobal) +{ + return Tooltip{QString("%1
%2 7TV Emote
By: %3") + .arg(name, isGlobal ? "Global" : "Channel", + author.isEmpty() ? "" : author)}; +} + +Tooltip createAliasedTooltip(const QString &name, const QString &baseName, + const QString &author, bool isGlobal) +{ + return Tooltip{QString("%1
Alias of %2
%3 7TV Emote
By: %4") + .arg(name, baseName, isGlobal ? "Global" : "Channel", + author.isEmpty() ? "" : author)}; +} + +CreateEmoteResult createEmote(const QJsonObject &activeEmote, + const QJsonObject &emoteData, bool isGlobal) +{ + auto emoteId = EmoteId{activeEmote["id"].toString()}; + auto emoteName = EmoteName{activeEmote["name"].toString()}; + auto author = + EmoteAuthor{emoteData["owner"].toObject()["display_name"].toString()}; + auto baseEmoteName = EmoteName{emoteData["name"].toString()}; + bool zeroWidth = isZeroWidthActive(activeEmote); + bool aliasedName = emoteName != baseEmoteName; + auto tooltip = + aliasedName + ? createAliasedTooltip(emoteName.string, baseEmoteName.string, + author.string, isGlobal) + : createTooltip(emoteName.string, author.string, isGlobal); + auto imageSet = SeventvEmotes::createImageSet(emoteData); + + auto emote = + Emote({emoteName, imageSet, tooltip, + Url{EMOTE_LINK_FORMAT.arg(emoteId.string)}, zeroWidth, emoteId, + author, makeConditionedOptional(aliasedName, baseEmoteName)}); + + return {emote, emoteId, emoteName, !emote.images.getImage1()->isEmpty()}; +} + +bool checkEmoteVisibility(const QJsonObject &emoteData) +{ + if (!emoteData["listed"].toBool() && + !getSettings()->showUnlistedSevenTVEmotes) + { + return false; + } + auto flags = + SeventvEmoteFlags(SeventvEmoteFlag(emoteData["flags"].toInt())); + return !flags.has(SeventvEmoteFlag::ContentTwitchDisallowed); +} + +EmotePtr createUpdatedEmote(const EmotePtr &oldEmote, + const EmoteUpdateDispatch &dispatch) +{ + bool toNonAliased = oldEmote->baseName.has_value() && + dispatch.emoteName == oldEmote->baseName->string; + + auto baseName = oldEmote->baseName.value_or(oldEmote->name); + auto emote = std::make_shared(Emote( + {EmoteName{dispatch.emoteName}, oldEmote->images, + toNonAliased + ? createTooltip(dispatch.emoteName, oldEmote->author.string, false) + : createAliasedTooltip(dispatch.emoteName, baseName.string, + oldEmote->author.string, false), + oldEmote->homePage, oldEmote->zeroWidth, oldEmote->id, + oldEmote->author, makeConditionedOptional(!toNonAliased, baseName)})); + return emote; +} + +} // namespace + +namespace chatterino { + +using namespace seventv::eventapi; +using namespace seventv::detail; +using namespace literals; + +EmoteMap seventv::detail::parseEmotes(const QJsonArray &emoteSetEmotes, + bool isGlobal) +{ + auto emotes = EmoteMap(); + + for (const auto &activeEmoteJson : emoteSetEmotes) + { + auto activeEmote = activeEmoteJson.toObject(); + auto emoteData = activeEmote["data"].toObject(); + + if (emoteData.empty() || !checkEmoteVisibility(emoteData)) + { + continue; + } + + auto result = createEmote(activeEmote, emoteData, isGlobal); + if (!result.hasImages) + { + // this shouldn't happen but if it does, it will crash, + // so we don't add the emote + qCDebug(chatterinoSeventv) + << "Emote without images:" << activeEmote; + continue; + } + auto ptr = cachedOrMake(std::move(result.emote), result.id); + emotes[result.name] = ptr; + } + + return emotes; +} + +SeventvEmotes::SeventvEmotes() + : global_(std::make_shared()) +{ + getSettings()->enableSevenTVGlobalEmotes.connect( + [this] { + this->loadGlobalEmotes(); + }, + this->managedConnections, false); +} + +std::shared_ptr SeventvEmotes::globalEmotes() const +{ + return this->global_.get(); +} + +std::optional SeventvEmotes::globalEmote(const EmoteName &name) const +{ + auto emotes = this->global_.get(); + auto it = emotes->find(name); + + if (it == emotes->end()) + { + return std::nullopt; + } + return it->second; +} + +void SeventvEmotes::loadGlobalEmotes() +{ + if (!Settings::instance().enableSevenTVGlobalEmotes) + { + this->setGlobalEmotes(EMPTY_EMOTE_MAP); + return; + } + + qCDebug(chatterinoSeventv) << "Loading 7TV Global Emotes"; + + getApp()->getSeventvAPI()->getEmoteSet( + u"global"_s, + [this](const auto &json) { + QJsonArray parsedEmotes = json["emotes"].toArray(); + + auto emoteMap = parseEmotes(parsedEmotes, true); + qCDebug(chatterinoSeventv) + << "Loaded" << emoteMap.size() << "7TV Global Emotes"; + this->setGlobalEmotes( + std::make_shared(std::move(emoteMap))); + }, + [](const auto &result) { + qCWarning(chatterinoSeventv) + << "Couldn't load 7TV global emotes" << result.getData(); + }); +} + +void SeventvEmotes::setGlobalEmotes(std::shared_ptr emotes) +{ + this->global_.set(std::move(emotes)); +} + +void SeventvEmotes::loadChannelEmotes( + const std::weak_ptr &channel, const QString &channelId, + std::function callback, bool manualRefresh) +{ + qCDebug(chatterinoSeventv) + << "Reloading 7TV Channel Emotes" << channelId << manualRefresh; + + getApp()->getSeventvAPI()->getUserByTwitchID( + channelId, + [callback = std::move(callback), channel, channelId, + manualRefresh](const auto &json) { + const auto emoteSet = json["emote_set"].toObject(); + const auto parsedEmotes = emoteSet["emotes"].toArray(); + + auto emoteMap = parseEmotes(parsedEmotes, false); + bool hasEmotes = !emoteMap.empty(); + + qCDebug(chatterinoSeventv) + << "Loaded" << emoteMap.size() << "7TV Channel Emotes for" + << channelId << "manual refresh:" << manualRefresh; + + if (hasEmotes) + { + auto user = json["user"].toObject(); + + size_t connectionIdx = 0; + for (const auto &conn : user["connections"].toArray()) + { + if (conn.toObject()["platform"].toString() == "TWITCH") + { + break; + } + connectionIdx++; + } + + callback(std::move(emoteMap), + {user["id"].toString(), emoteSet["id"].toString(), + connectionIdx}); + } + + auto shared = channel.lock(); + if (!shared) + { + return; + } + + if (manualRefresh) + { + if (hasEmotes) + { + shared->addSystemMessage("7TV channel emotes reloaded."); + } + else + { + shared->addSystemMessage(CHANNEL_HAS_NO_EMOTES); + } + } + }, + [channelId, channel, manualRefresh](const auto &result) { + auto shared = channel.lock(); + if (!shared) + { + return; + } + if (result.status() == 404) + { + qCWarning(chatterinoSeventv) + << "Error occurred fetching 7TV emotes: " + << result.parseJson(); + if (manualRefresh) + { + shared->addSystemMessage(CHANNEL_HAS_NO_EMOTES); + } + } + else + { + // TODO: Auto retry in case of a timeout, with a delay + auto errorString = result.formatError(); + qCWarning(chatterinoSeventv) + << "Error fetching 7TV emotes for channel" << channelId + << ", error" << errorString; + shared->addSystemMessage( + QStringLiteral("Failed to fetch 7TV channel " + "emotes. (Error: %1)") + .arg(errorString)); + } + }); +} + +std::optional SeventvEmotes::addEmote( + Atomic> &map, + const EmoteAddDispatch &dispatch) +{ + // Check for visibility first, so we don't copy the map. + auto emoteData = dispatch.emoteJson["data"].toObject(); + if (emoteData.empty() || !checkEmoteVisibility(emoteData)) + { + return std::nullopt; + } + + // This copies the map. + EmoteMap updatedMap = *map.get(); + auto result = createEmote(dispatch.emoteJson, emoteData, false); + if (!result.hasImages) + { + // Incoming emote didn't contain any images, abort + qCDebug(chatterinoSeventv) + << "Emote without images:" << dispatch.emoteJson; + return std::nullopt; + } + auto emote = std::make_shared(std::move(result.emote)); + updatedMap[result.name] = emote; + map.set(std::make_shared(std::move(updatedMap))); + + return emote; +} + +std::optional SeventvEmotes::updateEmote( + Atomic> &map, + const EmoteUpdateDispatch &dispatch) +{ + auto oldMap = map.get(); + auto oldEmote = oldMap->findEmote(dispatch.emoteName, dispatch.emoteID); + if (oldEmote == oldMap->end()) + { + return std::nullopt; + } + + // This copies the map. + EmoteMap updatedMap = *map.get(); + updatedMap.erase(oldEmote->second->name); + + auto emote = createUpdatedEmote(oldEmote->second, dispatch); + updatedMap[emote->name] = emote; + map.set(std::make_shared(std::move(updatedMap))); + + return emote; +} + +std::optional SeventvEmotes::removeEmote( + Atomic> &map, + const EmoteRemoveDispatch &dispatch) +{ + // This copies the map. + EmoteMap updatedMap = *map.get(); + auto it = updatedMap.findEmote(dispatch.emoteName, dispatch.emoteID); + if (it == updatedMap.end()) + { + // We already copied the map at this point and are now discarding the copy. + // This is fine, because this case should be really rare. + return std::nullopt; + } + auto emote = it->second; + updatedMap.erase(it); + map.set(std::make_shared(std::move(updatedMap))); + + return emote; +} + +void SeventvEmotes::getEmoteSet( + const QString &emoteSetId, + std::function successCallback, + std::function errorCallback) +{ + qCDebug(chatterinoSeventv) << "Loading 7TV Emote Set" << emoteSetId; + + getApp()->getSeventvAPI()->getEmoteSet( + emoteSetId, + [callback = std::move(successCallback), emoteSetId](const auto &json) { + auto parsedEmotes = json["emotes"].toArray(); + + auto emoteMap = parseEmotes(parsedEmotes, false); + + qCDebug(chatterinoSeventv) << "Loaded" << emoteMap.size() + << "7TV Emotes from" << emoteSetId; + + callback(std::move(emoteMap), json["name"].toString()); + }, + [emoteSetId, callback = std::move(errorCallback)](const auto &result) { + callback(result.formatError()); + }); +} + +ImageSet SeventvEmotes::createImageSet(const QJsonObject &emoteData) +{ + auto host = emoteData["host"].toObject(); + // "//cdn.7tv[...]" + auto baseUrl = host["url"].toString(); + auto files = host["files"].toArray(); + + std::array sizes; + double baseWidth = 0.0; + size_t nextSize = 0; + + for (auto fileItem : files) + { + if (nextSize >= sizes.size()) + { + break; + } + + auto file = fileItem.toObject(); + if (file["format"].toString() != "WEBP") + { + continue; // We only use webp + } + + double width = file["width"].toDouble(); + double scale = 1.0; // in relation to first image + if (baseWidth > 0.0) + { + scale = baseWidth / width; + } + else + { + // => this is the first image + baseWidth = width; + } + + auto image = Image::fromUrl( + {QString("https:%1/%2").arg(baseUrl, file["name"].toString())}, + scale, {static_cast(width), file["height"].toInt(16)}); + + sizes.at(nextSize) = image; + nextSize++; + } + + if (nextSize < sizes.size()) + { + // this should be really rare + // this means we didn't get all sizes of an emote + if (nextSize == 0) + { + qCDebug(chatterinoSeventv) + << "Got file list without any eligible files"; + // When this emote is typed, chatterino will crash. + return ImageSet{}; + } + for (; nextSize < sizes.size(); nextSize++) + { + sizes.at(nextSize) = Image::getEmpty(); + } + } + + // Typically, 7TV provides four versions (1x, 2x, 3x, and 4x). The 3x + // version has a scale factor of 1/3, which is a size other providers don't + // provide - they only provide the 4x version (0.25). To be in line with + // other providers, we prefer the 4x version but fall back to the 3x one if + // it doesn't exist. + auto largest = std::move(sizes[3]); + if (!largest || largest->isEmpty()) + { + largest = std::move(sizes[2]); + } + + return ImageSet{sizes[0], sizes[1], largest}; +} + +} // namespace chatterino diff --git a/src/providers/seventv/SeventvEmotes.hpp b/src/providers/seventv/SeventvEmotes.hpp new file mode 100644 index 000000000..03b966f9c --- /dev/null +++ b/src/providers/seventv/SeventvEmotes.hpp @@ -0,0 +1,166 @@ +#pragma once + +#include "common/Aliases.hpp" +#include "common/Atomic.hpp" +#include "common/FlagsEnum.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace chatterino { + +class ImageSet; +class Channel; +namespace seventv::eventapi { + struct EmoteAddDispatch; + struct EmoteUpdateDispatch; + struct EmoteRemoveDispatch; +} // namespace seventv::eventapi + +// https://github.com/SevenTV/API/blob/a84e884b5590dbb5d91a5c6b3548afabb228f385/data/model/emote-set.model.go#L29-L36 +enum class SeventvActiveEmoteFlag : std::int64_t { + None = 0LL, + + // Emote is zero-width + ZeroWidth = (1LL << 0), + + // Overrides Twitch Global emotes with the same name + OverrideTwitchGlobal = (1 << 16), + // Overrides Twitch Subscriber emotes with the same name + OverrideTwitchSubscriber = (1 << 17), + // Overrides BetterTTV emotes with the same name + OverrideBetterTTV = (1 << 18), +}; + +// https://github.com/SevenTV/API/blob/a84e884b5590dbb5d91a5c6b3548afabb228f385/data/model/emote.model.go#L57-L70 +enum class SeventvEmoteFlag : int64_t { + None = 0LL, + // The emote is private and can only be accessed by its owner, editors and moderators + Private = 1 << 0, + // The emote was verified to be an original creation by the uploader + Authentic = (1LL << 1), + // The emote is recommended to be enabled as Zero-Width + ZeroWidth = (1LL << 8), + + // Content Flags + + // Sexually Suggesive + ContentSexual = (1LL << 16), + // Rapid flashing + ContentEpilepsy = (1LL << 17), + // Edgy or distasteful, may be offensive to some users + ContentEdgy = (1 << 18), + // Not allowed specifically on the Twitch platform + ContentTwitchDisallowed = (1LL << 24), +}; + +using SeventvActiveEmoteFlags = FlagsEnum; +using SeventvEmoteFlags = FlagsEnum; + +struct Emote; +using EmotePtr = std::shared_ptr; +class EmoteMap; + +enum class SeventvEmoteSetKind : uint8_t { + Global, + Personal, + Channel, +}; + +enum class SeventvEmoteSetFlag : uint32_t { + Immutable = (1 << 0), + Privileged = (1 << 1), + Personal = (1 << 2), + Commercial = (1 << 3), +}; +using SeventvEmoteSetFlags = FlagsEnum; + +namespace seventv::detail { + + EmoteMap parseEmotes(const QJsonArray &emoteSetEmotes, bool isGlobal); + +} // namespace seventv::detail + +class SeventvEmotes final +{ +public: + struct ChannelInfo { + QString userID; + QString emoteSetID; + size_t twitchConnectionIndex; + }; + + SeventvEmotes(); + + std::shared_ptr globalEmotes() const; + std::optional globalEmote(const EmoteName &name) const; + void loadGlobalEmotes(); + void setGlobalEmotes(std::shared_ptr emotes); + static void loadChannelEmotes( + const std::weak_ptr &channel, const QString &channelId, + std::function callback, + bool manualRefresh); + + /** + * Adds an emote to the `map` if it's valid. + * This will _copy_ the emote map and + * update the `Atomic`. + * + * @return The added emote if an emote was added. + */ + static std::optional addEmote( + Atomic> &map, + const seventv::eventapi::EmoteAddDispatch &dispatch); + + /** + * Updates an emote in this `map`. + * This will _copy_ the emote map and + * update the `Atomic`. + * + * @return The updated emote if any emote was updated. + */ + static std::optional updateEmote( + Atomic> &map, + const seventv::eventapi::EmoteUpdateDispatch &dispatch); + + /** + * Removes an emote from this `map`. + * This will _copy_ the emote map and + * update the `Atomic`. + * + * @return The removed emote if any emote was removed. + */ + static std::optional removeEmote( + Atomic> &map, + const seventv::eventapi::EmoteRemoveDispatch &dispatch); + + /** Fetches an emote-set by its id */ + static void getEmoteSet( + const QString &emoteSetId, + std::function successCallback, + std::function errorCallback); + + /** + * Creates an image set from a 7TV emote or badge. + * + * @param emoteData { host: { files: [], url } } + */ + static ImageSet createImageSet(const QJsonObject &emoteData); + +private: + Atomic> global_; + + std::vector> + managedConnections; +}; + +} // namespace chatterino diff --git a/src/providers/seventv/SeventvEventAPI.cpp b/src/providers/seventv/SeventvEventAPI.cpp new file mode 100644 index 000000000..fcb99ce12 --- /dev/null +++ b/src/providers/seventv/SeventvEventAPI.cpp @@ -0,0 +1,403 @@ +#include "providers/seventv/SeventvEventAPI.hpp" + +#include "Application.hpp" +#include "common/Literals.hpp" +#include "providers/seventv/eventapi/Client.hpp" +#include "providers/seventv/eventapi/Dispatch.hpp" +#include "providers/seventv/eventapi/Message.hpp" +#include "providers/seventv/SeventvBadges.hpp" +#include "providers/seventv/SeventvCosmetics.hpp" +#include "util/QMagicEnum.hpp" + +#include + +#include + +namespace chatterino { + +using namespace seventv; +using namespace seventv::eventapi; +using namespace chatterino::literals; + +SeventvEventAPI::SeventvEventAPI( + QString host, std::chrono::milliseconds defaultHeartbeatInterval) + : BasicPubSubManager(std::move(host), u"7TV"_s) + , heartbeatInterval_(defaultHeartbeatInterval) +{ +} + +SeventvEventAPI::~SeventvEventAPI() +{ + this->stop(); +} + +void SeventvEventAPI::subscribeUser(const QString &userID, + const QString &emoteSetID) +{ + if (!userID.isEmpty() && this->subscribedUsers_.insert(userID).second) + { + this->subscribe( + {ObjectIDCondition{userID}, SubscriptionType::UpdateUser}); + } + if (!emoteSetID.isEmpty() && + this->subscribedEmoteSets_.insert(emoteSetID).second) + { + this->subscribe( + {ObjectIDCondition{emoteSetID}, SubscriptionType::UpdateEmoteSet}); + } +} + +void SeventvEventAPI::subscribeTwitchChannel(const QString &id) +{ + if (this->subscribedTwitchChannels_.insert(id).second) + { + this->subscribe({ + ChannelCondition{id}, + SubscriptionType::CreateCosmetic, + }); + this->subscribe({ + ChannelCondition{id}, + SubscriptionType::CreateEntitlement, + }); + this->subscribe({ + ChannelCondition{id}, + SubscriptionType::DeleteEntitlement, + }); + } +} + +void SeventvEventAPI::unsubscribeEmoteSet(const QString &id) +{ + if (this->subscribedEmoteSets_.erase(id) > 0) + { + this->unsubscribe( + {ObjectIDCondition{id}, SubscriptionType::UpdateEmoteSet}); + } +} + +void SeventvEventAPI::unsubscribeUser(const QString &id) +{ + if (this->subscribedUsers_.erase(id) > 0) + { + this->unsubscribe( + {ObjectIDCondition{id}, SubscriptionType::UpdateUser}); + } +} + +void SeventvEventAPI::unsubscribeTwitchChannel(const QString &id) +{ + if (this->subscribedTwitchChannels_.erase(id) > 0) + { + this->unsubscribe({ + ChannelCondition{id}, + SubscriptionType::CreateCosmetic, + }); + this->unsubscribe({ + ChannelCondition{id}, + SubscriptionType::CreateEntitlement, + }); + this->unsubscribe({ + ChannelCondition{id}, + SubscriptionType::DeleteEntitlement, + }); + } +} + +std::shared_ptr> SeventvEventAPI::createClient( + liveupdates::WebsocketClient &client, websocketpp::connection_hdl hdl) +{ + auto shared = + std::make_shared(client, hdl, this->heartbeatInterval_); + return std::static_pointer_cast>( + std::move(shared)); +} + +void SeventvEventAPI::onMessage( + websocketpp::connection_hdl hdl, + BasicPubSubManager::WebsocketMessagePtr msg) +{ + const auto &payload = QString::fromStdString(msg->get_payload()); + + auto pMessage = parseBaseMessage(payload); + + if (!pMessage) + { + qCDebug(chatterinoSeventvEventAPI) + << "Unable to parse incoming event-api message: " << payload; + return; + } + auto message = *pMessage; + switch (message.op) + { + case Opcode::Hello: { + if (auto client = this->findClient(hdl)) + { + if (auto *stvClient = dynamic_cast(client.get())) + { + stvClient->setHeartbeatInterval( + message.data["heartbeat_interval"].toInt()); + } + } + } + break; + case Opcode::Heartbeat: { + if (auto client = this->findClient(hdl)) + { + if (auto *stvClient = dynamic_cast(client.get())) + { + stvClient->handleHeartbeat(); + } + } + } + break; + case Opcode::Dispatch: { + auto dispatch = message.toInner(); + if (!dispatch) + { + qCDebug(chatterinoSeventvEventAPI) + << "Malformed dispatch" << payload; + return; + } + this->handleDispatch(*dispatch); + } + break; + case Opcode::Reconnect: { + if (auto client = this->findClient(hdl)) + { + if (auto *stvClient = dynamic_cast(client.get())) + { + stvClient->close("Reconnecting"); + } + } + } + break; + case Opcode::Ack: { + // unhandled + } + break; + default: { + qCDebug(chatterinoSeventvEventAPI) << "Unhandled op:" << payload; + } + break; + } +} + +void SeventvEventAPI::handleDispatch(const Dispatch &dispatch) +{ + switch (dispatch.type) + { + case SubscriptionType::UpdateEmoteSet: { + this->onEmoteSetUpdate(dispatch); + } + break; + case SubscriptionType::UpdateUser: { + this->onUserUpdate(dispatch); + } + break; + case SubscriptionType::CreateCosmetic: { + const CosmeticCreateDispatch cosmetic(dispatch); + if (cosmetic.validate()) + { + this->onCosmeticCreate(cosmetic); + } + else + { + qCDebug(chatterinoSeventvEventAPI) + << "Invalid cosmetic dispatch" << dispatch.body; + } + } + break; + case SubscriptionType::CreateEntitlement: { + const EntitlementCreateDeleteDispatch entitlement(dispatch); + if (entitlement.validate()) + { + this->onEntitlementCreate(entitlement); + } + else + { + qCDebug(chatterinoSeventvEventAPI) + << "Invalid entitlement create dispatch" << dispatch.body; + } + } + break; + case SubscriptionType::DeleteEntitlement: { + const EntitlementCreateDeleteDispatch entitlement(dispatch); + if (entitlement.validate()) + { + this->onEntitlementDelete(entitlement); + } + else + { + qCDebug(chatterinoSeventvEventAPI) + << "Invalid entitlement delete dispatch" << dispatch.body; + } + } + break; + default: { + qCDebug(chatterinoSeventvEventAPI) + << "Unknown subscription type:" + << qmagicenum::enumName(dispatch.type) + << "body:" << dispatch.body; + } + break; + } +} + +void SeventvEventAPI::onEmoteSetUpdate(const Dispatch &dispatch) +{ + // dispatchBody: { + // pushed: Array<{ key, value }>, + // pulled: Array<{ key, old_value }>, + // updated: Array<{ key, value, old_value }>, + // } + for (const auto pushedRef : dispatch.body["pushed"].toArray()) + { + auto pushed = pushedRef.toObject(); + if (pushed["key"].toString() != "emotes") + { + continue; + } + + const EmoteAddDispatch added(dispatch, pushed["value"].toObject()); + + if (added.validate()) + { + this->signals_.emoteAdded.invoke(added); + } + else + { + qCDebug(chatterinoSeventvEventAPI) + << "Invalid dispatch" << dispatch.body; + } + } + for (const auto updatedRef : dispatch.body["updated"].toArray()) + { + auto updated = updatedRef.toObject(); + if (updated["key"].toString() != "emotes") + { + continue; + } + + const EmoteUpdateDispatch update(dispatch, + updated["old_value"].toObject(), + updated["value"].toObject()); + + if (update.validate()) + { + this->signals_.emoteUpdated.invoke(update); + } + else + { + qCDebug(chatterinoSeventvEventAPI) + << "Invalid dispatch" << dispatch.body; + } + } + for (const auto pulledRef : dispatch.body["pulled"].toArray()) + { + auto pulled = pulledRef.toObject(); + if (pulled["key"].toString() != "emotes") + { + continue; + } + + const EmoteRemoveDispatch removed(dispatch, + pulled["old_value"].toObject()); + + if (removed.validate()) + { + this->signals_.emoteRemoved.invoke(removed); + } + else + { + qCDebug(chatterinoSeventvEventAPI) + << "Invalid dispatch" << dispatch.body; + } + } +} + +void SeventvEventAPI::onUserUpdate(const Dispatch &dispatch) +{ + // dispatchBody: { + // updated: Array<{ key, value: Array<{key, value}> }> + // } + for (const auto updatedRef : dispatch.body["updated"].toArray()) + { + auto updated = updatedRef.toObject(); + if (updated["key"].toString() != "connections") + { + continue; + } + for (const auto valueRef : updated["value"].toArray()) + { + auto value = valueRef.toObject(); + if (value["key"].toString() != "emote_set") + { + continue; + } + + const UserConnectionUpdateDispatch update( + dispatch, value, (size_t)updated["index"].toInt()); + + if (update.validate()) + { + this->signals_.userUpdated.invoke(update); + } + else + { + qCDebug(chatterinoSeventvEventAPI) + << "Invalid dispatch" << dispatch.body; + } + } + } +} + +// NOLINTBEGIN(readability-convert-member-functions-to-static) + +void SeventvEventAPI::onCosmeticCreate(const CosmeticCreateDispatch &cosmetic) +{ + auto *badges = getApp()->getSeventvBadges(); + switch (cosmetic.kind) + { + case CosmeticKind::Badge: { + badges->registerBadge(cosmetic.data); + } + break; + default: + break; + } +} + +void SeventvEventAPI::onEntitlementCreate( + const EntitlementCreateDeleteDispatch &entitlement) +{ + auto *badges = getApp()->getSeventvBadges(); + switch (entitlement.kind) + { + case CosmeticKind::Badge: { + badges->assignBadgeToUser(entitlement.refID, + UserId{entitlement.userID}); + } + break; + default: + break; + } +} + +void SeventvEventAPI::onEntitlementDelete( + const EntitlementCreateDeleteDispatch &entitlement) +{ + auto *badges = getApp()->getSeventvBadges(); + switch (entitlement.kind) + { + case CosmeticKind::Badge: { + badges->clearBadgeFromUser(entitlement.refID, + UserId{entitlement.userID}); + } + break; + default: + break; + } +} +// NOLINTEND(readability-convert-member-functions-to-static) + +} // namespace chatterino diff --git a/src/providers/seventv/SeventvEventAPI.hpp b/src/providers/seventv/SeventvEventAPI.hpp new file mode 100644 index 000000000..d4b4c34f9 --- /dev/null +++ b/src/providers/seventv/SeventvEventAPI.hpp @@ -0,0 +1,98 @@ +#pragma once + +#include "providers/liveupdates/BasicPubSubClient.hpp" +#include "providers/liveupdates/BasicPubSubManager.hpp" +#include "providers/seventv/eventapi/Subscription.hpp" +#include "util/QStringHash.hpp" + +#include + +namespace chatterino { + +namespace seventv::eventapi { + struct Dispatch; + struct EmoteAddDispatch; + struct EmoteUpdateDispatch; + struct EmoteRemoveDispatch; + struct UserConnectionUpdateDispatch; + struct CosmeticCreateDispatch; + struct EntitlementCreateDeleteDispatch; +} // namespace seventv::eventapi + +class SeventvBadges; + +class SeventvEventAPI + : public BasicPubSubManager +{ + template + using Signal = + pajlada::Signals::Signal; // type-id is vector> + +public: + SeventvEventAPI(QString host, + std::chrono::milliseconds defaultHeartbeatInterval = + std::chrono::milliseconds(25000)); + + ~SeventvEventAPI() override; + + struct { + Signal emoteAdded; + Signal emoteUpdated; + Signal emoteRemoved; + Signal userUpdated; + } signals_; // NOLINT(readability-identifier-naming) + + /** + * Subscribes to a user and emote-set + * if not already subscribed. + * + * @param userID 7TV user-id, may be empty. + * @param emoteSetID 7TV emote-set-id, may be empty. + */ + void subscribeUser(const QString &userID, const QString &emoteSetID); + /** + * Subscribes to cosmetics and entitlements in a Twitch channel + * if not already subscribed. + * + * @param id Twitch channel id + */ + void subscribeTwitchChannel(const QString &id); + + /** Unsubscribes from a user by its 7TV user id */ + void unsubscribeUser(const QString &id); + /** Unsubscribes from an emote-set by its id */ + void unsubscribeEmoteSet(const QString &id); + /** Unsubscribes from cosmetics and entitlements in a Twitch channel */ + void unsubscribeTwitchChannel(const QString &id); + +protected: + std::shared_ptr> + createClient(liveupdates::WebsocketClient &client, + websocketpp::connection_hdl hdl) override; + void onMessage( + websocketpp::connection_hdl hdl, + BasicPubSubManager::WebsocketMessagePtr + msg) override; + +private: + void handleDispatch(const seventv::eventapi::Dispatch &dispatch); + + void onEmoteSetUpdate(const seventv::eventapi::Dispatch &dispatch); + void onUserUpdate(const seventv::eventapi::Dispatch &dispatch); + void onCosmeticCreate( + const seventv::eventapi::CosmeticCreateDispatch &cosmetic); + void onEntitlementCreate( + const seventv::eventapi::EntitlementCreateDeleteDispatch &entitlement); + void onEntitlementDelete( + const seventv::eventapi::EntitlementCreateDeleteDispatch &entitlement); + + /** emote-set ids */ + std::unordered_set subscribedEmoteSets_; + /** user ids */ + std::unordered_set subscribedUsers_; + /** Twitch channel ids */ + std::unordered_set subscribedTwitchChannels_; + std::chrono::milliseconds heartbeatInterval_; +}; + +} // namespace chatterino diff --git a/src/providers/seventv/eventapi/Client.cpp b/src/providers/seventv/eventapi/Client.cpp new file mode 100644 index 000000000..84533a066 --- /dev/null +++ b/src/providers/seventv/eventapi/Client.cpp @@ -0,0 +1,73 @@ +#include "providers/seventv/eventapi/Client.hpp" + +#include "providers/seventv/eventapi/Subscription.hpp" +#include "providers/twitch/PubSubHelpers.hpp" + +#include + +namespace chatterino::seventv::eventapi { + +Client::Client(liveupdates::WebsocketClient &websocketClient, + liveupdates::WebsocketHandle handle, + std::chrono::milliseconds heartbeatInterval) + : BasicPubSubClient(websocketClient, std::move(handle)) + , lastHeartbeat_(std::chrono::steady_clock::now()) + , heartbeatInterval_(heartbeatInterval) + , heartbeatTimer_(std::make_shared( + this->websocketClient_.get_io_service())) +{ +} + +void Client::stopImpl() +{ + this->heartbeatTimer_->cancel(); +} + +void Client::onConnectionEstablished() +{ + this->lastHeartbeat_.store(std::chrono::steady_clock::now(), + std::memory_order_release); + this->checkHeartbeat(); +} + +void Client::setHeartbeatInterval(int intervalMs) +{ + qCDebug(chatterinoSeventvEventAPI) + << "Setting expected heartbeat interval to" << intervalMs << "ms"; + this->heartbeatInterval_ = std::chrono::milliseconds(intervalMs); +} + +void Client::handleHeartbeat() +{ + this->lastHeartbeat_.store(std::chrono::steady_clock::now(), + std::memory_order_release); +} + +void Client::checkHeartbeat() +{ + // Following the heartbeat docs, a connection is dead + // after three missed heartbeats. + // https://github.com/SevenTV/EventAPI/tree/ca4ff15cc42b89560fa661a76c5849047763d334#heartbeat + assert(this->isStarted()); + if ((std::chrono::steady_clock::now() - this->lastHeartbeat_.load()) > + 3 * this->heartbeatInterval_) + { + qCDebug(chatterinoSeventvEventAPI) + << "Didn't receive a heartbeat in time, disconnecting!"; + this->close("Didn't receive a heartbeat in time"); + + return; + } + + auto self = std::dynamic_pointer_cast(this->shared_from_this()); + + runAfter(this->heartbeatTimer_, this->heartbeatInterval_, [self](auto) { + if (!self->isStarted()) + { + return; + } + self->checkHeartbeat(); + }); +} + +} // namespace chatterino::seventv::eventapi diff --git a/src/providers/seventv/eventapi/Client.hpp b/src/providers/seventv/eventapi/Client.hpp new file mode 100644 index 000000000..ecbc7cb73 --- /dev/null +++ b/src/providers/seventv/eventapi/Client.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include "providers/liveupdates/BasicPubSubClient.hpp" +// this needs to be included for the specialization +// of std::hash for Subscription +#include "providers/seventv/eventapi/Subscription.hpp" + +namespace chatterino { +class SeventvEventAPI; + +} // namespace chatterino + +namespace chatterino::seventv::eventapi { + +class Client : public BasicPubSubClient +{ +public: + Client(liveupdates::WebsocketClient &websocketClient, + liveupdates::WebsocketHandle handle, + std::chrono::milliseconds heartbeatInterval); + + void stopImpl() override; + + void setHeartbeatInterval(int intervalMs); + void handleHeartbeat(); + +protected: + void onConnectionEstablished() override; + +private: + void checkHeartbeat(); + + std::atomic> + lastHeartbeat_; + // This will be set once on the welcome message. + std::chrono::milliseconds heartbeatInterval_; + std::shared_ptr heartbeatTimer_; + + friend SeventvEventAPI; +}; + +} // namespace chatterino::seventv::eventapi diff --git a/src/providers/seventv/eventapi/Dispatch.cpp b/src/providers/seventv/eventapi/Dispatch.cpp new file mode 100644 index 000000000..b4fd31044 --- /dev/null +++ b/src/providers/seventv/eventapi/Dispatch.cpp @@ -0,0 +1,137 @@ +#include "providers/seventv/eventapi/Dispatch.hpp" + +#include "util/QMagicEnum.hpp" + +#include + +#include + +namespace chatterino::seventv::eventapi { + +Dispatch::Dispatch(QJsonObject obj) + : type(qmagicenum::enumCast(obj["type"].toString()) + .value_or(SubscriptionType::INVALID)) + , body(obj["body"].toObject()) + , id(this->body["id"].toString()) + , actorName(this->body["actor"].toObject()["display_name"].toString()) +{ +} + +EmoteAddDispatch::EmoteAddDispatch(const Dispatch &dispatch, QJsonObject emote) + : emoteSetID(dispatch.id) + , actorName(dispatch.actorName) + , emoteJson(std::move(emote)) + , emoteID(this->emoteJson["id"].toString()) +{ +} + +bool EmoteAddDispatch::validate() const +{ + bool validValues = + !this->emoteSetID.isEmpty() && !this->emoteJson.isEmpty(); + if (!validValues) + { + return false; + } + bool validActiveEmote = this->emoteJson.contains("id") && + this->emoteJson.contains("name") && + this->emoteJson.contains("data"); + if (!validActiveEmote) + { + return false; + } + auto emoteData = this->emoteJson["data"].toObject(); + return emoteData.contains("name") && emoteData.contains("host") && + emoteData.contains("owner"); +} + +EmoteRemoveDispatch::EmoteRemoveDispatch(const Dispatch &dispatch, + QJsonObject emote) + : emoteSetID(dispatch.id) + , actorName(dispatch.actorName) + , emoteName(emote["name"].toString()) + , emoteID(emote["id"].toString()) +{ +} + +bool EmoteRemoveDispatch::validate() const +{ + return !this->emoteSetID.isEmpty() && !this->emoteName.isEmpty() && + !this->emoteID.isEmpty(); +} + +EmoteUpdateDispatch::EmoteUpdateDispatch(const Dispatch &dispatch, + QJsonObject oldValue, + QJsonObject value) + : emoteSetID(dispatch.id) + , actorName(dispatch.actorName) + , emoteID(value["id"].toString()) + , oldEmoteName(oldValue["name"].toString()) + , emoteName(value["name"].toString()) +{ +} + +bool EmoteUpdateDispatch::validate() const +{ + return !this->emoteSetID.isEmpty() && !this->emoteID.isEmpty() && + !this->oldEmoteName.isEmpty() && !this->emoteName.isEmpty() && + this->oldEmoteName != this->emoteName; +} + +UserConnectionUpdateDispatch::UserConnectionUpdateDispatch( + const Dispatch &dispatch, const QJsonObject &update, size_t connectionIndex) + : userID(dispatch.id) + , actorName(dispatch.actorName) + , oldEmoteSetID(update["old_value"].toObject()["id"].toString()) + , emoteSetID(update["value"].toObject()["id"].toString()) + , connectionIndex(connectionIndex) +{ +} + +bool UserConnectionUpdateDispatch::validate() const +{ + return !this->userID.isEmpty() && !this->oldEmoteSetID.isEmpty() && + !this->emoteSetID.isEmpty(); +} + +CosmeticCreateDispatch::CosmeticCreateDispatch(const Dispatch &dispatch) + : data(dispatch.body["object"]["data"].toObject()) + , kind(qmagicenum::enumCast( + dispatch.body["object"]["kind"].toString()) + .value_or(CosmeticKind::INVALID)) +{ +} + +bool CosmeticCreateDispatch::validate() const +{ + return !this->data.empty() && this->kind != CosmeticKind::INVALID; +} + +EntitlementCreateDeleteDispatch::EntitlementCreateDeleteDispatch( + const Dispatch &dispatch) +{ + const auto obj = dispatch.body["object"].toObject(); + this->refID = obj["ref_id"].toString(); + this->kind = qmagicenum::enumCast(obj["kind"].toString()) + .value_or(CosmeticKind::INVALID); + + const auto userConnections = obj["user"]["connections"].toArray(); + for (const auto &connectionJson : userConnections) + { + const auto connection = connectionJson.toObject(); + if (connection["platform"].toString() == "TWITCH") + { + this->userID = connection["id"].toString(); + this->userName = connection["username"].toString(); + break; + } + } +} + +bool EntitlementCreateDeleteDispatch::validate() const +{ + return !this->userID.isEmpty() && !this->userName.isEmpty() && + !this->refID.isEmpty() && this->kind != CosmeticKind::INVALID; +} + +} // namespace chatterino::seventv::eventapi diff --git a/src/providers/seventv/eventapi/Dispatch.hpp b/src/providers/seventv/eventapi/Dispatch.hpp new file mode 100644 index 000000000..04bad159b --- /dev/null +++ b/src/providers/seventv/eventapi/Dispatch.hpp @@ -0,0 +1,93 @@ +#pragma once + +#include "providers/seventv/eventapi/Subscription.hpp" +#include "providers/seventv/SeventvCosmetics.hpp" + +#include +#include + +namespace chatterino::seventv::eventapi { + +// https://github.com/SevenTV/EventAPI/tree/ca4ff15cc42b89560fa661a76c5849047763d334#message-payload +struct Dispatch { + SubscriptionType type; + QJsonObject body; + QString id; + // it's okay for this to be empty + QString actorName; + + Dispatch(QJsonObject obj); +}; + +struct EmoteAddDispatch { + QString emoteSetID; + QString actorName; + QJsonObject emoteJson; + QString emoteID; + + EmoteAddDispatch(const Dispatch &dispatch, QJsonObject emote); + + bool validate() const; +}; + +struct EmoteRemoveDispatch { + QString emoteSetID; + QString actorName; + QString emoteName; + QString emoteID; + + EmoteRemoveDispatch(const Dispatch &dispatch, QJsonObject emote); + + bool validate() const; +}; + +struct EmoteUpdateDispatch { + QString emoteSetID; + QString actorName; + QString emoteID; + QString oldEmoteName; + QString emoteName; + + EmoteUpdateDispatch(const Dispatch &dispatch, QJsonObject oldValue, + QJsonObject value); + + bool validate() const; +}; + +struct UserConnectionUpdateDispatch { + QString userID; + QString actorName; + QString oldEmoteSetID; + QString emoteSetID; + size_t connectionIndex; + + UserConnectionUpdateDispatch(const Dispatch &dispatch, + const QJsonObject &update, + size_t connectionIndex); + + bool validate() const; +}; + +struct CosmeticCreateDispatch { + QJsonObject data; + CosmeticKind kind; + + CosmeticCreateDispatch(const Dispatch &dispatch); + + bool validate() const; +}; + +struct EntitlementCreateDeleteDispatch { + /** id of the user */ + QString userID; + QString userName; + /** id of the entitlement */ + QString refID; + CosmeticKind kind; + + EntitlementCreateDeleteDispatch(const Dispatch &dispatch); + + bool validate() const; +}; + +} // namespace chatterino::seventv::eventapi diff --git a/src/providers/seventv/eventapi/Message.cpp b/src/providers/seventv/eventapi/Message.cpp new file mode 100644 index 000000000..f3b59f7a9 --- /dev/null +++ b/src/providers/seventv/eventapi/Message.cpp @@ -0,0 +1,23 @@ +#include "providers/seventv/eventapi/Message.hpp" + +namespace chatterino::seventv::eventapi { + +Message::Message(QJsonObject _json) + : data(_json["d"].toObject()) + , op(Opcode(_json["op"].toInt())) +{ +} + +std::optional parseBaseMessage(const QString &blob) +{ + QJsonDocument jsonDoc(QJsonDocument::fromJson(blob.toUtf8())); + + if (jsonDoc.isNull()) + { + return std::nullopt; + } + + return Message(jsonDoc.object()); +} + +} // namespace chatterino::seventv::eventapi diff --git a/src/providers/seventv/eventapi/Message.hpp b/src/providers/seventv/eventapi/Message.hpp new file mode 100644 index 000000000..5a3eebc99 --- /dev/null +++ b/src/providers/seventv/eventapi/Message.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include "providers/seventv/eventapi/Subscription.hpp" + +#include +#include +#include +#include + +#include + +namespace chatterino::seventv::eventapi { + +struct Message { + QJsonObject data; + + Opcode op; + + Message(QJsonObject _json); + + template + std::optional toInner(); +}; + +template +std::optional Message::toInner() +{ + return InnerClass{this->data}; +} + +std::optional parseBaseMessage(const QString &blob); + +} // namespace chatterino::seventv::eventapi diff --git a/src/providers/seventv/eventapi/Subscription.cpp b/src/providers/seventv/eventapi/Subscription.cpp new file mode 100644 index 000000000..2a1a46a94 --- /dev/null +++ b/src/providers/seventv/eventapi/Subscription.cpp @@ -0,0 +1,137 @@ +#include "providers/seventv/eventapi/Subscription.hpp" + +#include "util/QMagicEnum.hpp" + +#include +#include +#include + +#include +#include + +namespace { + +using namespace chatterino; +using namespace chatterino::seventv::eventapi; + +QString typeToString(SubscriptionType type) +{ + return qmagicenum::enumNameString(type); +} + +QJsonObject createDataJson(const QString &typeName, const Condition &condition) +{ + QJsonObject data; + data["type"] = typeName; + data["condition"] = std::visit( + [](const auto &c) { + return c.encode(); + }, + condition); + return data; +} + +} // namespace + +namespace chatterino::seventv::eventapi { + +bool Subscription::operator==(const Subscription &rhs) const +{ + return std::tie(this->condition, this->type) == + std::tie(rhs.condition, rhs.type); +} + +bool Subscription::operator!=(const Subscription &rhs) const +{ + return !(rhs == *this); +} + +QByteArray Subscription::encodeSubscribe() const +{ + auto typeName = typeToString(this->type); + QJsonObject root; + root["op"] = (int)Opcode::Subscribe; + root["d"] = createDataJson(typeName, this->condition); + return QJsonDocument(root).toJson(); +} + +QByteArray Subscription::encodeUnsubscribe() const +{ + auto typeName = typeToString(this->type); + QJsonObject root; + root["op"] = (int)Opcode::Unsubscribe; + root["d"] = createDataJson(typeName, this->condition); + return QJsonDocument(root).toJson(); +} + +QDebug &operator<<(QDebug &dbg, const Subscription &subscription) +{ + std::visit( + [&](const auto &cond) { + dbg << "Subscription{ condition:" << cond + << "type:" << qmagicenum::enumName(subscription.type) << '}'; + }, + subscription.condition); + return dbg; +} + +ObjectIDCondition::ObjectIDCondition(QString objectID) + : objectID(std::move(objectID)) +{ +} + +QJsonObject ObjectIDCondition::encode() const +{ + QJsonObject obj; + obj["object_id"] = this->objectID; + + return obj; +} + +bool ObjectIDCondition::operator==(const ObjectIDCondition &rhs) const +{ + return this->objectID == rhs.objectID; +} + +bool ObjectIDCondition::operator!=(const ObjectIDCondition &rhs) const +{ + return !(*this == rhs); +} + +QDebug &operator<<(QDebug &dbg, const ObjectIDCondition &condition) +{ + dbg << "{ objectID:" << condition.objectID << "}"; + return dbg; +} + +ChannelCondition::ChannelCondition(QString twitchID) + : twitchID(std::move(twitchID)) +{ +} + +QJsonObject ChannelCondition::encode() const +{ + QJsonObject obj; + obj["ctx"] = "channel"; + obj["platform"] = "TWITCH"; + obj["id"] = this->twitchID; + return obj; +} + +QDebug &operator<<(QDebug &dbg, const ChannelCondition &condition) +{ + dbg << "{ twitchID:" << condition.twitchID << '}'; + return dbg; +} + +bool ChannelCondition::operator==(const ChannelCondition &rhs) const +{ + return this->twitchID == rhs.twitchID; +} + +bool ChannelCondition::operator!=(const ChannelCondition &rhs) const +{ + return !(*this == rhs); +} + +} // namespace chatterino::seventv::eventapi diff --git a/src/providers/seventv/eventapi/Subscription.hpp b/src/providers/seventv/eventapi/Subscription.hpp new file mode 100644 index 000000000..65cf03544 --- /dev/null +++ b/src/providers/seventv/eventapi/Subscription.hpp @@ -0,0 +1,160 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include + +namespace chatterino::seventv::eventapi { + +// https://github.com/SevenTV/EventAPI/tree/ca4ff15cc42b89560fa661a76c5849047763d334#subscription-types +enum class SubscriptionType { + AnyEmoteSet, + CreateEmoteSet, + UpdateEmoteSet, + + UpdateUser, + + AnyCosmetic, + CreateCosmetic, + UpdateCosmetic, + DeleteCosmetic, + + AnyEntitlement, + CreateEntitlement, + UpdateEntitlement, + DeleteEntitlement, + + INVALID, +}; + +// https://github.com/SevenTV/EventAPI/tree/ca4ff15cc42b89560fa661a76c5849047763d334#opcodes +enum class Opcode { + Dispatch = 0, + Hello = 1, + Heartbeat = 2, + Reconnect = 4, + Ack = 5, + Error = 6, + EndOfStream = 7, + Identify = 33, + Resume = 34, + Subscribe = 35, + Unsubscribe = 36, + Signal = 37, +}; + +struct ObjectIDCondition { + ObjectIDCondition(QString objectID); + + QString objectID; + + QJsonObject encode() const; + + friend QDebug &operator<<(QDebug &dbg, const ObjectIDCondition &condition); + bool operator==(const ObjectIDCondition &rhs) const; + bool operator!=(const ObjectIDCondition &rhs) const; +}; + +struct ChannelCondition { + ChannelCondition(QString twitchID); + + QString twitchID; + + QJsonObject encode() const; + + friend QDebug &operator<<(QDebug &dbg, const ChannelCondition &condition); + bool operator==(const ChannelCondition &rhs) const; + bool operator!=(const ChannelCondition &rhs) const; +}; + +using Condition = std::variant; + +struct Subscription { + bool operator==(const Subscription &rhs) const; + bool operator!=(const Subscription &rhs) const; + Condition condition; + SubscriptionType type; + + QByteArray encodeSubscribe() const; + QByteArray encodeUnsubscribe() const; + + friend QDebug &operator<<(QDebug &dbg, const Subscription &subscription); +}; + +} // namespace chatterino::seventv::eventapi + +template <> +constexpr magic_enum::customize::customize_t magic_enum::customize::enum_name< + chatterino::seventv::eventapi::SubscriptionType>( + chatterino::seventv::eventapi::SubscriptionType value) noexcept +{ + using chatterino::seventv::eventapi::SubscriptionType; + switch (value) + { + case SubscriptionType::AnyEmoteSet: + return "emote_set.*"; + case SubscriptionType::CreateEmoteSet: + return "emote_set.create"; + case SubscriptionType::UpdateEmoteSet: + return "emote_set.update"; + case SubscriptionType::UpdateUser: + return "user.update"; + case SubscriptionType::AnyCosmetic: + return "cosmetic.*"; + case SubscriptionType::CreateCosmetic: + return "cosmetic.create"; + case SubscriptionType::UpdateCosmetic: + return "cosmetic.update"; + case SubscriptionType::DeleteCosmetic: + return "cosmetic.delete"; + case SubscriptionType::AnyEntitlement: + return "entitlement.*"; + case SubscriptionType::CreateEntitlement: + return "entitlement.create"; + case SubscriptionType::UpdateEntitlement: + return "entitlement.update"; + case SubscriptionType::DeleteEntitlement: + return "entitlement.delete"; + + default: + return default_tag; + } +} + +namespace std { + +template <> +struct hash { + size_t operator()( + const chatterino::seventv::eventapi::ObjectIDCondition &c) const + { + return (size_t)qHash(c.objectID); + } +}; + +template <> +struct hash { + size_t operator()( + const chatterino::seventv::eventapi::ChannelCondition &c) const + { + return qHash(c.twitchID); + } +}; + +template <> +struct hash { + size_t operator()( + const chatterino::seventv::eventapi::Subscription &sub) const + { + const size_t conditionHash = + std::hash{}( + sub.condition); + return (size_t)qHash(conditionHash, qHash((int)sub.type)); + } +}; + +} // namespace std diff --git a/src/providers/twitch/ChannelPointReward.cpp b/src/providers/twitch/ChannelPointReward.cpp index 319dba8ca..8849b8b62 100644 --- a/src/providers/twitch/ChannelPointReward.cpp +++ b/src/providers/twitch/ChannelPointReward.cpp @@ -1,5 +1,17 @@ -#include "ChannelPointReward.hpp" -#include "common/QLogging.hpp" +#include "providers/twitch/ChannelPointReward.hpp" + +#include "messages/Image.hpp" + +#include + +namespace { + +QString twitchChannelPointRewardUrl(const QString &file) +{ + return u"https://static-cdn.jtvnw.net/custom-reward-images/default-" % file; +} + +} // namespace namespace chatterino { @@ -12,6 +24,47 @@ ChannelPointReward::ChannelPointReward(const QJsonObject &redemption) this->title = reward.value("title").toString(); this->cost = reward.value("cost").toInt(); this->isUserInputRequired = reward.value("is_user_input_required").toBool(); + this->isBits = reward.value("pricing_type").toString() == "BITS"; + + // accommodate idiosyncrasies of automatic reward redemptions + const auto rewardType = reward.value("reward_type").toString(); + if (rewardType == "SEND_ANIMATED_MESSAGE") + { + this->id = "animated-message"; + this->isUserInputRequired = true; + this->title = "Message Effects"; + } + else if (rewardType == "SEND_GIGANTIFIED_EMOTE") + { + this->id = "gigantified-emote-message"; + this->isUserInputRequired = true; + this->title = "Gigantify an Emote"; + } + else if (rewardType == "CELEBRATION") + { + this->id = rewardType; + this->title = "On-Screen Celebration"; + const auto metadata = + redemption.value("redemption_metadata").toObject(); + const auto emote = metadata.value("celebration_emote_metadata") + .toObject() + .value("emote") + .toObject(); + this->emoteId = emote.value("id").toString(); + this->emoteName = emote.value("token").toString(); + } + + // use bits cost when channel points were not used + if (cost == 0) + { + this->cost = reward.value("bits_cost").toInt(); + } + + // workaround twitch bug where bits_cost is always 0 in practice + if (cost == 0) + { + this->cost = reward.value("default_bits_cost").toInt(); + } // We don't need to store user information for rewards with user input // because we will get the user info from a corresponding IRC message @@ -26,21 +79,36 @@ ChannelPointReward::ChannelPointReward(const QJsonObject &redemption) auto imageValue = reward.value("image"); + // automatic reward redemptions have specialized default images + if (imageValue.isNull() && this->isBits) + { + imageValue = reward.value("default_image"); + } + + // From Twitch docs + // The size is only an estimation, the actual size might vary. + constexpr QSize baseSize(28, 28); + if (imageValue.isObject()) { auto imageObject = imageValue.toObject(); this->image = ImageSet{ - Image::fromUrl({imageObject.value("url_1x").toString()}, 1), - Image::fromUrl({imageObject.value("url_2x").toString()}, 0.5), - Image::fromUrl({imageObject.value("url_4x").toString()}, 0.25), + Image::fromUrl({imageObject.value("url_1x").toString()}, 1, + baseSize), + Image::fromUrl({imageObject.value("url_2x").toString()}, 0.5, + baseSize * 2), + Image::fromUrl({imageObject.value("url_4x").toString()}, 0.25, + baseSize * 4), }; } else { static const ImageSet defaultImage{ - Image::fromUrl({TWITCH_CHANNEL_POINT_REWARD_URL("1.png")}, 1), - Image::fromUrl({TWITCH_CHANNEL_POINT_REWARD_URL("2.png")}, 0.5), - Image::fromUrl({TWITCH_CHANNEL_POINT_REWARD_URL("4.png")}, 0.25)}; + Image::fromUrl({twitchChannelPointRewardUrl("1.png")}, 1, baseSize), + Image::fromUrl({twitchChannelPointRewardUrl("2.png")}, 0.5, + baseSize * 2), + Image::fromUrl({twitchChannelPointRewardUrl("4.png")}, 0.25, + baseSize * 4)}; this->image = defaultImage; } } diff --git a/src/providers/twitch/ChannelPointReward.hpp b/src/providers/twitch/ChannelPointReward.hpp index fad2ed375..6fdd985b6 100644 --- a/src/providers/twitch/ChannelPointReward.hpp +++ b/src/providers/twitch/ChannelPointReward.hpp @@ -1,16 +1,11 @@ #pragma once -#include "common/Aliases.hpp" -#include "messages/Image.hpp" #include "messages/ImageSet.hpp" #include -#define TWITCH_CHANNEL_POINT_REWARD_URL(x) \ - QString("https://static-cdn.jtvnw.net/custom-reward-images/default-%1") \ - .arg(x) - namespace chatterino { + struct ChannelPointReward { ChannelPointReward(const QJsonObject &redemption); ChannelPointReward() = delete; @@ -20,6 +15,9 @@ struct ChannelPointReward { int cost; ImageSet image; bool isUserInputRequired = false; + bool isBits = false; + QString emoteId; // currently only for celebrations + QString emoteName; // currently only for celebrations struct { QString id; diff --git a/src/providers/twitch/ChatterinoWebSocketppLogger.hpp b/src/providers/twitch/ChatterinoWebSocketppLogger.hpp index 951149dd6..c846affab 100644 --- a/src/providers/twitch/ChatterinoWebSocketppLogger.hpp +++ b/src/providers/twitch/ChatterinoWebSocketppLogger.hpp @@ -25,175 +25,170 @@ * */ -#ifndef CHATTERINOWEBSOCKETPPLOGGER_HPP -#define CHATTERINOWEBSOCKETPPLOGGER_HPP +#pragma once + +#include "common/QLogging.hpp" -#include #include #include #include -#include "common/QLogging.hpp" -namespace websocketpp { -namespace log { +#include - template - class chatterinowebsocketpplogger : public basic +namespace websocketpp::log { + +template +class chatterinowebsocketpplogger : public basic +{ +public: + using base = chatterinowebsocketpplogger; + + chatterinowebsocketpplogger(channel_type_hint::value) + : m_static_channels(0xffffffff) + , m_dynamic_channels(0) { - public: - typedef chatterinowebsocketpplogger base; + } - chatterinowebsocketpplogger( - channel_type_hint::value) - : m_static_channels(0xffffffff) - , m_dynamic_channels(0) - { - } + chatterinowebsocketpplogger(std::ostream *) + : m_static_channels(0xffffffff) + , m_dynamic_channels(0) + { + } - chatterinowebsocketpplogger(std::ostream *) - : m_static_channels(0xffffffff) - , m_dynamic_channels(0) - { - } + chatterinowebsocketpplogger(level c, channel_type_hint::value) + : m_static_channels(c) + , m_dynamic_channels(0) + { + } - chatterinowebsocketpplogger( - level c, channel_type_hint::value) - : m_static_channels(c) - , m_dynamic_channels(0) - { - } + chatterinowebsocketpplogger(level c, std::ostream *) + : m_static_channels(c) + , m_dynamic_channels(0) + { + } - chatterinowebsocketpplogger(level c, std::ostream *) - : m_static_channels(c) - , m_dynamic_channels(0) - { - } + ~chatterinowebsocketpplogger() + { + } - ~chatterinowebsocketpplogger() - { - } - - chatterinowebsocketpplogger( - chatterinowebsocketpplogger const &other) - : m_static_channels(other.m_static_channels) - , m_dynamic_channels(other.m_dynamic_channels) - { - } + chatterinowebsocketpplogger( + chatterinowebsocketpplogger const &other) + : m_static_channels(other.m_static_channels) + , m_dynamic_channels(other.m_dynamic_channels) + { + } #ifdef _WEBSOCKETPP_DEFAULT_DELETE_FUNCTIONS_ - chatterinowebsocketpplogger &operator=( - chatterinowebsocketpplogger const &) = delete; + chatterinowebsocketpplogger &operator=( + chatterinowebsocketpplogger const &) = delete; #endif // _WEBSOCKETPP_DEFAULT_DELETE_FUNCTIONS_ #ifdef _WEBSOCKETPP_MOVE_SEMANTICS_ - /// Move constructor - chatterinowebsocketpplogger( - chatterinowebsocketpplogger &&other) - : m_static_channels(other.m_static_channels) - , m_dynamic_channels(other.m_dynamic_channels) - { - } + /// Move constructor + chatterinowebsocketpplogger( + chatterinowebsocketpplogger &&other) + : m_static_channels(other.m_static_channels) + , m_dynamic_channels(other.m_dynamic_channels) + { + } # ifdef _WEBSOCKETPP_DEFAULT_DELETE_FUNCTIONS_ - // no move assignment operator because of const member variables - chatterinowebsocketpplogger &operator=( - chatterinowebsocketpplogger &&) = delete; + // no move assignment operator because of const member variables + chatterinowebsocketpplogger &operator=( + chatterinowebsocketpplogger &&) = delete; # endif // _WEBSOCKETPP_DEFAULT_DELETE_FUNCTIONS_ #endif // _WEBSOCKETPP_MOVE_SEMANTICS_ - /// Explicitly do nothing, this logger doesn't support changing ostream - void set_ostream(std::ostream *) - { - } + /// Explicitly do nothing, this logger doesn't support changing ostream + void set_ostream(std::ostream *) + { + } - /// Dynamically enable the given list of channels - /** + /// Dynamically enable the given list of channels + /** * @param channels The package of channels to enable */ - void set_channels(level channels) + void set_channels(level channels) + { + if (channels == names::none) { - if (channels == names::none) - { - clear_channels(names::all); - return; - } - - scoped_lock_type lock(m_lock); - m_dynamic_channels |= (channels & m_static_channels); + clear_channels(names::all); + return; } - /// Dynamically disable the given list of channels - /** + scoped_lock_type lock(m_lock); + m_dynamic_channels |= (channels & m_static_channels); + } + + /// Dynamically disable the given list of channels + /** * @param channels The package of channels to disable */ - void clear_channels(level channels) - { - scoped_lock_type lock(m_lock); - m_dynamic_channels &= ~channels; - } + void clear_channels(level channels) + { + scoped_lock_type lock(m_lock); + m_dynamic_channels &= ~channels; + } - /// Write a string message to the given channel - /** + /// Write a string message to the given channel + /** * @param channel The channel to write to * @param msg The message to write */ - void write(level channel, std::string const &msg) + void write(level channel, std::string const &msg) + { + scoped_lock_type lock(m_lock); + if (!this->dynamic_test(channel)) { - scoped_lock_type lock(m_lock); - if (!this->dynamic_test(channel)) - { - return; - } - qCDebug(chatterinoWebsocket).nospace() - << names::channel_name(channel) << ": " - << QString::fromStdString(msg); + return; } + qCDebug(chatterinoWebsocket).nospace() + << names::channel_name(channel) << ": " + << QString::fromStdString(msg); + } - /// Write a cstring message to the given channel - /** + /// Write a cstring message to the given channel + /** * @param channel The channel to write to * @param msg The message to write */ - void write(level channel, char const *msg) + void write(level channel, char const *msg) + { + scoped_lock_type lock(m_lock); + if (!this->dynamic_test(channel)) { - scoped_lock_type lock(m_lock); - if (!this->dynamic_test(channel)) - { - return; - } - qCDebug(chatterinoWebsocket).nospace() - << names::channel_name(channel) << ": " << msg; + return; } + qCDebug(chatterinoWebsocket).nospace() + << names::channel_name(channel) << ": " << msg; + } - /// Test whether a channel is statically enabled - /** + /// Test whether a channel is statically enabled + /** * @param channel The package of channels to test */ - _WEBSOCKETPP_CONSTEXPR_TOKEN_ bool static_test(level channel) const - { - return ((channel & m_static_channels) != 0); - } + _WEBSOCKETPP_CONSTEXPR_TOKEN_ bool static_test(level channel) const + { + return ((channel & m_static_channels) != 0); + } - /// Test whether a channel is dynamically enabled - /** + /// Test whether a channel is dynamically enabled + /** * @param channel The package of channels to test */ - bool dynamic_test(level channel) - { - return ((channel & m_dynamic_channels) != 0); - } + bool dynamic_test(level channel) + { + return ((channel & m_dynamic_channels) != 0); + } - protected: - typedef typename concurrency::scoped_lock_type scoped_lock_type; - typedef typename concurrency::mutex_type mutex_type; - mutex_type m_lock; +protected: + using scoped_lock_type = typename concurrency::scoped_lock_type; + using mutex_type = typename concurrency::mutex_type; + mutex_type m_lock; - private: - level const m_static_channels; - level m_dynamic_channels; - }; +private: + level const m_static_channels; + level m_dynamic_channels; +}; -} // namespace log -} // namespace websocketpp - -#endif // CHATTERINOWEBSOCKETPPLOGGER_HPP +} // namespace websocketpp::log diff --git a/src/providers/twitch/IrcMessageHandler.cpp b/src/providers/twitch/IrcMessageHandler.cpp index 5c6013360..35ec83761 100644 --- a/src/providers/twitch/IrcMessageHandler.cpp +++ b/src/providers/twitch/IrcMessageHandler.cpp @@ -1,39 +1,60 @@ -#include "IrcMessageHandler.hpp" +#include "providers/twitch/IrcMessageHandler.hpp" #include "Application.hpp" +#include "common/Channel.hpp" +#include "common/Common.hpp" +#include "common/Literals.hpp" #include "common/QLogging.hpp" #include "controllers/accounts/AccountController.hpp" +#include "controllers/ignores/IgnoreController.hpp" #include "messages/LimitedQueue.hpp" +#include "messages/Link.hpp" #include "messages/Message.hpp" +#include "messages/MessageBuilder.hpp" +#include "messages/MessageColor.hpp" +#include "messages/MessageElement.hpp" +#include "messages/MessageThread.hpp" +#include "providers/twitch/ChannelPointReward.hpp" +#include "providers/twitch/TwitchAccount.hpp" #include "providers/twitch/TwitchAccountManager.hpp" #include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchHelpers.hpp" #include "providers/twitch/TwitchIrcServer.hpp" #include "singletons/Resources.hpp" #include "singletons/Settings.hpp" +#include "singletons/StreamerMode.hpp" #include "singletons/WindowManager.hpp" +#include "util/ChannelHelpers.hpp" #include "util/FormatTime.hpp" #include "util/Helpers.hpp" #include "util/IrcHelpers.hpp" #include +#include +#include #include #include +using namespace chatterino::literals; + namespace { + using namespace chatterino; // Message types below are the ones that might contain special user's message on USERNOTICE -static const QSet specialMessageTypes{ - "sub", // - "subgift", // - "resub", // resub messages - "bitsbadgetier", // bits badge upgrade - "ritual", // new viewer ritual - "announcement", // new mod announcement thing +const QSet SPECIAL_MESSAGE_TYPES{ + "sub", // + "subgift", // + "resub", // resub messages + "bitsbadgetier", // bits badge upgrade + "ritual", // new viewer ritual + "announcement", // new mod announcement thing + "viewermilestone", // watch streak, but other categories possible in future }; +const QString ANONYMOUS_GIFTER_ID = "274598607"; + MessagePtr generateBannedMessage(bool confirmedBan) { const auto linkColor = MessageColor(MessageColor::Link); @@ -67,10 +88,99 @@ MessagePtr generateBannedMessage(bool confirmedBan) return builder.release(); } -} // namespace -namespace chatterino { +int stripLeadingReplyMention(const QVariantMap &tags, QString &content) +{ + if (!getSettings()->stripReplyMention) + { + return 0; + } + if (getSettings()->hideReplyContext) + { + // Never strip reply mentions if reply contexts are hidden + return 0; + } -static float relativeSimilarity(const QString &str1, const QString &str2) + if (const auto it = tags.find("reply-parent-display-name"); + it != tags.end()) + { + auto displayName = it.value().toString(); + + if (content.length() <= 1 + displayName.length()) + { + // The reply contains no content + return 0; + } + + if (content.startsWith('@') && + content.at(1 + displayName.length()) == ' ' && + content.indexOf(displayName, 1) == 1) + { + int messageOffset = 1 + displayName.length() + 1; + content.remove(0, messageOffset); + return messageOffset; + } + } + return 0; +} + +void updateReplyParticipatedStatus(const QVariantMap &tags, + const QString &senderLogin, + MessageBuilder &builder, + std::shared_ptr &thread, + bool isNew) +{ + const auto ¤tLogin = + getApp()->getAccounts()->twitch.getCurrent()->getUserName(); + + if (thread->subscribed()) + { + builder.message().flags.set(MessageFlag::SubscribedThread); + return; + } + + if (thread->unsubscribed()) + { + return; + } + + if (getSettings()->autoSubToParticipatedThreads) + { + if (isNew) + { + if (const auto it = tags.find("reply-parent-user-login"); + it != tags.end()) + { + auto name = it.value().toString(); + if (name == currentLogin) + { + thread->markSubscribed(); + builder.message().flags.set(MessageFlag::SubscribedThread); + return; // already marked as participated + } + } + } + + if (senderLogin == currentLogin) + { + thread->markSubscribed(); + // don't set the highlight here + } + } +} + +ChannelPtr channelOrEmptyByTarget(const QString &target, + ITwitchIrcServer &server) +{ + QString channelName; + if (!trimChannelName(target, channelName)) + { + return Channel::getEmpty(); + } + + return server.getChannelOrEmpty(channelName); +} + +float relativeSimilarity(const QString &str1, const QString &str2) { // Longest Common Substring Problem std::vector> tree(str1.size(), @@ -104,67 +214,17 @@ static float relativeSimilarity(const QString &str1, const QString &str2) } // ensure that no div by 0 - return z == 0 ? 0.f - : float(z) / - std::max(1, std::max(str1.size(), str2.size())); -}; - -float IrcMessageHandler::similarity( - MessagePtr msg, const LimitedQueueSnapshot &messages) -{ - float similarityPercent = 0.0f; - int checked = 0; - for (int i = 1; i <= messages.size(); ++i) + if (z == 0) { - if (checked >= getSettings()->hideSimilarMaxMessagesToCheck) - { - break; - } - const auto &prevMsg = messages[messages.size() - i]; - if (prevMsg->parseTime.secsTo(QTime::currentTime()) >= - getSettings()->hideSimilarMaxDelay) - { - break; - } - if (getSettings()->hideSimilarBySameUser && - msg->loginName != prevMsg->loginName) - { - continue; - } - ++checked; - similarityPercent = std::max( - similarityPercent, - relativeSimilarity(msg->messageText, prevMsg->messageText)); + return 0.F; } - return similarityPercent; + + auto div = std::max(1, std::max(str1.size(), str2.size())); + + return float(z) / float(div); } -void IrcMessageHandler::setSimilarityFlags(MessagePtr msg, ChannelPtr chan) -{ - if (getSettings()->similarityEnabled) - { - bool isMyself = msg->loginName == - getApp()->accounts->twitch.getCurrent()->getUserName(); - bool hideMyself = getSettings()->hideSimilarMyself; - - if (isMyself && !hideMyself) - { - return; - } - - if (IrcMessageHandler::similarity(msg, chan->getMessageSnapshot()) > - getSettings()->similarityPercentage) - { - msg->flags.set(MessageFlag::Similar, true); - if (getSettings()->colorSimilarDisabled) - { - msg->flags.set(MessageFlag::Disabled, true); - } - } - } -} - -static QMap parseBadges(QString badgesString) +QMap parseBadges(const QString &badgesString) { QMap badges; @@ -182,277 +242,482 @@ static QMap parseBadges(QString badgesString) return badges; } +void populateReply(TwitchChannel *channel, Communi::IrcMessage *message, + const std::vector &otherLoaded, + MessageBuilder &builder) +{ + const auto &tags = message->tags(); + if (const auto it = tags.find("reply-thread-parent-msg-id"); + it != tags.end()) + { + const QString replyID = it.value().toString(); + auto threadIt = channel->threads().find(replyID); + std::shared_ptr rootThread; + if (threadIt != channel->threads().end()) + { + auto owned = threadIt->second.lock(); + if (owned) + { + // Thread already exists (has a reply) + updateReplyParticipatedStatus(tags, message->nick(), builder, + owned, false); + builder.setThread(owned); + rootThread = owned; + } + } + + if (!rootThread) + { + MessagePtr foundMessage; + + // Thread does not yet exist, find root reply and create thread. + // Linear search is justified by the infrequent use of replies + for (const auto &otherMsg : otherLoaded) + { + if (otherMsg->id == replyID) + { + // Found root reply message + foundMessage = otherMsg; + break; + } + } + + if (!foundMessage) + { + // We didn't find the reply root message in the otherLoaded messages + // which are typically the already-parsed recent messages from the + // Recent Messages API. We could have a really old message that + // still exists being replied to, so check for that here. + foundMessage = channel->findMessage(replyID); + } + + if (foundMessage) + { + std::shared_ptr newThread = + std::make_shared(foundMessage); + updateReplyParticipatedStatus(tags, message->nick(), builder, + newThread, true); + + builder.setThread(newThread); + rootThread = newThread; + // Store weak reference to thread in channel + channel->addReplyThread(newThread); + } + } + + if (const auto parentIt = tags.find("reply-parent-msg-id"); + parentIt != tags.end()) + { + const QString parentID = parentIt.value().toString(); + if (replyID == parentID) + { + if (rootThread) + { + builder.setParent(rootThread->root()); + } + } + else + { + auto parentThreadIt = channel->threads().find(parentID); + if (parentThreadIt != channel->threads().end()) + { + auto thread = parentThreadIt->second.lock(); + if (thread) + { + builder.setParent(thread->root()); + } + } + else + { + auto parent = channel->findMessage(parentID); + if (parent) + { + builder.setParent(parent); + } + } + } + } + } +} + +std::optional parseClearChatMessage( + Communi::IrcMessage *message) +{ + // check parameter count + if (message->parameters().length() < 1) + { + return std::nullopt; + } + + // check if the chat has been cleared by a moderator + if (message->parameters().length() == 1) + { + return ClearChatMessage{ + .message = + makeSystemMessage("Chat has been cleared by a moderator.", + calculateMessageTime(message).time()), + .disableAllMessages = true, + }; + } + + // get username, duration and message of the timed out user + QString username = message->parameter(1); + QString durationInSeconds; + QVariant v = message->tag("ban-duration"); + if (v.isValid()) + { + durationInSeconds = v.toString(); + } + + auto timeoutMsg = + MessageBuilder(timeoutMessage, username, durationInSeconds, false, + calculateMessageTime(message).time()) + .release(); + + return ClearChatMessage{.message = timeoutMsg, .disableAllMessages = false}; +} + +/** + * Parse a single IRC NOTICE message into 0 or more Chatterino messages + **/ +std::vector parseNoticeMessage(Communi::IrcNoticeMessage *message) +{ + assert(message != nullptr); + + if (message->content().startsWith("Login auth", Qt::CaseInsensitive)) + { + const auto linkColor = MessageColor(MessageColor::Link); + const auto accountsLink = Link(Link::OpenAccountsPage, QString()); + const auto curUser = getApp()->getAccounts()->twitch.getCurrent(); + const auto expirationText = QString("Login expired for user \"%1\"!") + .arg(curUser->getUserName()); + const auto loginPromptText = QString("Try adding your account again."); + + MessageBuilder builder; + auto text = QString("%1 %2").arg(expirationText, loginPromptText); + builder.message().messageText = text; + builder.message().searchText = text; + builder.message().flags.set(MessageFlag::System); + builder.message().flags.set(MessageFlag::DoNotTriggerNotification); + + builder.emplace(); + builder.emplace(expirationText, MessageElementFlag::Text, + MessageColor::System); + builder + .emplace(loginPromptText, MessageElementFlag::Text, + linkColor) + ->setLink(accountsLink); + + return {builder.release()}; + } + + if (message->content().startsWith("You are permanently banned ")) + { + return {generateBannedMessage(true)}; + } + + if (message->tags().value("msg-id") == "msg_timedout") + { + std::vector builtMessage; + + QString remainingTime = + formatTime(message->content().split(" ").value(5)); + QString formattedMessage = + QString("You are timed out for %1.") + .arg(remainingTime.isEmpty() ? "0s" : remainingTime); + + builtMessage.emplace_back(makeSystemMessage( + formattedMessage, calculateMessageTime(message).time())); + + return builtMessage; + } + + // default case + std::vector builtMessages; + + builtMessages.emplace_back(makeSystemMessage( + message->content(), calculateMessageTime(message).time())); + + return builtMessages; +} + +/** + * Parse a single IRC USERNOTICE message into 0 or more Chatterino messages + **/ +std::vector parseUserNoticeMessage(Channel *channel, + Communi::IrcMessage *message) +{ + assert(channel != nullptr); + assert(message != nullptr); + + std::vector builtMessages; + + auto tags = message->tags(); + auto parameters = message->parameters(); + + QString msgType = tags.value("msg-id").toString(); + QString content; + if (parameters.size() >= 2) + { + content = parameters[1]; + } + + if (isIgnoredMessage({ + .message = content, + .twitchUserID = tags.value("user-id").toString(), + .isMod = channel->isMod(), + .isBroadcaster = channel->isBroadcaster(), + })) + { + return {}; + } + + if (SPECIAL_MESSAGE_TYPES.contains(msgType)) + { + // Messages are not required, so they might be empty + if (!content.isEmpty()) + { + MessageParseArgs args; + args.trimSubscriberUsername = true; + + MessageBuilder builder(channel, message, args, content, false); + builder->flags.set(MessageFlag::Subscription); + builder->flags.unset(MessageFlag::Highlighted); + builtMessages.emplace_back(builder.build()); + } + } + + auto it = tags.find("system-msg"); + + if (it != tags.end()) + { + // By default, we return value of system-msg tag + QString messageText = it.value().toString(); + + if (msgType == "bitsbadgetier") + { + messageText = + QString("%1 just earned a new %2 Bits badge!") + .arg(tags.value("display-name").toString(), + kFormatNumbers( + tags.value("msg-param-threshold").toInt())); + } + else if (msgType == "announcement") + { + messageText = "Announcement"; + } + else if (msgType == "subgift") + { + if (auto monthsIt = tags.find("msg-param-gift-months"); + monthsIt != tags.end()) + { + int months = monthsIt.value().toInt(); + if (months > 1) + { + auto plan = tags.value("msg-param-sub-plan").toString(); + QString name = + ANONYMOUS_GIFTER_ID == tags.value("user-id").toString() + ? "An anonymous user" + : tags.value("display-name").toString(); + messageText = + QString("%1 gifted %2 months of a Tier %3 sub to %4!") + .arg(name, QString::number(months), + plan.isEmpty() ? '1' : plan.at(0), + tags.value("msg-param-recipient-display-name") + .toString()); + + if (auto countIt = tags.find("msg-param-sender-count"); + countIt != tags.end()) + { + int count = countIt.value().toInt(); + if (count > months) + { + messageText += + QString( + " They've gifted %1 months in the channel.") + .arg(QString::number(count)); + } + } + } + } + } + + auto b = MessageBuilder(systemMessage, parseTagString(messageText), + calculateMessageTime(message).time()); + + b->flags.set(MessageFlag::Subscription); + auto newMessage = b.release(); + builtMessages.emplace_back(newMessage); + } + + return builtMessages; +} + +/** + * Parse a single IRC PRIVMSG into 0-1 Chatterino messages + */ +std::vector parsePrivMessage(Channel *channel, + Communi::IrcPrivateMessage *message) +{ + assert(channel != nullptr); + assert(message != nullptr); + + std::vector builtMessages; + MessageParseArgs args; + MessageBuilder builder(channel, message, args, message->content(), + message->isAction()); + if (!builder.isIgnored()) + { + builtMessages.emplace_back(builder.build()); + builder.triggerHighlights(); + } + + if (message->tags().contains(u"pinned-chat-paid-amount"_s)) + { + auto ptr = MessageBuilder::buildHypeChatMessage(message); + if (ptr) + { + builtMessages.emplace_back(std::move(ptr)); + } + } + + return builtMessages; +} + +} // namespace + +namespace chatterino { + +using namespace literals; + IrcMessageHandler &IrcMessageHandler::instance() { static IrcMessageHandler instance; return instance; } -std::vector IrcMessageHandler::parseMessage( - Channel *channel, Communi::IrcMessage *message) -{ - std::vector builtMessages; - - auto command = message->command(); - - if (command == "PRIVMSG") - { - return this->parsePrivMessage( - channel, static_cast(message)); - } - else if (command == "USERNOTICE") - { - return this->parseUserNoticeMessage(channel, message); - } - else if (command == "NOTICE") - { - return this->parseNoticeMessage( - static_cast(message)); - } - - return builtMessages; -} - -std::vector IrcMessageHandler::parsePrivMessage( - Channel *channel, Communi::IrcPrivateMessage *message) -{ - std::vector builtMessages; - MessageParseArgs args; - TwitchMessageBuilder builder(channel, message, args, message->content(), - message->isAction()); - if (!builder.isIgnored()) - { - builtMessages.emplace_back(builder.build()); - builder.triggerHighlights(); - } - return builtMessages; -} - -void IrcMessageHandler::handlePrivMessage(Communi::IrcPrivateMessage *message, - TwitchIrcServer &server) -{ - // This is to make sure that combined emoji go through properly, see - // https://github.com/Chatterino/chatterino2/issues/3384 and - // https://mm2pl.github.io/emoji_rfc.pdf for more details - // Constants used here are defined in TwitchChannel.hpp - - this->addMessage( - message, message->target(), - message->content().replace(COMBINED_FIXER, ZERO_WIDTH_JOINER), server, - false, message->isAction()); -} - std::vector IrcMessageHandler::parseMessageWithReply( Channel *channel, Communi::IrcMessage *message, - const std::vector &otherLoaded) + std::vector &otherLoaded) { std::vector builtMessages; auto command = message->command(); - if (command == "PRIVMSG") + if (command == u"PRIVMSG"_s) { - auto privMsg = static_cast(message); - auto tc = dynamic_cast(channel); + auto *privMsg = dynamic_cast(message); + auto *tc = dynamic_cast(channel); if (!tc) { - return this->parsePrivMessage(channel, privMsg); + return parsePrivMessage(channel, privMsg); } + QString content = privMsg->content(); + int messageOffset = stripLeadingReplyMention(privMsg->tags(), content); MessageParseArgs args; - TwitchMessageBuilder builder(channel, message, args, privMsg->content(), - privMsg->isAction()); + MessageBuilder builder(channel, message, args, content, + privMsg->isAction()); + builder.setMessageOffset(messageOffset); - this->populateReply(tc, message, otherLoaded, builder); + populateReply(tc, message, otherLoaded, builder); if (!builder.isIgnored()) { builtMessages.emplace_back(builder.build()); builder.triggerHighlights(); } + + return builtMessages; } - else if (command == "USERNOTICE") + + if (command == u"USERNOTICE"_s) { - return this->parseUserNoticeMessage(channel, message); + return parseUserNoticeMessage(channel, message); } - else if (command == "NOTICE") + + if (command == u"NOTICE"_s) { - return this->parseNoticeMessage( - static_cast(message)); + return parseNoticeMessage( + dynamic_cast(message)); + } + + if (command == u"CLEARCHAT"_s) + { + auto cc = parseClearChatMessage(message); + if (!cc) + { + return builtMessages; + } + auto &clearChat = *cc; + if (clearChat.disableAllMessages) + { + builtMessages.emplace_back(std::move(clearChat.message)); + } + else + { + addOrReplaceChannelTimeout( + otherLoaded, std::move(clearChat.message), + calculateMessageTime(message).time(), + [&](auto idx, auto /*msg*/, auto &&replacement) { + replacement->flags.set(MessageFlag::RecentMessage); + otherLoaded[idx] = replacement; + }, + [&](auto &&msg) { + builtMessages.emplace_back(msg); + }, + false); + } + + return builtMessages; } return builtMessages; } -void IrcMessageHandler::populateReply( - TwitchChannel *channel, Communi::IrcMessage *message, - const std::vector &otherLoaded, TwitchMessageBuilder &builder) +void IrcMessageHandler::handlePrivMessage(Communi::IrcPrivateMessage *message, + ITwitchIrcServer &twitchServer) { - const auto &tags = message->tags(); - if (const auto it = tags.find("reply-parent-msg-id"); it != tags.end()) - { - const QString replyID = it.value().toString(); - auto threadIt = channel->threads_.find(replyID); - if (threadIt != channel->threads_.end()) - { - const auto owned = threadIt->second.lock(); - if (owned) - { - // Thread already exists (has a reply) - builder.setThread(owned); - return; - } - } - - MessagePtr foundMessage; - - // Thread does not yet exist, find root reply and create thread. - // Linear search is justified by the infrequent use of replies - for (auto &otherMsg : otherLoaded) - { - if (otherMsg->id == replyID) - { - // Found root reply message - foundMessage = otherMsg; - break; - } - } - - if (!foundMessage) - { - // We didn't find the reply root message in the otherLoaded messages - // which are typically the already-parsed recent messages from the - // Recent Messages API. We could have a really old message that - // still exists being replied to, so check for that here. - foundMessage = channel->findMessage(replyID); - } - - if (foundMessage) - { - std::shared_ptr newThread = - std::make_shared(foundMessage); - - builder.setThread(newThread); - // Store weak reference to thread in channel - channel->addReplyThread(newThread); - } - } -} - -void IrcMessageHandler::addMessage(Communi::IrcMessage *_message, - const QString &target, - const QString &content, - TwitchIrcServer &server, bool isSub, - bool isAction) -{ - QString channelName; - if (!trimChannelName(target, channelName)) - { - return; - } - - auto chan = server.getChannelOrEmpty(channelName); - + auto chan = channelOrEmptyByTarget(message->target(), twitchServer); if (chan->isEmpty()) { return; } - MessageParseArgs args; - if (isSub) - { - args.isSubscriptionMessage = true; - args.trimSubscriberUsername = true; - } + auto *twitchChannel = dynamic_cast(chan.get()); - if (chan->isBroadcaster()) + if (twitchChannel != nullptr) { - args.isStaffOrBroadcaster = true; - } - - auto channel = dynamic_cast(chan.get()); - - const auto &tags = _message->tags(); - if (const auto it = tags.find("custom-reward-id"); it != tags.end()) - { - const auto rewardId = it.value().toString(); - if (!channel->isChannelPointRewardKnown(rewardId)) + auto currentUser = getApp()->getAccounts()->twitch.getCurrent(); + if (message->tag("user-id") == currentUser->getUserId()) { - // Need to wait for pubsub reward notification - auto clone = _message->clone(); - channel->channelPointRewardAdded.connect( - [=, &server](ChannelPointReward reward) { - if (reward.id == rewardId) - { - this->addMessage(clone, target, content, server, isSub, - isAction); - clone->deleteLater(); - return true; - } - return false; - }); - return; - } - args.channelPointRewardId = rewardId; - } - - TwitchMessageBuilder builder(chan.get(), _message, args, content, isAction); - - if (const auto it = tags.find("reply-parent-msg-id"); it != tags.end()) - { - const QString replyID = it.value().toString(); - auto threadIt = channel->threads_.find(replyID); - if (threadIt != channel->threads_.end() && !threadIt->second.expired()) - { - // Thread already exists (has a reply) - builder.setThread(threadIt->second.lock()); - } - else - { - // Thread does not yet exist, find root reply and create thread. - auto root = channel->findMessage(replyID); - if (root) + auto badgesTag = message->tag("badges"); + if (badgesTag.isValid()) { - // Found root reply message - const auto newThread = std::make_shared(root); - - builder.setThread(newThread); - // Store weak reference to thread in channel - channel->addReplyThread(newThread); + auto parsedBadges = parseBadges(badgesTag.toString()); + twitchChannel->setMod(parsedBadges.contains("moderator")); + twitchChannel->setVIP(parsedBadges.contains("vip")); + twitchChannel->setStaff(parsedBadges.contains("staff")); } } } - if (isSub || !builder.isIgnored()) + // This is for compatibility with older Chatterino versions. Twitch didn't use + // to allow ZERO WIDTH JOINER unicode character, so Chatterino used ESCAPE_TAG + // instead. + // See https://github.com/Chatterino/chatterino2/issues/3384 and + // https://mm2pl.github.io/emoji_rfc.pdf for more details + this->addMessage( + message, chan, + message->content().replace(COMBINED_FIXER, ZERO_WIDTH_JOINER), + twitchServer, false, message->isAction()); + + if (message->tags().contains(u"pinned-chat-paid-amount"_s)) { - if (isSub) + auto ptr = MessageBuilder::buildHypeChatMessage(message); + if (ptr) { - builder->flags.set(MessageFlag::Subscription); - builder->flags.unset(MessageFlag::Highlighted); - } - auto msg = builder.build(); - - IrcMessageHandler::setSimilarityFlags(msg, chan); - - if (!msg->flags.has(MessageFlag::Similar) || - (!getSettings()->hideSimilar && - getSettings()->shownSimilarTriggerHighlights)) - { - builder.triggerHighlights(); - } - - const auto highlighted = msg->flags.has(MessageFlag::Highlighted); - const auto showInMentions = msg->flags.has(MessageFlag::ShowInMentions); - - if (highlighted && showInMentions) - { - server.mentionsChannel->addMessage(msg); - } - - chan->addMessage(msg); - if (auto chatters = dynamic_cast(chan.get())) - { - chatters->addRecentChatter(msg->displayName); + chan->addMessage(ptr, MessageContext::Original); } } } @@ -467,7 +732,7 @@ void IrcMessageHandler::handleRoomStateMessage(Communi::IrcMessage *message) { return; } - auto chan = getApp()->twitch->getChannelOrEmpty(chanName); + auto chan = getApp()->getTwitch()->getChannelOrEmpty(chanName); auto *twitchChannel = dynamic_cast(chan.get()); if (!twitchChannel) @@ -515,11 +780,12 @@ void IrcMessageHandler::handleRoomStateMessage(Communi::IrcMessage *message) void IrcMessageHandler::handleClearChatMessage(Communi::IrcMessage *message) { - // check parameter count - if (message->parameters().length() < 1) + auto cc = parseClearChatMessage(message); + if (!cc) { return; } + auto &clearChat = *cc; QString chanName; if (!trimChannelName(message->parameter(0), chanName)) @@ -528,47 +794,33 @@ void IrcMessageHandler::handleClearChatMessage(Communi::IrcMessage *message) } // get channel - auto chan = getApp()->twitch->getChannelOrEmpty(chanName); + auto chan = getApp()->getTwitch()->getChannelOrEmpty(chanName); if (chan->isEmpty()) { qCDebug(chatterinoTwitch) - << "[IrcMessageHandler:handleClearChatMessage] Twitch channel" + << "[IrcMessageHandler::handleClearChatMessage] Twitch channel" << chanName << "not found"; return; } - // check if the chat has been cleared by a moderator - if (message->parameters().length() == 1) + // chat has been cleared by a moderator + if (clearChat.disableAllMessages) { chan->disableAllMessages(); - chan->addMessage( - makeSystemMessage("Chat has been cleared by a moderator.", - calculateMessageTime(message).time())); + chan->addMessage(std::move(clearChat.message), + MessageContext::Original); return; } - // get username, duration and message of the timed out user - QString username = message->parameter(1); - QString durationInSeconds; - QVariant v = message->tag("ban-duration"); - if (v.isValid()) - { - durationInSeconds = v.toString(); - } - - auto timeoutMsg = - MessageBuilder(timeoutMessage, username, durationInSeconds, false, - calculateMessageTime(message).time()) - .release(); - chan->addOrReplaceTimeout(timeoutMsg); + chan->addOrReplaceTimeout(std::move(clearChat.message)); // refresh all - getApp()->windows->repaintVisibleChatWidgets(chan.get()); + getApp()->getWindows()->repaintVisibleChatWidgets(chan.get()); if (getSettings()->hideModerated) { - getApp()->windows->forceLayoutChannelViews(); + getApp()->getWindows()->forceLayoutChannelViews(); } } @@ -587,7 +839,7 @@ void IrcMessageHandler::handleClearMessageMessage(Communi::IrcMessage *message) } // get channel - auto chan = getApp()->twitch->getChannelOrEmpty(chanName); + auto chan = getApp()->getTwitch()->getChannelOrEmpty(chanName); if (chan->isEmpty()) { @@ -604,20 +856,21 @@ void IrcMessageHandler::handleClearMessageMessage(Communi::IrcMessage *message) auto msg = chan->findMessage(targetID); if (msg == nullptr) + { return; + } msg->flags.set(MessageFlag::Disabled); if (!getSettings()->hideDeletionActions) { - MessageBuilder builder; - TwitchMessageBuilder::deletionMessage(msg, &builder); - chan->addMessage(builder.release()); + chan->addMessage(MessageBuilder::makeDeletionMessageFromIRC(msg), + MessageContext::Original); } } void IrcMessageHandler::handleUserStateMessage(Communi::IrcMessage *message) { - auto currentUser = getApp()->accounts->twitch.getCurrent(); + auto currentUser = getApp()->getAccounts()->twitch.getCurrent(); // set received emote-sets, used in TwitchAccount::loadUserstateEmotes bool emoteSetsChanged = currentUser->setUserstateEmoteSets( @@ -634,33 +887,33 @@ void IrcMessageHandler::handleUserStateMessage(Communi::IrcMessage *message) return; } - auto c = getApp()->twitch->getChannelOrEmpty(channelName); + auto c = getApp()->getTwitch()->getChannelOrEmpty(channelName); if (c->isEmpty()) { return; } // Checking if currentUser is a VIP or staff member - QVariant _badges = message->tag("badges"); - if (_badges.isValid()) + QVariant badgesTag = message->tag("badges"); + if (badgesTag.isValid()) { - TwitchChannel *tc = dynamic_cast(c.get()); + auto *tc = dynamic_cast(c.get()); if (tc != nullptr) { - auto parsedBadges = parseBadges(_badges.toString()); + auto parsedBadges = parseBadges(badgesTag.toString()); tc->setVIP(parsedBadges.contains("vip")); tc->setStaff(parsedBadges.contains("staff")); } } // Checking if currentUser is a moderator - QVariant _mod = message->tag("mod"); - if (_mod.isValid()) + QVariant modTag = message->tag("mod"); + if (modTag.isValid()) { - TwitchChannel *tc = dynamic_cast(c.get()); + auto *tc = dynamic_cast(c.get()); if (tc != nullptr) { - tc->setMod(_mod == "1"); + tc->setMod(modTag == "1"); } } } @@ -669,7 +922,7 @@ void IrcMessageHandler::handleUserStateMessage(Communi::IrcMessage *message) void IrcMessageHandler::handleGlobalUserStateMessage( Communi::IrcMessage *message) { - auto currentUser = getApp()->accounts->twitch.getCurrent(); + auto currentUser = getApp()->getAccounts()->twitch.getCurrent(); // set received emote-sets, this time used to initially load emotes // NOTE: this should always return true unless we reconnect @@ -683,17 +936,17 @@ void IrcMessageHandler::handleGlobalUserStateMessage( currentUser->loadEmotes(); } -void IrcMessageHandler::handleWhisperMessage(Communi::IrcMessage *message) +void IrcMessageHandler::handleWhisperMessage(Communi::IrcMessage *ircMessage) { MessageParseArgs args; args.isReceivedWhisper = true; - auto c = getApp()->twitch->whispersChannel.get(); + auto *c = getApp()->getTwitch()->getWhispersChannel().get(); - TwitchMessageBuilder builder( - c, message, args, - message->parameter(1).replace(COMBINED_FIXER, ZERO_WIDTH_JOINER), + MessageBuilder builder( + c, ircMessage, args, + ircMessage->parameter(1).replace(COMBINED_FIXER, ZERO_WIDTH_JOINER), false); if (builder.isIgnored()) @@ -702,95 +955,36 @@ void IrcMessageHandler::handleWhisperMessage(Communi::IrcMessage *message) } builder->flags.set(MessageFlag::Whisper); - MessagePtr _message = builder.build(); + MessagePtr message = builder.build(); builder.triggerHighlights(); - getApp()->twitch->lastUserThatWhisperedMe.set(builder.userName); + getApp()->getTwitch()->setLastUserThatWhisperedMe(builder.userName); - if (_message->flags.has(MessageFlag::Highlighted)) + if (message->flags.has(MessageFlag::ShowInMentions)) { - getApp()->twitch->mentionsChannel->addMessage(_message); + getApp()->getTwitch()->getMentionsChannel()->addMessage( + message, MessageContext::Original); } - c->addMessage(_message); + c->addMessage(message, MessageContext::Original); - auto overrideFlags = boost::optional(_message->flags); + auto overrideFlags = std::optional(message->flags); overrideFlags->set(MessageFlag::DoNotTriggerNotification); overrideFlags->set(MessageFlag::DoNotLog); - if (getSettings()->inlineWhispers) + if (getSettings()->inlineWhispers && + !(getSettings()->streamerModeSuppressInlineWhispers && + getApp()->getStreamerMode()->isEnabled())) { - getApp()->twitch->forEachChannel( - [&_message, overrideFlags](ChannelPtr channel) { - channel->addMessage(_message, overrideFlags); - }); + getApp()->getTwitch()->forEachChannel([&message, overrideFlags]( + ChannelPtr channel) { + channel->addMessage(message, MessageContext::Repost, overrideFlags); + }); } } -std::vector IrcMessageHandler::parseUserNoticeMessage( - Channel *channel, Communi::IrcMessage *message) -{ - std::vector builtMessages; - - auto tags = message->tags(); - auto parameters = message->parameters(); - - QString msgType = tags.value("msg-id").toString(); - QString content; - if (parameters.size() >= 2) - { - content = parameters[1]; - } - - if (specialMessageTypes.contains(msgType)) - { - // Messages are not required, so they might be empty - if (!content.isEmpty()) - { - MessageParseArgs args; - args.trimSubscriberUsername = true; - - TwitchMessageBuilder builder(channel, message, args, content, - false); - builder->flags.set(MessageFlag::Subscription); - builder->flags.unset(MessageFlag::Highlighted); - builtMessages.emplace_back(builder.build()); - } - } - - auto it = tags.find("system-msg"); - - if (it != tags.end()) - { - // By default, we return value of system-msg tag - QString messageText = it.value().toString(); - - if (msgType == "bitsbadgetier") - { - messageText = - QString("%1 just earned a new %2 Bits badge!") - .arg(tags.value("display-name").toString(), - kFormatNumbers( - tags.value("msg-param-threshold").toInt())); - } - else if (msgType == "announcement") - { - messageText = "Announcement"; - } - - auto b = MessageBuilder(systemMessage, parseTagString(messageText), - calculateMessageTime(message).time()); - - b->flags.set(MessageFlag::Subscription); - auto newMessage = b.release(); - builtMessages.emplace_back(newMessage); - } - - return builtMessages; -} - void IrcMessageHandler::handleUserNoticeMessage(Communi::IrcMessage *message, - TwitchIrcServer &server) + ITwitchIrcServer &twitchServer) { auto tags = message->tags(); auto parameters = message->parameters(); @@ -803,12 +997,23 @@ void IrcMessageHandler::handleUserNoticeMessage(Communi::IrcMessage *message, content = parameters[1]; } - if (specialMessageTypes.contains(msgType)) + auto chn = twitchServer.getChannelOrEmpty(target); + if (isIgnoredMessage({ + .message = content, + .twitchUserID = tags.value("user-id").toString(), + .isMod = chn->isMod(), + .isBroadcaster = chn->isBroadcaster(), + })) + { + return; + } + + if (SPECIAL_MESSAGE_TYPES.contains(msgType)) { // Messages are not required, so they might be empty if (!content.isEmpty()) { - this->addMessage(message, target, content, server, true, false); + this->addMessage(message, chn, content, twitchServer, true, false); } } @@ -831,6 +1036,41 @@ void IrcMessageHandler::handleUserNoticeMessage(Communi::IrcMessage *message, { messageText = "Announcement"; } + else if (msgType == "subgift") + { + if (auto monthsIt = tags.find("msg-param-gift-months"); + monthsIt != tags.end()) + { + int months = monthsIt.value().toInt(); + if (months > 1) + { + auto plan = tags.value("msg-param-sub-plan").toString(); + QString name = + ANONYMOUS_GIFTER_ID == tags.value("user-id").toString() + ? "An anonymous user" + : tags.value("display-name").toString(); + messageText = + QString("%1 gifted %2 months of a Tier %3 sub to %4!") + .arg(name, QString::number(months), + plan.isEmpty() ? '1' : plan.at(0), + tags.value("msg-param-recipient-display-name") + .toString()); + + if (auto countIt = tags.find("msg-param-sender-count"); + countIt != tags.end()) + { + int count = countIt.value().toInt(); + if (count > months) + { + messageText += + QString( + " They've gifted %1 months in the channel.") + .arg(QString::number(count)); + } + } + } + } + } auto b = MessageBuilder(systemMessage, parseTagString(messageText), calculateMessageTime(message).time()); @@ -850,76 +1090,18 @@ void IrcMessageHandler::handleUserNoticeMessage(Communi::IrcMessage *message, return; } - auto chan = server.getChannelOrEmpty(channelName); + auto chan = twitchServer.getChannelOrEmpty(channelName); if (!chan->isEmpty()) { - chan->addMessage(newMessage); + chan->addMessage(newMessage, MessageContext::Original); } } } -std::vector IrcMessageHandler::parseNoticeMessage( - Communi::IrcNoticeMessage *message) -{ - if (message->content().startsWith("Login auth", Qt::CaseInsensitive)) - { - const auto linkColor = MessageColor(MessageColor::Link); - const auto accountsLink = Link(Link::OpenAccountsPage, QString()); - const auto curUser = getApp()->accounts->twitch.getCurrent(); - const auto expirationText = QString("Login expired for user \"%1\"!") - .arg(curUser->getUserName()); - const auto loginPromptText = QString("Try adding your account again."); - - MessageBuilder builder; - auto text = QString("%1 %2").arg(expirationText, loginPromptText); - builder.message().messageText = text; - builder.message().searchText = text; - builder.message().flags.set(MessageFlag::System); - builder.message().flags.set(MessageFlag::DoNotTriggerNotification); - - builder.emplace(); - builder.emplace(expirationText, MessageElementFlag::Text, - MessageColor::System); - builder - .emplace(loginPromptText, MessageElementFlag::Text, - linkColor) - ->setLink(accountsLink); - - return {builder.release()}; - } - else if (message->content().startsWith("You are permanently banned ")) - { - return {generateBannedMessage(true)}; - } - else if (message->tags().value("msg-id") == "msg_timedout") - { - std::vector builtMessage; - - QString remainingTime = - formatTime(message->content().split(" ").value(5)); - QString formattedMessage = - QString("You are timed out for %1.") - .arg(remainingTime.isEmpty() ? "0s" : remainingTime); - - builtMessage.emplace_back(makeSystemMessage( - formattedMessage, calculateMessageTime(message).time())); - - return builtMessage; - } - - // default case - std::vector builtMessages; - - builtMessages.emplace_back(makeSystemMessage( - message->content(), calculateMessageTime(message).time())); - - return builtMessages; -} - void IrcMessageHandler::handleNoticeMessage(Communi::IrcNoticeMessage *message) { - auto builtMessages = this->parseNoticeMessage(message); + auto builtMessages = parseNoticeMessage(message); for (const auto &msg : builtMessages) { @@ -929,15 +1111,15 @@ void IrcMessageHandler::handleNoticeMessage(Communi::IrcNoticeMessage *message) { // Notice wasn't targeted at a single channel, send to all twitch // channels - getApp()->twitch->forEachChannelAndSpecialChannels( + getApp()->getTwitch()->forEachChannelAndSpecialChannels( [msg](const auto &c) { - c->addMessage(msg); + c->addMessage(msg, MessageContext::Original); }); return; } - auto channel = getApp()->twitch->getChannelOrEmpty(channelName); + auto channel = getApp()->getTwitch()->getChannelOrEmpty(channelName); if (channel->isEmpty()) { @@ -950,15 +1132,15 @@ void IrcMessageHandler::handleNoticeMessage(Communi::IrcNoticeMessage *message) QString tags = message->tags().value("msg-id").toString(); if (tags == "usage_delete") { - channel->addMessage(makeSystemMessage( + channel->addSystemMessage( "Usage: /delete - Deletes the specified message. " - "Can't take more than one argument.")); + "Can't take more than one argument."); } else if (tags == "bad_delete_message_error") { - channel->addMessage(makeSystemMessage( + channel->addSystemMessage( "There was a problem deleting the message. " - "It might be from another channel or too old to delete.")); + "It might be from another channel or too old to delete."); } else if (tags == "host_on" || tags == "host_target_went_offline") { @@ -977,10 +1159,9 @@ void IrcMessageHandler::handleNoticeMessage(Communi::IrcNoticeMessage *message) { hostedChannelName.chop(1); } - MessageBuilder builder; - TwitchMessageBuilder::hostingSystemMessage(hostedChannelName, - &builder, hostOn); - channel->addMessage(builder.release()); + channel->addMessage(MessageBuilder::makeHostingSystemMessage( + hostedChannelName, hostOn), + MessageContext::Original); } else if (tags == "room_mods" || tags == "vips_success") { @@ -998,7 +1179,7 @@ void IrcMessageHandler::handleNoticeMessage(Communi::IrcNoticeMessage *message) QStringList msgParts = noticeText.split(':'); MessageBuilder builder; - auto tc = dynamic_cast(channel.get()); + auto *tc = dynamic_cast(channel.get()); assert(tc != nullptr && "IrcMessageHandler::handleNoticeMessage. Twitch specific " "functionality called in non twitch channel"); @@ -1007,21 +1188,21 @@ void IrcMessageHandler::handleNoticeMessage(Communi::IrcNoticeMessage *message) .mid(1) // there is a space before the first user .split(", "); users.sort(Qt::CaseInsensitive); - TwitchMessageBuilder::listOfUsersSystemMessage(msgParts.at(0), - users, tc, &builder); - channel->addMessage(builder.release()); + channel->addMessage(MessageBuilder::makeListOfUsersMessage( + msgParts.at(0), users, tc), + MessageContext::Original); } else { - channel->addMessage(msg); + channel->addMessage(msg, MessageContext::Original); } } } void IrcMessageHandler::handleJoinMessage(Communi::IrcMessage *message) { - auto channel = - getApp()->twitch->getChannelOrEmpty(message->parameter(0).remove(0, 1)); + auto channel = getApp()->getTwitch()->getChannelOrEmpty( + message->parameter(0).remove(0, 1)); auto *twitchChannel = dynamic_cast(channel.get()); if (!twitchChannel) @@ -1029,9 +1210,13 @@ void IrcMessageHandler::handleJoinMessage(Communi::IrcMessage *message) return; } - if (message->nick() != - getApp()->accounts->twitch.getCurrent()->getUserName() && - getSettings()->showJoins.getValue()) + if (message->nick() == + getApp()->getAccounts()->twitch.getCurrent()->getUserName()) + { + twitchChannel->addSystemMessage("joined channel"); + twitchChannel->joined.invoke(); + } + else if (getSettings()->showJoins.getValue()) { twitchChannel->addJoinedUser(message->nick()); } @@ -1039,8 +1224,8 @@ void IrcMessageHandler::handleJoinMessage(Communi::IrcMessage *message) void IrcMessageHandler::handlePartMessage(Communi::IrcMessage *message) { - auto channel = - getApp()->twitch->getChannelOrEmpty(message->parameter(0).remove(0, 1)); + auto channel = getApp()->getTwitch()->getChannelOrEmpty( + message->parameter(0).remove(0, 1)); auto *twitchChannel = dynamic_cast(channel.get()); if (!twitchChannel) @@ -1049,7 +1234,7 @@ void IrcMessageHandler::handlePartMessage(Communi::IrcMessage *message) } const auto selfAccountName = - getApp()->accounts->twitch.getCurrent()->getUserName(); + getApp()->getAccounts()->twitch.getCurrent()->getUserName(); if (message->nick() != selfAccountName && getSettings()->showParts.getValue()) { @@ -1058,7 +1243,228 @@ void IrcMessageHandler::handlePartMessage(Communi::IrcMessage *message) if (message->nick() == selfAccountName) { - channel->addMessage(generateBannedMessage(false)); + channel->addMessage(generateBannedMessage(false), + MessageContext::Original); } } + +float IrcMessageHandler::similarity( + const MessagePtr &msg, const LimitedQueueSnapshot &messages) +{ + float similarityPercent = 0.0F; + int checked = 0; + + for (int i = 1; i <= messages.size(); ++i) + { + if (checked >= getSettings()->hideSimilarMaxMessagesToCheck) + { + break; + } + const auto &prevMsg = messages[messages.size() - i]; + if (prevMsg->parseTime.secsTo(QTime::currentTime()) >= + getSettings()->hideSimilarMaxDelay) + { + break; + } + if (getSettings()->hideSimilarBySameUser && + msg->loginName != prevMsg->loginName) + { + continue; + } + ++checked; + similarityPercent = std::max( + similarityPercent, + relativeSimilarity(msg->messageText, prevMsg->messageText)); + } + + return similarityPercent; +} + +void IrcMessageHandler::setSimilarityFlags(const MessagePtr &message, + const ChannelPtr &channel) +{ + if (getSettings()->similarityEnabled) + { + bool isMyself = + message->loginName == + getApp()->getAccounts()->twitch.getCurrent()->getUserName(); + bool hideMyself = getSettings()->hideSimilarMyself; + + if (isMyself && !hideMyself) + { + return; + } + + if (IrcMessageHandler::similarity(message, + channel->getMessageSnapshot()) > + getSettings()->similarityPercentage) + { + message->flags.set(MessageFlag::Similar, true); + if (getSettings()->colorSimilarDisabled) + { + message->flags.set(MessageFlag::Disabled, true); + } + } + } +} + +void IrcMessageHandler::addMessage(Communi::IrcMessage *message, + const ChannelPtr &chan, + const QString &originalContent, + ITwitchIrcServer &server, bool isSub, + bool isAction) +{ + if (chan->isEmpty()) + { + return; + } + + MessageParseArgs args; + if (isSub) + { + args.isSubscriptionMessage = true; + args.trimSubscriberUsername = true; + } + + if (chan->isBroadcaster()) + { + args.isStaffOrBroadcaster = true; + } + + auto *channel = dynamic_cast(chan.get()); + + const auto &tags = message->tags(); + QString rewardId; + if (const auto it = tags.find("custom-reward-id"); it != tags.end()) + { + rewardId = it.value().toString(); + } + else if (const auto typeIt = tags.find("msg-id"); typeIt != tags.end()) + { + // slight hack to treat bits power-ups as channel point redemptions + const auto msgId = typeIt.value().toString(); + if (msgId == "animated-message" || msgId == "gigantified-emote-message") + { + rewardId = msgId; + } + } + if (!rewardId.isEmpty() && !channel->isChannelPointRewardKnown(rewardId)) + { + // Need to wait for pubsub reward notification + qCDebug(chatterinoTwitch) << "TwitchChannel reward added ADD " + "callback since reward is not known:" + << rewardId; + channel->addQueuedRedemption(rewardId, originalContent, message); + return; + } + args.channelPointRewardId = rewardId; + + QString content = originalContent; + int messageOffset = stripLeadingReplyMention(tags, content); + + MessageBuilder builder(channel, message, args, content, isAction); + builder.setMessageOffset(messageOffset); + + if (const auto it = tags.find("reply-thread-parent-msg-id"); + it != tags.end()) + { + const QString replyID = it.value().toString(); + auto threadIt = channel->threads().find(replyID); + std::shared_ptr rootThread; + if (threadIt != channel->threads().end() && !threadIt->second.expired()) + { + // Thread already exists (has a reply) + auto thread = threadIt->second.lock(); + updateReplyParticipatedStatus(tags, message->nick(), builder, + thread, false); + builder.setThread(thread); + rootThread = thread; + } + else + { + // Thread does not yet exist, find root reply and create thread. + auto root = channel->findMessage(replyID); + if (root) + { + // Found root reply message + auto newThread = std::make_shared(root); + updateReplyParticipatedStatus(tags, message->nick(), builder, + newThread, true); + + builder.setThread(newThread); + rootThread = newThread; + // Store weak reference to thread in channel + channel->addReplyThread(newThread); + } + } + + if (const auto parentIt = tags.find("reply-parent-msg-id"); + parentIt != tags.end()) + { + const QString parentID = parentIt.value().toString(); + if (replyID == parentID) + { + if (rootThread) + { + builder.setParent(rootThread->root()); + } + } + else + { + auto parentThreadIt = channel->threads().find(parentID); + if (parentThreadIt != channel->threads().end()) + { + auto thread = parentThreadIt->second.lock(); + if (thread) + { + builder.setParent(thread->root()); + } + } + else + { + auto parent = channel->findMessage(parentID); + if (parent) + { + builder.setParent(parent); + } + } + } + } + } + + if (isSub || !builder.isIgnored()) + { + if (isSub) + { + builder->flags.set(MessageFlag::Subscription); + builder->flags.unset(MessageFlag::Highlighted); + } + auto msg = builder.build(); + + IrcMessageHandler::setSimilarityFlags(msg, chan); + + if (!msg->flags.has(MessageFlag::Similar) || + (!getSettings()->hideSimilar && + getSettings()->shownSimilarTriggerHighlights)) + { + builder.triggerHighlights(); + } + + const auto highlighted = msg->flags.has(MessageFlag::Highlighted); + const auto showInMentions = msg->flags.has(MessageFlag::ShowInMentions); + + if (highlighted && showInMentions) + { + server.getMentionsChannel()->addMessage(msg, + MessageContext::Original); + } + + chan->addMessage(msg, MessageContext::Original); + if (auto *chatters = dynamic_cast(chan.get())) + { + chatters->addRecentChatter(msg->displayName); + } + } +} + } // namespace chatterino diff --git a/src/providers/twitch/IrcMessageHandler.hpp b/src/providers/twitch/IrcMessageHandler.hpp index db3fb8c48..705f39ba3 100644 --- a/src/providers/twitch/IrcMessageHandler.hpp +++ b/src/providers/twitch/IrcMessageHandler.hpp @@ -1,17 +1,26 @@ #pragma once -#include -#include "common/Channel.hpp" -#include "messages/Message.hpp" -#include "providers/twitch/TwitchChannel.hpp" -#include "providers/twitch/TwitchMessageBuilder.hpp" +#include "messages/LimitedQueueSnapshot.hpp" +#include + +#include #include namespace chatterino { -class TwitchIrcServer; +class ITwitchIrcServer; class Channel; +using ChannelPtr = std::shared_ptr; +struct Message; +using MessagePtr = std::shared_ptr; +class TwitchChannel; +class TwitchMessageBuilder; + +struct ClearChatMessage { + MessagePtr message; + bool disableAllMessages; +}; class IrcMessageHandler { @@ -20,57 +29,41 @@ class IrcMessageHandler public: static IrcMessageHandler &instance(); - // parseMessage parses a single IRC message into 0+ Chatterino messages - std::vector parseMessage(Channel *channel, - Communi::IrcMessage *message); - - std::vector parseMessageWithReply( + /** + * Parse an IRC message into 0 or more Chatterino messages + * Takes previously loaded messages into consideration to add reply contexts + **/ + static std::vector parseMessageWithReply( Channel *channel, Communi::IrcMessage *message, - const std::vector &otherLoaded); + std::vector &otherLoaded); - // parsePrivMessage arses a single IRC PRIVMSG into 0-1 Chatterino messages - std::vector parsePrivMessage( - Channel *channel, Communi::IrcPrivateMessage *message); void handlePrivMessage(Communi::IrcPrivateMessage *message, - TwitchIrcServer &server); + ITwitchIrcServer &twitchServer); void handleRoomStateMessage(Communi::IrcMessage *message); void handleClearChatMessage(Communi::IrcMessage *message); void handleClearMessageMessage(Communi::IrcMessage *message); void handleUserStateMessage(Communi::IrcMessage *message); void handleGlobalUserStateMessage(Communi::IrcMessage *message); - void handleWhisperMessage(Communi::IrcMessage *message); + void handleWhisperMessage(Communi::IrcMessage *ircMessage); - // parseUserNoticeMessage parses a single IRC USERNOTICE message into 0+ - // Chatterino messages - std::vector parseUserNoticeMessage( - Channel *channel, Communi::IrcMessage *message); void handleUserNoticeMessage(Communi::IrcMessage *message, - TwitchIrcServer &server); + ITwitchIrcServer &twitchServer); - void handleModeMessage(Communi::IrcMessage *message); - - // parseNoticeMessage parses a single IRC NOTICE message into 0+ chatterino - // messages - std::vector parseNoticeMessage( - Communi::IrcNoticeMessage *message); void handleNoticeMessage(Communi::IrcNoticeMessage *message); void handleJoinMessage(Communi::IrcMessage *message); void handlePartMessage(Communi::IrcMessage *message); - static float similarity(MessagePtr msg, - const LimitedQueueSnapshot &messages); - static void setSimilarityFlags(MessagePtr message, ChannelPtr channel); + void addMessage(Communi::IrcMessage *message, const ChannelPtr &chan, + const QString &originalContent, ITwitchIrcServer &server, + bool isSub, bool isAction); private: - void addMessage(Communi::IrcMessage *message, const QString &target, - const QString &content, TwitchIrcServer &server, - bool isResub, bool isAction); - - void populateReply(TwitchChannel *channel, Communi::IrcMessage *message, - const std::vector &otherLoaded, - TwitchMessageBuilder &builder); + static float similarity(const MessagePtr &msg, + const LimitedQueueSnapshot &messages); + static void setSimilarityFlags(const MessagePtr &message, + const ChannelPtr &channel); }; } // namespace chatterino diff --git a/src/providers/twitch/PubSubActions.hpp b/src/providers/twitch/PubSubActions.hpp index d698b9aef..5f83b4051 100644 --- a/src/providers/twitch/PubSubActions.hpp +++ b/src/providers/twitch/PubSubActions.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -33,6 +34,7 @@ inline QDebug operator<<(QDebug dbg, const ActionUser &user) } struct PubSubAction { + PubSubAction() = default; PubSubAction(const QJsonObject &data, const QString &_roomID); ActionUser source; @@ -170,4 +172,22 @@ struct AutomodInfoAction : PubSubAction { } type; }; +struct RaidAction : PubSubAction { + using PubSubAction::PubSubAction; + + QString target; +}; + +struct UnraidAction : PubSubAction { + using PubSubAction::PubSubAction; +}; + +struct WarnAction : PubSubAction { + using PubSubAction::PubSubAction; + + ActionUser target; + + QStringList reasons; +}; + } // namespace chatterino diff --git a/src/providers/twitch/PubSubClient.cpp b/src/providers/twitch/PubSubClient.cpp index c35d9a418..514fd487d 100644 --- a/src/providers/twitch/PubSubClient.cpp +++ b/src/providers/twitch/PubSubClient.cpp @@ -4,7 +4,6 @@ #include "providers/twitch/PubSubActions.hpp" #include "providers/twitch/PubSubHelpers.hpp" #include "providers/twitch/PubSubMessages.hpp" -#include "providers/twitch/pubsubmessages/Unlisten.hpp" #include "singletons/Settings.hpp" #include "util/DebugCount.hpp" #include "util/Helpers.hpp" @@ -22,6 +21,8 @@ PubSubClient::PubSubClient(WebsocketClient &websocketClient, const PubSubClientOptions &clientOptions) : websocketClient_(websocketClient) , handle_(handle) + , heartbeatTimer_(std::make_shared( + this->websocketClient_.get_io_service())) , clientOptions_(clientOptions) { } @@ -40,32 +41,41 @@ void PubSubClient::stop() assert(this->started_); this->started_ = false; + this->heartbeatTimer_->cancel(); } void PubSubClient::close(const std::string &reason, websocketpp::close::status::value code) { - WebsocketErrorCode ec; + boost::asio::post( + this->websocketClient_.get_io_service().get_executor(), + [this, reason, code] { + // We need to post this request to the io service executor + // to ensure the weak pointer used in get_con_from_hdl is used in a safe way + WebsocketErrorCode ec; - auto conn = this->websocketClient_.get_con_from_hdl(this->handle_, ec); - if (ec) - { - qCDebug(chatterinoPubSub) - << "Error getting con:" << ec.message().c_str(); - return; - } + auto conn = + this->websocketClient_.get_con_from_hdl(this->handle_, ec); + if (ec) + { + qCDebug(chatterinoPubSub) + << "Error getting con:" << ec.message().c_str(); + return; + } - conn->close(code, reason, ec); - if (ec) - { - qCDebug(chatterinoPubSub) << "Error closing:" << ec.message().c_str(); - return; - } + conn->close(code, reason, ec); + if (ec) + { + qCDebug(chatterinoPubSub) + << "Error closing:" << ec.message().c_str(); + return; + } + }); } -bool PubSubClient::listen(PubSubListenMessage msg) +bool PubSubClient::listen(const PubSubListenMessage &msg) { - int numRequestedListens = msg.topics.size(); + auto numRequestedListens = msg.topics.size(); if (this->numListens_ + numRequestedListens > PubSubClient::MAX_LISTENS) { @@ -73,11 +83,19 @@ bool PubSubClient::listen(PubSubListenMessage msg) return false; } this->numListens_ += numRequestedListens; - DebugCount::increase("PubSub topic pending listens", numRequestedListens); + DebugCount::increase("PubSub topic pending listens", + static_cast(numRequestedListens)); for (const auto &topic : msg.topics) { - this->listeners_.emplace_back(Listener{topic, false, false, false}); + this->listeners_.emplace_back(Listener{ + TopicData{ + topic, + false, + false, + }, + false, + }); } qCDebug(chatterinoPubSub) @@ -116,7 +134,7 @@ PubSubClient::UnlistenPrefixResponse PubSubClient::unlistenPrefix( this->numListens_ -= numRequestedUnlistens; DebugCount::increase("PubSub topic pending unlistens", - numRequestedUnlistens); + static_cast(numRequestedUnlistens)); PubSubUnlistenMessage message(topics); @@ -179,8 +197,9 @@ void PubSubClient::ping() auto self = this->shared_from_this(); - runAfter(this->websocketClient_.get_io_service(), - this->clientOptions_.pingInterval_, [self](auto timer) { + runAfter(this->heartbeatTimer_, this->clientOptions_.pingInterval_, + [self](auto timer) { + (void)timer; if (!self->started_) { return; diff --git a/src/providers/twitch/PubSubClient.hpp b/src/providers/twitch/PubSubClient.hpp index 848328617..01d212de5 100644 --- a/src/providers/twitch/PubSubClient.hpp +++ b/src/providers/twitch/PubSubClient.hpp @@ -1,17 +1,19 @@ #pragma once #include "providers/twitch/PubSubClientOptions.hpp" -#include "providers/twitch/PubSubMessages.hpp" #include "providers/twitch/PubSubWebsocket.hpp" -#include #include +#include #include #include namespace chatterino { +struct PubSubMessage; +struct PubSubListenMessage; + struct TopicData { QString topic; bool authed{false}; @@ -43,7 +45,7 @@ public: websocketpp::close::status::value code = websocketpp::close::status::normal); - bool listen(PubSubListenMessage msg); + bool listen(const PubSubListenMessage &msg); UnlistenPrefixResponse unlistenPrefix(const QString &prefix); void handleListenResponse(const PubSubMessage &message); @@ -68,6 +70,7 @@ private: std::atomic awaitingPong_{false}; std::atomic started_{false}; + std::shared_ptr heartbeatTimer_; const PubSubClientOptions &clientOptions_; }; diff --git a/src/providers/twitch/PubSubHelpers.hpp b/src/providers/twitch/PubSubHelpers.hpp index d1e616c6a..faf01f4c7 100644 --- a/src/providers/twitch/PubSubHelpers.hpp +++ b/src/providers/twitch/PubSubHelpers.hpp @@ -1,11 +1,11 @@ #pragma once -#include +#include "common/QLogging.hpp" + #include #include + #include -#include "common/QLogging.hpp" -#include "util/RapidjsonHelpers.hpp" namespace chatterino { diff --git a/src/providers/twitch/PubSubManager.cpp b/src/providers/twitch/PubSubManager.cpp index cce10786b..89a08327b 100644 --- a/src/providers/twitch/PubSubManager.cpp +++ b/src/providers/twitch/PubSubManager.cpp @@ -1,21 +1,32 @@ #include "providers/twitch/PubSubManager.hpp" +#include "Application.hpp" #include "common/QLogging.hpp" +#include "controllers/accounts/AccountController.hpp" +#include "providers/NetworkConfigurationProvider.hpp" #include "providers/twitch/PubSubActions.hpp" +#include "providers/twitch/PubSubClient.hpp" #include "providers/twitch/PubSubHelpers.hpp" #include "providers/twitch/PubSubMessages.hpp" +#include "providers/twitch/TwitchAccount.hpp" #include "util/DebugCount.hpp" #include "util/Helpers.hpp" #include "util/RapidjsonHelpers.hpp" +#include "util/RenameThread.hpp" + +#include #include #include +#include #include +#include #include using websocketpp::lib::bind; using websocketpp::lib::placeholders::_1; using websocketpp::lib::placeholders::_2; +using namespace std::chrono_literals; namespace chatterino { @@ -29,7 +40,7 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval) const auto &roomID) { ClearChatAction action(data, roomID); - this->signals_.moderation.chatCleared.invoke(action); + this->moderation.chatCleared.invoke(action); }; this->moderationActionHandlers["slowoff"] = [this](const auto &data, @@ -39,7 +50,7 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval) action.mode = ModeChangedAction::Mode::Slow; action.state = ModeChangedAction::State::Off; - this->signals_.moderation.modeChanged.invoke(action); + this->moderation.modeChanged.invoke(action); }; this->moderationActionHandlers["slow"] = [this](const auto &data, @@ -62,7 +73,7 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval) action.duration = args.at(0).toString().toUInt(&ok, 10); - this->signals_.moderation.modeChanged.invoke(action); + this->moderation.modeChanged.invoke(action); }; this->moderationActionHandlers["r9kbetaoff"] = [this](const auto &data, @@ -72,7 +83,7 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval) action.mode = ModeChangedAction::Mode::R9K; action.state = ModeChangedAction::State::Off; - this->signals_.moderation.modeChanged.invoke(action); + this->moderation.modeChanged.invoke(action); }; this->moderationActionHandlers["r9kbeta"] = [this](const auto &data, @@ -82,7 +93,7 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval) action.mode = ModeChangedAction::Mode::R9K; action.state = ModeChangedAction::State::On; - this->signals_.moderation.modeChanged.invoke(action); + this->moderation.modeChanged.invoke(action); }; this->moderationActionHandlers["subscribersoff"] = @@ -92,7 +103,7 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval) action.mode = ModeChangedAction::Mode::SubscribersOnly; action.state = ModeChangedAction::State::Off; - this->signals_.moderation.modeChanged.invoke(action); + this->moderation.modeChanged.invoke(action); }; this->moderationActionHandlers["subscribers"] = [this](const auto &data, @@ -102,7 +113,7 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval) action.mode = ModeChangedAction::Mode::SubscribersOnly; action.state = ModeChangedAction::State::On; - this->signals_.moderation.modeChanged.invoke(action); + this->moderation.modeChanged.invoke(action); }; this->moderationActionHandlers["emoteonlyoff"] = @@ -112,7 +123,7 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval) action.mode = ModeChangedAction::Mode::EmoteOnly; action.state = ModeChangedAction::State::Off; - this->signals_.moderation.modeChanged.invoke(action); + this->moderation.modeChanged.invoke(action); }; this->moderationActionHandlers["emoteonly"] = [this](const auto &data, @@ -122,7 +133,7 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval) action.mode = ModeChangedAction::Mode::EmoteOnly; action.state = ModeChangedAction::State::On; - this->signals_.moderation.modeChanged.invoke(action); + this->moderation.modeChanged.invoke(action); }; this->moderationActionHandlers["unmod"] = [this](const auto &data, @@ -142,7 +153,7 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval) action.modded = false; - this->signals_.moderation.moderationStateChanged.invoke(action); + this->moderation.moderationStateChanged.invoke(action); }; this->moderationActionHandlers["mod"] = [this](const auto &data, @@ -160,7 +171,7 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval) action.target.id = data.value("target_user_id").toString(); action.target.login = data.value("target_user_login").toString(); - this->signals_.moderation.moderationStateChanged.invoke(action); + this->moderation.moderationStateChanged.invoke(action); }; this->moderationActionHandlers["timeout"] = [this](const auto &data, @@ -184,7 +195,7 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval) action.duration = args[1].toString().toUInt(&ok, 10); action.reason = args[2].toString(); // May be omitted - this->signals_.moderation.userBanned.invoke(action); + this->moderation.userBanned.invoke(action); }; this->moderationActionHandlers["delete"] = [this](const auto &data, @@ -204,11 +215,10 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval) } action.target.login = args[0].toString(); - bool ok; action.messageText = args[1].toString(); action.messageId = args[2].toString(); - this->signals_.moderation.messageDeleted.invoke(action); + this->moderation.messageDeleted.invoke(action); }; this->moderationActionHandlers["ban"] = [this](const auto &data, @@ -230,7 +240,7 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval) action.target.login = args[0].toString(); action.reason = args[1].toString(); // May be omitted - this->signals_.moderation.userBanned.invoke(action); + this->moderation.userBanned.invoke(action); }; this->moderationActionHandlers["unban"] = [this](const auto &data, @@ -253,7 +263,7 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval) action.target.login = args[0].toString(); - this->signals_.moderation.userUnbanned.invoke(action); + this->moderation.userUnbanned.invoke(action); }; this->moderationActionHandlers["untimeout"] = [this](const auto &data, @@ -276,7 +286,67 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval) action.target.login = args[0].toString(); - this->signals_.moderation.userUnbanned.invoke(action); + this->moderation.userUnbanned.invoke(action); + }; + + this->moderationActionHandlers["warn"] = [this](const auto &data, + const auto &roomID) { + WarnAction action(data, roomID); + + action.source.id = data.value("created_by_user_id").toString(); + action.source.login = + data.value("created_by").toString(); // currently always empty + + action.target.id = data.value("target_user_id").toString(); + action.target.login = data.value("target_user_login").toString(); + + const auto reasons = data.value("args").toArray(); + bool firstArg = true; + for (const auto &reasonValue : reasons) + { + if (firstArg) + { + // Skip first arg in the reasons array since it's not a reason + firstArg = false; + continue; + } + const auto &reason = reasonValue.toString(); + if (!reason.isEmpty()) + { + action.reasons.append(reason); + } + } + + this->moderation.userWarned.invoke(action); + }; + + this->moderationActionHandlers["raid"] = [this](const auto &data, + const auto &roomID) { + RaidAction action(data, roomID); + + action.source.id = data.value("created_by_user_id").toString(); + action.source.login = data.value("created_by").toString(); + + const auto args = data.value("args").toArray(); + + if (args.isEmpty()) + { + return; + } + + action.target = args[0].toString(); + + this->moderation.raidStarted.invoke(action); + }; + + this->moderationActionHandlers["unraid"] = [this](const auto &data, + const auto &roomID) { + UnraidAction action(data, roomID); + + action.source.id = data.value("created_by_user_id").toString(); + action.source.login = data.value("created_by").toString(); + + this->moderation.raidCanceled.invoke(action); }; /* @@ -309,7 +379,7 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval) action.message = args[1].toString(); // May be omitted action.reason = args[2].toString(); // May be omitted - this->signals_.moderation.autoModMessageBlocked.invoke(action); + this->moderation.autoModMessageBlocked.invoke(action); }; */ @@ -317,21 +387,21 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval) [this](const auto &data, const auto &roomID) { AutomodInfoAction action(data, roomID); action.type = AutomodInfoAction::OnHold; - this->signals_.moderation.automodInfoMessage.invoke(action); + this->moderation.automodInfoMessage.invoke(action); }; this->moderationActionHandlers["automod_message_denied"] = [this](const auto &data, const auto &roomID) { AutomodInfoAction action(data, roomID); action.type = AutomodInfoAction::Denied; - this->signals_.moderation.automodInfoMessage.invoke(action); + this->moderation.automodInfoMessage.invoke(action); }; this->moderationActionHandlers["automod_message_approved"] = [this](const auto &data, const auto &roomID) { AutomodInfoAction action(data, roomID); action.type = AutomodInfoAction::Approved; - this->signals_.moderation.automodInfoMessage.invoke(action); + this->moderation.automodInfoMessage.invoke(action); }; this->channelTermsActionHandlers["add_permitted_term"] = @@ -345,7 +415,7 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval) action.message = data.value("text").toString(); action.source.login = data.value("requester_login").toString(); - this->signals_.moderation.automodUserMessage.invoke(action); + this->moderation.automodUserMessage.invoke(action); }; this->channelTermsActionHandlers["add_blocked_term"] = @@ -359,7 +429,7 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval) action.message = data.value("text").toString(); action.source.login = data.value("requester_login").toString(); - this->signals_.moderation.automodUserMessage.invoke(action); + this->moderation.automodUserMessage.invoke(action); }; this->moderationActionHandlers["delete_permitted_term"] = @@ -379,7 +449,7 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval) action.message = args[0].toString(); - this->signals_.moderation.automodUserMessage.invoke(action); + this->moderation.automodUserMessage.invoke(action); }; this->channelTermsActionHandlers["delete_permitted_term"] = @@ -393,7 +463,7 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval) action.message = data.value("text").toString(); action.source.login = data.value("requester_login").toString(); - this->signals_.moderation.automodUserMessage.invoke(action); + this->moderation.automodUserMessage.invoke(action); }; this->moderationActionHandlers["delete_blocked_term"] = @@ -414,7 +484,7 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval) action.message = args[0].toString(); - this->signals_.moderation.automodUserMessage.invoke(action); + this->moderation.automodUserMessage.invoke(action); }; this->channelTermsActionHandlers["delete_blocked_term"] = [this](const auto &data, const auto &roomID) { @@ -428,21 +498,9 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval) action.message = data.value("text").toString(); action.source.login = data.value("requester_login").toString(); - this->signals_.moderation.automodUserMessage.invoke(action); + this->moderation.automodUserMessage.invoke(action); }; - // We don't get this one anymore or anything similiar - // We need some new topic so we can listen - // - //this->moderationActionHandlers["modified_automod_properties"] = - // [this](const auto &data, const auto &roomID) { - // // The automod settings got modified - // AutomodUserAction action(data, roomID); - // getCreatedByUser(data, action.source); - // action.type = AutomodUserAction::Properties; - // this->signals_.moderation.automodUserMessage.invoke(action); - // }; - this->moderationActionHandlers["denied_automod_message"] = [](const auto &data, const auto &roomID) { // This message got denied by a moderator @@ -476,6 +534,34 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval) bind(&PubSub::onConnectionFail, this, ::_1)); } +PubSub::~PubSub() +{ + this->stop(); +} + +void PubSub::initialize() +{ + this->start(); + this->setAccount(getApp()->getAccounts()->twitch.getCurrent()); + + getApp()->getAccounts()->twitch.currentUserChanged.connect( + [this] { + this->unlistenChannelModerationActions(); + this->unlistenAutomod(); + this->unlistenLowTrustUsers(); + this->unlistenChannelPointRewards(); + + this->setAccount(getApp()->getAccounts()->twitch.getCurrent()); + }, + boost::signals2::at_front); +} + +void PubSub::setAccount(std::shared_ptr account) +{ + this->token_ = account->getOAuthToken(); + this->userID_ = account->getUserId(); +} + void PubSub::addClient() { if (this->addingClient) @@ -498,6 +584,8 @@ void PubSub::addClient() return; } + NetworkConfigurationProvider::applyToWebSocket(con); + this->websocketClient.connect(con); } @@ -505,81 +593,40 @@ void PubSub::start() { this->work = std::make_shared( this->websocketClient.get_io_service()); - this->mainThread.reset( - new std::thread(std::bind(&PubSub::runThread, this))); + this->thread = std::make_unique([this] { + runThread(); + }); + renameThread(*this->thread, "PubSub"); } void PubSub::stop() { this->stopping_ = true; - for (const auto &client : this->clients) + for (const auto &[hdl, client] : this->clients) { - client.second->close("Shutting down"); + (void)hdl; + + client->close("Shutting down"); } this->work.reset(); - if (this->mainThread->joinable()) + if (this->thread->joinable()) { - this->mainThread->join(); - } - - assert(this->clients.empty()); -} - -void PubSub::unlistenAllModerationActions() -{ - for (const auto &p : this->clients) - { - const auto &client = p.second; - if (const auto &[topics, nonce] = - client->unlistenPrefix("chat_moderator_actions."); - !topics.empty()) + // NOTE: We spawn a new thread to join the websocket thread. + // There is a case where a new client was initiated but not added to the clients list. + // We just don't join the thread & let the operating system nuke the thread if joining fails + // within 1s. + // We could fix the underlying bug, but this is easier & we realistically won't use this exact code + // for super much longer. + auto joiner = std::async(std::launch::async, &std::thread::join, + this->thread.get()); + if (joiner.wait_for(1s) == std::future_status::timeout) { - this->registerNonce(nonce, { - client, - "UNLISTEN", - topics, - topics.size(), - }); - } - } -} - -void PubSub::unlistenAutomod() -{ - for (const auto &p : this->clients) - { - const auto &client = p.second; - if (const auto &[topics, nonce] = - client->unlistenPrefix("automod-queue."); - !topics.empty()) - { - this->registerNonce(nonce, { - client, - "UNLISTEN", - topics, - topics.size(), - }); - } - } -} - -void PubSub::unlistenWhispers() -{ - for (const auto &p : this->clients) - { - const auto &client = p.second; - if (const auto &[topics, nonce] = client->unlistenPrefix("whispers."); - !topics.empty()) - { - this->registerNonce(nonce, { - client, - "UNLISTEN", - topics, - topics.size(), - }); + qCWarning(chatterinoPubSub) + << "Thread didn't join within 1 second, rip it out"; + this->websocketClient.stop(); } } } @@ -603,6 +650,11 @@ bool PubSub::listenToWhispers() return true; } +void PubSub::unlistenWhispers() +{ + this->unlistenPrefix("whispers."); +} + void PubSub::listenToChannelModerationActions(const QString &channelID) { if (this->userID_.isEmpty()) @@ -627,6 +679,11 @@ void PubSub::listenToChannelModerationActions(const QString &channelID) this->listenToTopic(topic); } +void PubSub::unlistenChannelModerationActions() +{ + this->unlistenPrefix("chat_moderator_actions."); +} + void PubSub::listenToAutomod(const QString &channelID) { if (this->userID_.isEmpty()) @@ -651,6 +708,40 @@ void PubSub::listenToAutomod(const QString &channelID) this->listenToTopic(topic); } +void PubSub::unlistenAutomod() +{ + this->unlistenPrefix("automod-queue."); +} + +void PubSub::listenToLowTrustUsers(const QString &channelID) +{ + if (this->userID_.isEmpty()) + { + qCDebug(chatterinoPubSub) + << "Unable to listen to low trust users topic, no user logged in"; + return; + } + + static const QString topicFormat("low-trust-users.%1.%2"); + assert(!channelID.isEmpty()); + + auto topic = topicFormat.arg(this->userID_, channelID); + + if (this->isListeningToTopic(topic)) + { + return; + } + + qCDebug(chatterinoPubSub) << "Listen to topic" << topic; + + this->listenToTopic(topic); +} + +void PubSub::unlistenLowTrustUsers() +{ + this->unlistenPrefix("low-trust-users."); +} + void PubSub::listenToChannelPointRewards(const QString &channelID) { static const QString topicFormat("community-points-channel-v1.%1"); @@ -667,6 +758,30 @@ void PubSub::listenToChannelPointRewards(const QString &channelID) this->listenToTopic(topic); } +void PubSub::unlistenChannelPointRewards() +{ + this->unlistenPrefix("community-points-channel-v1."); +} + +void PubSub::unlistenPrefix(const QString &prefix) +{ + for (const auto &p : this->clients) + { + const auto &client = p.second; + if (const auto &[topics, nonce] = client->unlistenPrefix(prefix); + !topics.empty()) + { + NonceInfo nonceInfo{ + client, + "UNLISTEN", + topics, + topics.size(), + }; + this->registerNonce(nonce, nonceInfo); + } + } +} + void PubSub::listen(PubSubListenMessage msg) { if (this->tryListen(msg)) @@ -707,14 +822,14 @@ void PubSub::registerNonce(QString nonce, NonceInfo info) this->nonces_[nonce] = std::move(info); } -boost::optional PubSub::findNonceInfo(QString nonce) +std::optional PubSub::findNonceInfo(QString nonce) { // TODO: This should also DELETE the nonceinfo from the map auto it = this->nonces_.find(nonce); if (it == this->nonces_.end()) { - return boost::none; + return std::nullopt; } return it->second; @@ -816,7 +931,7 @@ void PubSub::onConnectionOpen(WebsocketHandle hdl) qCDebug(chatterinoPubSub) << "PubSub connection opened!"; const auto topicsToTake = - (std::min)(this->requests.size(), PubSubClient::MAX_LISTENS); + std::min(this->requests.size(), PubSubClient::MAX_LISTENS); std::vector newTopics( std::make_move_iterator(this->requests.begin()), @@ -1020,11 +1135,11 @@ void PubSub::handleMessageResponse(const PubSubMessageMessage &message) switch (whisperMessage.type) { case PubSubWhisperMessage::Type::WhisperReceived: { - this->signals_.whisper.received.invoke(whisperMessage); + this->whisper.received.invoke(whisperMessage); } break; case PubSubWhisperMessage::Type::WhisperSent: { - this->signals_.whisper.sent.invoke(whisperMessage); + this->whisper.sent.invoke(whisperMessage); } break; case PubSubWhisperMessage::Type::Thread: { @@ -1097,8 +1212,8 @@ void PubSub::handleMessageResponse(const PubSubMessageMessage &message) case PubSubChatModeratorActionMessage::Type::INVALID: default: { - qCDebug(chatterinoPubSub) - << "Invalid whisper type:" << innerMessage.typeString; + qCDebug(chatterinoPubSub) << "Invalid moderator action type:" + << innerMessage.typeString; } break; } @@ -1116,10 +1231,12 @@ void PubSub::handleMessageResponse(const PubSubMessageMessage &message) switch (innerMessage.type) { + case PubSubCommunityPointsChannelV1Message::Type:: + AutomaticRewardRedeemed: case PubSubCommunityPointsChannelV1Message::Type::RewardRedeemed: { auto redemption = innerMessage.data.value("redemption").toObject(); - this->signals_.pointReward.redeemed.invoke(redemption); + this->pointReward.redeemed.invoke(redemption); } break; @@ -1147,8 +1264,38 @@ void PubSub::handleMessageResponse(const PubSubMessageMessage &message) // Channel ID where the moderator actions are coming from auto channelID = topicParts[2]; - this->signals_.moderation.autoModMessageCaught.invoke(innerMessage, - channelID); + this->moderation.autoModMessageCaught.invoke(innerMessage, channelID); + } + else if (topic.startsWith("low-trust-users.")) + { + auto oInnerMessage = message.toInner(); + if (!oInnerMessage) + { + return; + } + + auto innerMessage = *oInnerMessage; + + switch (innerMessage.type) + { + case PubSubLowTrustUsersMessage::Type::UserMessage: { + this->moderation.suspiciousMessageReceived.invoke(innerMessage); + } + break; + + case PubSubLowTrustUsersMessage::Type::TreatmentUpdate: { + this->moderation.suspiciousTreatmentUpdated.invoke( + innerMessage); + } + break; + + case PubSubLowTrustUsersMessage::Type::INVALID: { + qCWarning(chatterinoPubSub) + << "Invalid low trust users event type:" + << innerMessage.typeString; + } + break; + } } else { diff --git a/src/providers/twitch/PubSubManager.hpp b/src/providers/twitch/PubSubManager.hpp index f1cecd440..f14eabf77 100644 --- a/src/providers/twitch/PubSubManager.hpp +++ b/src/providers/twitch/PubSubManager.hpp @@ -1,29 +1,66 @@ #pragma once -#include "providers/twitch/ChatterinoWebSocketppLogger.hpp" -#include "providers/twitch/PubSubActions.hpp" -#include "providers/twitch/PubSubClient.hpp" #include "providers/twitch/PubSubClientOptions.hpp" -#include "providers/twitch/PubSubMessages.hpp" #include "providers/twitch/PubSubWebsocket.hpp" -#include "providers/twitch/TwitchAccount.hpp" #include "util/ExponentialBackoff.hpp" +#include +#include +#include #include #include -#include #include +#include +#include +#include #include #include +#include +#include #include #include +#include #include #include #include +#if __has_include() +# include +#endif + namespace chatterino { +class TwitchAccount; +class PubSubClient; + +struct ClearChatAction; +struct DeleteAction; +struct ModeChangedAction; +struct ModerationStateAction; +struct BanAction; +struct UnbanAction; +struct PubSubAutoModQueueMessage; +struct AutomodAction; +struct AutomodUserAction; +struct AutomodInfoAction; +struct RaidAction; +struct UnraidAction; +struct WarnAction; +struct PubSubLowTrustUsersMessage; +struct PubSubWhisperMessage; + +struct PubSubListenMessage; +struct PubSubMessage; +struct PubSubMessageMessage; + +/** + * This handles the Twitch PubSub connection + * + * Known issues: + * - Upon closing a channel, we don't unsubscribe to its pubsub connections + * - Stop is never called, meaning we never do a clean shutdown + */ class PubSub { using WebsocketMessagePtr = @@ -43,92 +80,118 @@ class PubSub }; WebsocketClient websocketClient; - std::unique_ptr mainThread; + std::unique_ptr thread; // Account credentials - // Set from setAccount or setAccountData + // Set from setAccount QString token_; QString userID_; public: - // The max amount of connections we may open - static constexpr int maxConnections = 10; - PubSub(const QString &host, std::chrono::seconds pingInterval = std::chrono::seconds(15)); + ~PubSub(); - void setAccount(std::shared_ptr account) - { - this->token_ = account->getOAuthToken(); - this->userID_ = account->getUserId(); - } + PubSub(const PubSub &) = delete; + PubSub(PubSub &&) = delete; + PubSub &operator=(const PubSub &) = delete; + PubSub &operator=(PubSub &&) = delete; - void setAccountData(QString token, QString userID) - { - this->token_ = token; - this->userID_ = userID; - } - - ~PubSub() = delete; - - enum class State { - Connected, - Disconnected, - }; - - void start(); - void stop(); - - bool isConnected() const - { - return this->state == State::Connected; - } + /// Set up connections between itself & other parts of the application + void initialize(); struct { - struct { - Signal chatCleared; - Signal messageDeleted; - Signal modeChanged; - Signal moderationStateChanged; + Signal chatCleared; + Signal messageDeleted; + Signal modeChanged; + Signal moderationStateChanged; - Signal userBanned; - Signal userUnbanned; + Signal raidStarted; + Signal raidCanceled; - // Message caught by automod - // channelID - pajlada::Signals::Signal - autoModMessageCaught; + Signal userBanned; + Signal userUnbanned; + Signal userWarned; - // Message blocked by moderator - Signal autoModMessageBlocked; + Signal suspiciousMessageReceived; + Signal suspiciousTreatmentUpdated; - Signal automodUserMessage; - Signal automodInfoMessage; - } moderation; + // Message caught by automod + // channelID + pajlada::Signals::Signal + autoModMessageCaught; - struct { - // Parsing should be done in PubSubManager as well, - // but for now we just send the raw data - Signal received; - Signal sent; - } whisper; + // Message blocked by moderator + Signal autoModMessageBlocked; - struct { - Signal redeemed; - } pointReward; - } signals_; + Signal automodUserMessage; + Signal automodInfoMessage; + } moderation; - void unlistenAllModerationActions(); - void unlistenAutomod(); + struct { + // Parsing should be done in PubSubManager as well, + // but for now we just send the raw data + Signal received; + Signal sent; + } whisper; + + struct { + Signal redeemed; + } pointReward; + + /** + * Listen to incoming whispers for the currently logged in user. + * This topic is relevant for everyone. + * + * PubSub topic: whispers.{currentUserID} + */ + bool listenToWhispers(); void unlistenWhispers(); - bool listenToWhispers(); + /** + * Listen to moderation actions in the given channel. + * This topic is relevant for everyone. + * For moderators, this topic includes blocked/permitted terms updates, + * roomstate changes, general mod/vip updates, all bans/timeouts/deletions. + * For normal users, this topic includes moderation actions that are targetted at the local user: + * automod catching a user's sent message, a moderator approving or denying their caught messages, + * the user gaining/losing mod/vip, the user receiving a ban/timeout/deletion. + * + * PubSub topic: chat_moderator_actions.{currentUserID}.{channelID} + */ void listenToChannelModerationActions(const QString &channelID); + void unlistenChannelModerationActions(); + + /** + * Listen to Automod events in the given channel. + * This topic is only relevant for moderators. + * This will send events about incoming messages that + * are caught by Automod. + * + * PubSub topic: automod-queue.{currentUserID}.{channelID} + */ void listenToAutomod(const QString &channelID); + void unlistenAutomod(); + /** + * Listen to Low Trust events in the given channel. + * This topic is only relevant for moderators. + * This will fire events about suspicious treatment updates + * and messages sent by restricted/monitored users. + * + * PubSub topic: low-trust-users.{currentUserID}.{channelID} + */ + void listenToLowTrustUsers(const QString &channelID); + void unlistenLowTrustUsers(); + + /** + * Listen to incoming channel point redemptions in the given channel. + * This topic is relevant for everyone. + * + * PubSub topic: community-points-channel-v1.{channelID} + */ void listenToChannelPointRewards(const QString &channelID); - - std::vector requests; + void unlistenChannelPointRewards(); struct { std::atomic connectionsClosed{0}; @@ -141,20 +204,31 @@ public: std::atomic unlistenResponses{0}; } diag; +private: + void setAccount(std::shared_ptr account); + + void start(); + void stop(); + + /** + * Unlistens to all topics matching the prefix in all clients + */ + void unlistenPrefix(const QString &prefix); + void listenToTopic(const QString &topic); -private: void listen(PubSubListenMessage msg); bool tryListen(PubSubListenMessage msg); bool isListeningToTopic(const QString &topic); void addClient(); + + std::vector requests; + std::atomic addingClient{false}; ExponentialBackoff<5> connectBackoff{std::chrono::milliseconds(1000)}; - State state = State::Connected; - std::map, std::owner_less> clients; @@ -182,7 +256,7 @@ private: void registerNonce(QString nonce, NonceInfo nonceInfo); // Find client associated with a nonce - boost::optional findNonceInfo(QString nonce); + std::optional findNonceInfo(QString nonce); std::unordered_map nonces_; @@ -194,6 +268,22 @@ private: const PubSubClientOptions clientOptions_; bool stopping_{false}; + +#ifdef FRIEND_TEST + friend class FTest; + + FRIEND_TEST(TwitchPubSubClient, ServerRespondsToPings); + FRIEND_TEST(TwitchPubSubClient, ServerDoesntRespondToPings); + FRIEND_TEST(TwitchPubSubClient, DisconnectedAfter1s); + FRIEND_TEST(TwitchPubSubClient, ExceedTopicLimit); + FRIEND_TEST(TwitchPubSubClient, ExceedTopicLimitSingleStep); + FRIEND_TEST(TwitchPubSubClient, ReceivedWhisper); + FRIEND_TEST(TwitchPubSubClient, ModeratorActionsUserBanned); + FRIEND_TEST(TwitchPubSubClient, MissingToken); + FRIEND_TEST(TwitchPubSubClient, WrongToken); + FRIEND_TEST(TwitchPubSubClient, CorrectToken); + FRIEND_TEST(TwitchPubSubClient, AutoModMessageHeld); +#endif }; } // namespace chatterino diff --git a/src/providers/twitch/PubSubMessages.hpp b/src/providers/twitch/PubSubMessages.hpp index f9cc5c501..48c4066b3 100644 --- a/src/providers/twitch/PubSubMessages.hpp +++ b/src/providers/twitch/PubSubMessages.hpp @@ -1,10 +1,11 @@ #pragma once -#include "providers/twitch/pubsubmessages/AutoMod.hpp" -#include "providers/twitch/pubsubmessages/Base.hpp" -#include "providers/twitch/pubsubmessages/ChannelPoints.hpp" -#include "providers/twitch/pubsubmessages/ChatModeratorAction.hpp" -#include "providers/twitch/pubsubmessages/Listen.hpp" -#include "providers/twitch/pubsubmessages/Message.hpp" -#include "providers/twitch/pubsubmessages/Unlisten.hpp" -#include "providers/twitch/pubsubmessages/Whisper.hpp" +#include "providers/twitch/pubsubmessages/AutoMod.hpp" // IWYU pragma: export +#include "providers/twitch/pubsubmessages/Base.hpp" // IWYU pragma: export +#include "providers/twitch/pubsubmessages/ChannelPoints.hpp" // IWYU pragma: export +#include "providers/twitch/pubsubmessages/ChatModeratorAction.hpp" // IWYU pragma: export +#include "providers/twitch/pubsubmessages/Listen.hpp" // IWYU pragma: export +#include "providers/twitch/pubsubmessages/LowTrustUsers.hpp" // IWYU pragma: export +#include "providers/twitch/pubsubmessages/Message.hpp" // IWYU pragma: export +#include "providers/twitch/pubsubmessages/Unlisten.hpp" // IWYU pragma: export +#include "providers/twitch/pubsubmessages/Whisper.hpp" // IWYU pragma: export diff --git a/src/providers/twitch/TwitchAccount.cpp b/src/providers/twitch/TwitchAccount.cpp index 733acea2f..363d313ec 100644 --- a/src/providers/twitch/TwitchAccount.cpp +++ b/src/providers/twitch/TwitchAccount.cpp @@ -1,26 +1,26 @@ #include "providers/twitch/TwitchAccount.hpp" -#include - #include "Application.hpp" #include "common/Channel.hpp" #include "common/Env.hpp" -#include "common/NetworkRequest.hpp" -#include "common/Outcome.hpp" +#include "common/network/NetworkResult.hpp" #include "common/QLogging.hpp" #include "controllers/accounts/AccountController.hpp" +#include "debug/AssertInGuiThread.hpp" #include "messages/Message.hpp" #include "messages/MessageBuilder.hpp" #include "providers/IvrApi.hpp" -#include "providers/irc/IrcMessageBuilder.hpp" -#include "providers/twitch/TwitchCommon.hpp" -#include "providers/twitch/TwitchUser.hpp" +#include "providers/seventv/SeventvAPI.hpp" #include "providers/twitch/api/Helix.hpp" +#include "providers/twitch/TwitchCommon.hpp" #include "singletons/Emotes.hpp" +#include "util/CancellationToken.hpp" #include "util/Helpers.hpp" #include "util/QStringHash.hpp" #include "util/RapidjsonHelpers.hpp" +#include + namespace chatterino { TwitchAccount::TwitchAccount(const QString &username, const QString &oauthToken, @@ -100,77 +100,79 @@ bool TwitchAccount::isAnon() const void TwitchAccount::loadBlocks() { + assertInGuiThread(); + + auto token = CancellationToken(false); + this->blockToken_ = token; + this->ignores_.clear(); + this->ignoresUserIds_.clear(); + getHelix()->loadBlocks( - getIApp()->getAccounts()->twitch.getCurrent()->userId_, - [this](std::vector blocks) { - auto ignores = this->ignores_.access(); - auto userIds = this->ignoresUserIds_.access(); - ignores->clear(); - userIds->clear(); + getApp()->getAccounts()->twitch.getCurrent()->userId_, + [this](const std::vector &blocks) { + assertInGuiThread(); for (const HelixBlock &block : blocks) { TwitchUser blockedUser; blockedUser.fromHelixBlock(block); - ignores->insert(blockedUser); - userIds->insert(blockedUser.id); + this->ignores_.insert(blockedUser); + this->ignoresUserIds_.insert(blockedUser.id); } }, - [] { - qCWarning(chatterinoTwitch) << "Fetching blocks failed!"; - }); + [](auto error) { + qCWarning(chatterinoTwitch).noquote() + << "Fetching blocks failed:" << error; + }, + std::move(token)); } -void TwitchAccount::blockUser(QString userId, std::function onSuccess, +void TwitchAccount::blockUser(const QString &userId, const QObject *caller, + std::function onSuccess, std::function onFailure) { getHelix()->blockUser( - userId, - [this, userId, onSuccess] { + userId, caller, + [this, userId, onSuccess = std::move(onSuccess)] { + assertInGuiThread(); + TwitchUser blockedUser; blockedUser.id = userId; - { - auto ignores = this->ignores_.access(); - auto userIds = this->ignoresUserIds_.access(); - - ignores->insert(blockedUser); - userIds->insert(blockedUser.id); - } + this->ignores_.insert(blockedUser); + this->ignoresUserIds_.insert(blockedUser.id); onSuccess(); }, std::move(onFailure)); } -void TwitchAccount::unblockUser(QString userId, std::function onSuccess, +void TwitchAccount::unblockUser(const QString &userId, const QObject *caller, + std::function onSuccess, std::function onFailure) { getHelix()->unblockUser( - userId, - [this, userId, onSuccess] { + userId, caller, + [this, userId, onSuccess = std::move(onSuccess)] { + assertInGuiThread(); + TwitchUser ignoredUser; ignoredUser.id = userId; - { - auto ignores = this->ignores_.access(); - auto userIds = this->ignoresUserIds_.access(); - - ignores->erase(ignoredUser); - userIds->erase(ignoredUser.id); - } + this->ignores_.erase(ignoredUser); + this->ignoresUserIds_.erase(ignoredUser.id); onSuccess(); }, std::move(onFailure)); } -SharedAccessGuard> TwitchAccount::accessBlocks() - const +const std::unordered_set &TwitchAccount::blocks() const { - return this->ignores_.accessConst(); + assertInGuiThread(); + return this->ignores_; } -SharedAccessGuard> TwitchAccount::accessBlockedUserIds() - const +const std::unordered_set &TwitchAccount::blockedUserIds() const { - return this->ignoresUserIds_.accessConst(); + assertInGuiThread(); + return this->ignoresUserIds_; } void TwitchAccount::loadEmotes(std::weak_ptr weakChannel) @@ -229,7 +231,7 @@ void TwitchAccount::loadUserstateEmotes(std::weak_ptr weakChannel) } // filter out emote sets from userstate message, which are not in fetched emote set list - for (const auto &emoteSetKey : qAsConst(this->userstateEmoteSets_)) + for (const auto &emoteSetKey : this->userstateEmoteSets_) { if (!existingEmoteSetKeys.contains(emoteSetKey)) { @@ -260,12 +262,33 @@ void TwitchAccount::loadUserstateEmotes(std::weak_ptr weakChannel) [this, weakChannel](QJsonArray emoteSetArray) { auto emoteData = this->emotes_.access(); auto localEmoteData = this->localEmotes_.access(); - for (auto emoteSet_ : emoteSetArray) + + std::unordered_set subscriberChannelIDs; + std::vector ivrEmoteSets; + ivrEmoteSets.reserve(emoteSetArray.size()); + + for (auto emoteSet : emoteSetArray) + { + IvrEmoteSet ivrEmoteSet(emoteSet.toObject()); + if (!ivrEmoteSet.tier.isNull()) + { + subscriberChannelIDs.insert(ivrEmoteSet.channelId); + } + ivrEmoteSets.emplace_back(ivrEmoteSet); + } + + for (const auto &emoteSet : emoteData->emoteSets) + { + if (emoteSet->subscriber) + { + subscriberChannelIDs.insert(emoteSet->channelID); + } + } + + for (const auto &ivrEmoteSet : ivrEmoteSets) { auto emoteSet = std::make_shared(); - IvrEmoteSet ivrEmoteSet(emoteSet_.toObject()); - QString setKey = ivrEmoteSet.setId; emoteSet->key = setKey; @@ -281,8 +304,15 @@ void TwitchAccount::loadUserstateEmotes(std::weak_ptr weakChannel) continue; } + emoteSet->channelID = ivrEmoteSet.channelId; emoteSet->channelName = ivrEmoteSet.login; emoteSet->text = ivrEmoteSet.displayName; + emoteSet->subscriber = !ivrEmoteSet.tier.isNull(); + + // NOTE: If a user does not have a subscriber emote set, but a follower emote set, this logic will be wrong + // However, that's not a realistic problem. + bool haveSubscriberSetForChannel = + subscriberChannelIDs.contains(ivrEmoteSet.channelId); for (const auto &emoteObj : ivrEmoteSet.emotes) { @@ -294,11 +324,15 @@ void TwitchAccount::loadUserstateEmotes(std::weak_ptr weakChannel) emoteSet->emotes.push_back(TwitchEmote{id, code}); - auto emote = - getApp()->emotes->twitch.getOrCreateEmote(id, code); + auto emote = getApp() + ->getEmotes() + ->getTwitchEmotes() + ->getOrCreateEmote(id, code); // Follower emotes can be only used in their origin channel - if (ivrEmote.emoteType == "FOLLOWER") + // unless the user is subscribed, then they can be used anywhere. + if (ivrEmote.emoteType == "FOLLOWER" && + !haveSubscriberSetForChannel) { emoteSet->local = true; @@ -327,8 +361,8 @@ void TwitchAccount::loadUserstateEmotes(std::weak_ptr weakChannel) if (auto channel = weakChannel.lock(); channel != nullptr) { - channel->addMessage(makeSystemMessage( - "Twitch subscriber emotes reloaded.")); + channel->addSystemMessage( + "Twitch subscriber emotes reloaded."); } }, [] { @@ -392,7 +426,7 @@ void TwitchAccount::autoModAllow(const QString msgID, ChannelPtr channel) break; } - channel->addMessage(makeSystemMessage(errorMessage)); + channel->addSystemMessage(errorMessage); }); } @@ -438,74 +472,46 @@ void TwitchAccount::autoModDeny(const QString msgID, ChannelPtr channel) break; } - channel->addMessage(makeSystemMessage(errorMessage)); + channel->addSystemMessage(errorMessage); }); } -void TwitchAccount::loadEmoteSetData(std::shared_ptr emoteSet) +const QString &TwitchAccount::getSeventvUserID() const { - if (!emoteSet) + return this->seventvUserID_; +} + +void TwitchAccount::loadSeventvUserID() +{ + if (this->isAnon()) + { + return; + } + if (!this->seventvUserID_.isEmpty()) { - qCWarning(chatterinoTwitch) << "null emote set sent"; return; } - auto staticSetIt = this->staticEmoteSets.find(emoteSet->key); - if (staticSetIt != this->staticEmoteSets.end()) + auto *seventv = getApp()->getSeventvAPI(); + if (!seventv) { - const auto &staticSet = staticSetIt->second; - emoteSet->channelName = staticSet.channelName; - emoteSet->text = staticSet.text; + qCWarning(chatterinoSeventv) + << "Not loading 7TV User ID because the 7TV API is not initialized"; return; } - getHelix()->getEmoteSetData( - emoteSet->key, - [emoteSet](HelixEmoteSetData emoteSetData) { - // Follower emotes can be only used in their origin channel - if (emoteSetData.emoteType == "follower") + seventv->getUserByTwitchID( + this->getUserId(), + [this](const auto &json) { + const auto id = json["user"]["id"].toString(); + if (!id.isEmpty()) { - emoteSet->local = true; + this->seventvUserID_ = id; } - - if (emoteSetData.ownerId.isEmpty() || - emoteSetData.setId != emoteSet->key) - { - qCDebug(chatterinoTwitch) - << QString("Failed to fetch emoteSetData for %1, assuming " - "Twitch is the owner") - .arg(emoteSet->key); - - // most (if not all) emotes that fail to load are time limited event emotes owned by Twitch - emoteSet->channelName = "twitch"; - emoteSet->text = "Twitch"; - - return; - } - - // emote set 0 = global emotes - if (emoteSetData.ownerId == "0") - { - // emoteSet->channelName = QString(); - emoteSet->text = "Twitch Global"; - return; - } - - getHelix()->getUserById( - emoteSetData.ownerId, - [emoteSet](HelixUser user) { - emoteSet->channelName = user.login; - emoteSet->text = user.displayName; - }, - [emoteSetData] { - qCWarning(chatterinoTwitch) - << "Failed to query user by id:" << emoteSetData.ownerId - << emoteSetData.setId; - }); }, - [emoteSet] { - // fetching emoteset data failed - return; + [](const auto &result) { + qCDebug(chatterinoSeventv) + << "Failed to load 7TV user-id:" << result.formatError(); }); } diff --git a/src/providers/twitch/TwitchAccount.hpp b/src/providers/twitch/TwitchAccount.hpp index 4d1d6be6c..69032b80c 100644 --- a/src/providers/twitch/TwitchAccount.hpp +++ b/src/providers/twitch/TwitchAccount.hpp @@ -2,56 +2,28 @@ #include "common/Aliases.hpp" #include "common/Atomic.hpp" -#include "common/Channel.hpp" #include "common/UniqueAccess.hpp" #include "controllers/accounts/Account.hpp" #include "messages/Emote.hpp" #include "providers/twitch/TwitchUser.hpp" +#include "util/CancellationToken.hpp" #include "util/QStringHash.hpp" -#include #include #include +#include #include +#include #include #include -#include +#include +#include namespace chatterino { -enum FollowResult { - FollowResult_Following, - FollowResult_NotFollowing, - FollowResult_Failed, -}; - -struct TwitchEmoteSetResolverResponse { - const QString channelName; - const QString channelId; - const QString type; - const int tier; - const bool isCustom; - // Example response: - // { - // "channel_name": "zneix", - // "channel_id": "99631238", - // "type": "", - // "tier": 1, - // "custom": false - // } - - TwitchEmoteSetResolverResponse(QJsonObject jsonObject) - : channelName(jsonObject.value("channel_name").toString()) - , channelId(jsonObject.value("channel_id").toString()) - , type(jsonObject.value("type").toString()) - , tier(jsonObject.value("tier").toInt()) - , isCustom(jsonObject.value("custom").toBool()) - { - } -}; - -std::vector getEmoteSetBatches(QStringList emoteSetKeys); +class Channel; +using ChannelPtr = std::shared_ptr; class TwitchAccount : public Account { @@ -64,13 +36,13 @@ public: struct EmoteSet { QString key; QString channelName; + QString channelID; QString text; + bool subscriber{false}; bool local{false}; std::vector emotes; }; - std::map staticEmoteSets; - struct TwitchAccountEmoteData { std::vector> emoteSets; @@ -82,13 +54,19 @@ public: TwitchAccount(const QString &username, const QString &oauthToken_, const QString &oauthClient_, const QString &_userID); - virtual QString toString() const override; + QString toString() const override; const QString &getUserName() const; const QString &getOAuthToken() const; const QString &getOAuthClient() const; const QString &getUserId() const; + /** + * The Seventv user-id of the current user. + * Empty if there's no associated Seventv user with this twitch user. + */ + const QString &getSeventvUserID() const; + QColor color(); void setColor(QColor color); @@ -103,13 +81,15 @@ public: bool isAnon() const; void loadBlocks(); - void blockUser(QString userId, std::function onSuccess, + void blockUser(const QString &userId, const QObject *caller, + std::function onSuccess, std::function onFailure); - void unblockUser(QString userId, std::function onSuccess, + void unblockUser(const QString &userId, const QObject *caller, + std::function onSuccess, std::function onFailure); - SharedAccessGuard> accessBlockedUserIds() const; - SharedAccessGuard> accessBlocks() const; + [[nodiscard]] const std::unordered_set &blocks() const; + [[nodiscard]] const std::unordered_set &blockedUserIds() const; void loadEmotes(std::weak_ptr weakChannel = {}); // loadUserstateEmotes loads emote sets that are part of the USERSTATE emote-sets key @@ -126,9 +106,9 @@ public: void autoModAllow(const QString msgID, ChannelPtr channel); void autoModDeny(const QString msgID, ChannelPtr channel); -private: - void loadEmoteSetData(std::shared_ptr emoteSet); + void loadSeventvUserID(); +private: QString oauthClient_; QString oauthToken_; QString userName_; @@ -136,14 +116,17 @@ private: const bool isAnon_; Atomic color_; - mutable std::mutex ignoresMutex_; QStringList userstateEmoteSets_; - UniqueAccess> ignores_; - UniqueAccess> ignoresUserIds_; + + ScopedCancellationToken blockToken_; + std::unordered_set ignores_; + std::unordered_set ignoresUserIds_; // std::map emotes; UniqueAccess emotes_; UniqueAccess> localEmotes_; + + QString seventvUserID_; }; } // namespace chatterino diff --git a/src/providers/twitch/TwitchAccountManager.cpp b/src/providers/twitch/TwitchAccountManager.cpp index 573ba7906..7d1d30246 100644 --- a/src/providers/twitch/TwitchAccountManager.cpp +++ b/src/providers/twitch/TwitchAccountManager.cpp @@ -2,9 +2,11 @@ #include "common/Common.hpp" #include "common/QLogging.hpp" +#include "providers/twitch/api/Helix.hpp" #include "providers/twitch/TwitchAccount.hpp" #include "providers/twitch/TwitchCommon.hpp" -#include "providers/twitch/api/Helix.hpp" +#include "providers/twitch/TwitchUser.hpp" +#include "util/SharedPtrElementLess.hpp" namespace chatterino { @@ -15,9 +17,12 @@ TwitchAccountManager::TwitchAccountManager() this->currentUserChanged.connect([this] { auto currentUser = this->getCurrent(); currentUser->loadBlocks(); + currentUser->loadSeventvUserID(); }); - this->accounts.itemRemoved.connect([this](const auto &acc) { + // We can safely ignore this signal connection since accounts will always be removed + // before TwitchAccountManager + std::ignore = this->accounts.itemRemoved.connect([this](const auto &acc) { this->removeUser(acc.item.get()); }); } diff --git a/src/providers/twitch/TwitchAccountManager.hpp b/src/providers/twitch/TwitchAccountManager.hpp index eaa303c1a..d3bec563b 100644 --- a/src/providers/twitch/TwitchAccountManager.hpp +++ b/src/providers/twitch/TwitchAccountManager.hpp @@ -2,11 +2,13 @@ #include "common/ChatterinoSetting.hpp" #include "common/SignalVector.hpp" -#include "providers/twitch/TwitchAccount.hpp" -#include "util/SharedPtrElementLess.hpp" +#include "util/QStringHash.hpp" +#include "util/RapidJsonSerializeQString.hpp" #include +#include +#include #include #include diff --git a/src/providers/twitch/TwitchBadges.cpp b/src/providers/twitch/TwitchBadges.cpp index 11fe07af5..6e2b4c4aa 100644 --- a/src/providers/twitch/TwitchBadges.cpp +++ b/src/providers/twitch/TwitchBadges.cpp @@ -1,88 +1,137 @@ -#include "TwitchBadges.hpp" +#include "providers/twitch/TwitchBadges.hpp" + +#include "common/network/NetworkRequest.hpp" +#include "common/network/NetworkResult.hpp" +#include "common/QLogging.hpp" +#include "messages/Emote.hpp" +#include "messages/Image.hpp" +#include "providers/twitch/api/Helix.hpp" +#include "util/DisplayBadge.hpp" +#include "util/LoadPixmap.hpp" #include +#include #include #include #include -#include +#include #include #include #include -#include "common/NetworkRequest.hpp" -#include "common/Outcome.hpp" -#include "common/QLogging.hpp" -#include "messages/Emote.hpp" +namespace { + +// From Twitch docs - expected size for a badge (1x) +constexpr QSize BADGE_BASE_SIZE(18, 18); + +} // namespace namespace chatterino { -TwitchBadges::TwitchBadges() -{ - this->loadTwitchBadges(); -} - void TwitchBadges::loadTwitchBadges() { assert(this->loaded_ == false); - QUrl url("https://badges.twitch.tv/v1/badges/global/display"); + getHelix()->getGlobalBadges( + [this](auto globalBadges) { + auto badgeSets = this->badgeSets_.access(); - QUrlQuery urlQuery; - urlQuery.addQueryItem("language", "en"); - url.setQuery(urlQuery); - - NetworkRequest(url) - .onSuccess([this](auto result) -> Outcome { + for (const auto &badgeSet : globalBadges.badgeSets) { - auto root = result.parseJson(); - auto badgeSets = this->badgeSets_.access(); - - auto jsonSets = root.value("badge_sets").toObject(); - for (auto sIt = jsonSets.begin(); sIt != jsonSets.end(); ++sIt) + const auto &setID = badgeSet.setID; + for (const auto &version : badgeSet.versions) { - auto key = sIt.key(); - auto versions = - sIt.value().toObject().value("versions").toObject(); - - for (auto vIt = versions.begin(); vIt != versions.end(); - ++vIt) - { - auto versionObj = vIt.value().toObject(); - - auto emote = Emote{ - {""}, + const auto &emote = Emote{ + .name = EmoteName{}, + .images = ImageSet{ - Image::fromUrl({versionObj.value("image_url_1x") - .toString()}, - 1), - Image::fromUrl({versionObj.value("image_url_2x") - .toString()}, - .5), - Image::fromUrl({versionObj.value("image_url_4x") - .toString()}, - .25), + Image::fromUrl(version.imageURL1x, 1, + BADGE_BASE_SIZE), + Image::fromUrl(version.imageURL2x, .5, + BADGE_BASE_SIZE * 2), + Image::fromUrl(version.imageURL4x, .25, + BADGE_BASE_SIZE * 4), }, - Tooltip{versionObj.value("title").toString()}, - Url{versionObj.value("click_url").toString()}}; - // "title" - // "clickAction" - - (*badgeSets)[key][vIt.key()] = - std::make_shared(emote); - } + .tooltip = Tooltip{version.title}, + .homePage = version.clickURL, + }; + (*badgeSets)[setID][version.id] = + std::make_shared(emote); } } + this->loaded(); - return Success; - }) - .onError([this](auto res) { - qCDebug(chatterinoTwitch) - << "Error loading Twitch Badges:" << res.status(); - // Despite erroring out, we still want to reach the same point - // Loaded should still be set to true to not build up an endless queue, and the quuee should still be flushed. + }, + [this](auto error, auto message) { + QString errorMessage("Failed to load global badges - "); + + switch (error) + { + case HelixGetGlobalBadgesError::Forwarded: { + errorMessage += message; + } + break; + + // This would most likely happen if the service is down, or if the JSON payload returned has changed format + case HelixGetGlobalBadgesError::Unknown: { + errorMessage += "An unknown error has occurred."; + } + break; + } + qCWarning(chatterinoTwitch) << errorMessage; + QFile file(":/twitch-badges.json"); + if (!file.open(QFile::ReadOnly)) + { + // Despite erroring out, we still want to reach the same point + // Loaded should still be set to true to not build up an endless queue, and the quuee should still be flushed. + qCWarning(chatterinoTwitch) + << "Error loading Twitch Badges from the local backup file"; + this->loaded(); + return; + } + auto bytes = file.readAll(); + auto doc = QJsonDocument::fromJson(bytes); + + this->parseTwitchBadges(doc.object()); + this->loaded(); - }) - .execute(); + }); +} + +void TwitchBadges::parseTwitchBadges(QJsonObject root) +{ + auto badgeSets = this->badgeSets_.access(); + + auto jsonSets = root.value("badge_sets").toObject(); + for (auto sIt = jsonSets.begin(); sIt != jsonSets.end(); ++sIt) + { + auto key = sIt.key(); + auto versions = sIt.value().toObject().value("versions").toObject(); + + for (auto vIt = versions.begin(); vIt != versions.end(); ++vIt) + { + auto versionObj = vIt.value().toObject(); + auto emote = Emote{ + .name = {""}, + .images = + ImageSet{ + Image::fromUrl( + {versionObj.value("image_url_1x").toString()}, 1, + BADGE_BASE_SIZE), + Image::fromUrl( + {versionObj.value("image_url_2x").toString()}, .5, + BADGE_BASE_SIZE * 2), + Image::fromUrl( + {versionObj.value("image_url_4x").toString()}, .25, + BADGE_BASE_SIZE * 4), + }, + .tooltip = Tooltip{versionObj.value("title").toString()}, + .homePage = Url{versionObj.value("click_url").toString()}, + }; + + (*badgeSets)[key][vIt.key()] = std::make_shared(emote); + } + } } void TwitchBadges::loaded() @@ -107,8 +156,8 @@ void TwitchBadges::loaded() } } -boost::optional TwitchBadges::badge(const QString &set, - const QString &version) const +std::optional TwitchBadges::badge(const QString &set, + const QString &version) const { auto badgeSets = this->badgeSets_.access(); auto it = badgeSets->find(set); @@ -120,21 +169,22 @@ boost::optional TwitchBadges::badge(const QString &set, return it2->second; } } - return boost::none; + return std::nullopt; } -boost::optional TwitchBadges::badge(const QString &set) const +std::optional TwitchBadges::badge(const QString &set) const { auto badgeSets = this->badgeSets_.access(); auto it = badgeSets->find(set); if (it != badgeSets->end()) { - if (it->second.size() > 0) + const auto &badges = it->second; + if (!badges.empty()) { - return it->second.begin()->second; + return badges.begin()->second; } } - return boost::none; + return std::nullopt; } void TwitchBadges::getBadgeIcon(const QString &name, BadgeIconCallback callback) @@ -146,7 +196,7 @@ void TwitchBadges::getBadgeIcon(const QString &name, BadgeIconCallback callback) { // Badges have not been loaded yet, store callback in a queue std::unique_lock queueLock(this->queueMutex_); - this->callbackQueue_.push({name, std::move(callback)}); + this->callbackQueue_.emplace(name, std::move(callback)); return; } } @@ -190,60 +240,20 @@ void TwitchBadges::getBadgeIcons(const QList &badges, } } -void TwitchBadges::loadEmoteImage(const QString &name, ImagePtr image, +void TwitchBadges::loadEmoteImage(const QString &name, const ImagePtr &image, BadgeIconCallback &&callback) { - NetworkRequest(image->url().string) - .concurrent() - .cache() - .onSuccess([this, name, callback](auto result) -> Outcome { - auto data = result.getData(); + loadPixmapFromUrl(image->url(), + [this, name, callback{std::move(callback)}](auto pixmap) { + auto icon = std::make_shared(pixmap); - // const cast since we are only reading from it - QBuffer buffer(const_cast(&data)); - buffer.open(QIODevice::ReadOnly); - QImageReader reader(&buffer); + { + std::unique_lock lock(this->badgesMutex_); + this->badgesMap_[name] = icon; + } - if (!reader.canRead() || reader.size().isEmpty()) - { - return Failure; - } - - QImage image = reader.read(); - if (image.isNull()) - { - return Failure; - } - - if (reader.imageCount() <= 0) - { - return Failure; - } - - auto icon = std::make_shared(QPixmap::fromImage(image)); - - { - std::unique_lock lock(this->badgesMutex_); - this->badgesMap_[name] = icon; - } - - callback(name, icon); - - return Success; - }) - .execute(); -} - -TwitchBadges *TwitchBadges::instance_; - -TwitchBadges *TwitchBadges::instance() -{ - if (TwitchBadges::instance_ == nullptr) - { - TwitchBadges::instance_ = new TwitchBadges(); - } - - return TwitchBadges::instance_; + callback(name, icon); + }); } } // namespace chatterino diff --git a/src/providers/twitch/TwitchBadges.hpp b/src/providers/twitch/TwitchBadges.hpp index abbd5aeb8..fff0f5aff 100644 --- a/src/providers/twitch/TwitchBadges.hpp +++ b/src/providers/twitch/TwitchBadges.hpp @@ -1,20 +1,19 @@ #pragma once -#include -#include -#include -#include - #include "common/UniqueAccess.hpp" -#include "messages/Image.hpp" -#include "util/DisplayBadge.hpp" #include "util/QStringHash.hpp" -#include "pajlada/signals/signal.hpp" +#include +#include +#include +#include +#include #include +#include #include #include +#include namespace chatterino { @@ -23,6 +22,8 @@ using EmotePtr = std::shared_ptr; class Settings; class Paths; +class Image; +class DisplayBadge; class TwitchBadges { @@ -31,26 +32,23 @@ class TwitchBadges using BadgeIconCallback = std::function; public: - static TwitchBadges *instance(); - // Get badge from name and version - boost::optional badge(const QString &set, - const QString &version) const; + std::optional badge(const QString &set, + const QString &version) const; // Get first matching badge with name, regardless of version - boost::optional badge(const QString &set) const; + std::optional badge(const QString &set) const; void getBadgeIcon(const QString &name, BadgeIconCallback callback); void getBadgeIcon(const DisplayBadge &badge, BadgeIconCallback callback); void getBadgeIcons(const QList &badges, BadgeIconCallback callback); -private: - static TwitchBadges *instance_; - - TwitchBadges(); void loadTwitchBadges(); + +private: + void parseTwitchBadges(QJsonObject root); void loaded(); - void loadEmoteImage(const QString &name, ImagePtr image, + void loadEmoteImage(const QString &name, const ImagePtr &image, BadgeIconCallback &&callback); std::shared_mutex badgesMutex_; diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index 317a86978..ba020f529 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -1,41 +1,63 @@ #include "providers/twitch/TwitchChannel.hpp" +#include "Application.hpp" #include "common/Common.hpp" -#include "common/Env.hpp" -#include "common/NetworkRequest.hpp" +#include "common/network/NetworkRequest.hpp" +#include "common/network/NetworkResult.hpp" #include "common/QLogging.hpp" #include "controllers/accounts/AccountController.hpp" #include "controllers/notifications/NotificationController.hpp" +#include "controllers/twitch/LiveController.hpp" +#include "messages/Emote.hpp" +#include "messages/Image.hpp" +#include "messages/Link.hpp" #include "messages/Message.hpp" -#include "providers/RecentMessagesApi.hpp" +#include "messages/MessageBuilder.hpp" +#include "messages/MessageElement.hpp" +#include "messages/MessageThread.hpp" #include "providers/bttv/BttvEmotes.hpp" -#include "providers/bttv/LoadBttvChannelEmote.hpp" +#include "providers/bttv/BttvLiveUpdates.hpp" +#include "providers/bttv/liveupdates/BttvLiveUpdateMessages.hpp" +#include "providers/ffz/FfzBadges.hpp" +#include "providers/ffz/FfzEmotes.hpp" +#include "providers/recentmessages/Api.hpp" +#include "providers/seventv/eventapi/Dispatch.hpp" +#include "providers/seventv/SeventvAPI.hpp" +#include "providers/seventv/SeventvEmotes.hpp" +#include "providers/seventv/SeventvEventAPI.hpp" +#include "providers/twitch/api/Helix.hpp" +#include "providers/twitch/ChannelPointReward.hpp" #include "providers/twitch/IrcMessageHandler.hpp" #include "providers/twitch/PubSubManager.hpp" +#include "providers/twitch/TwitchAccount.hpp" #include "providers/twitch/TwitchCommon.hpp" #include "providers/twitch/TwitchIrcServer.hpp" -#include "providers/twitch/TwitchMessageBuilder.hpp" -#include "providers/twitch/api/Helix.hpp" #include "singletons/Emotes.hpp" #include "singletons/Settings.hpp" +#include "singletons/StreamerMode.hpp" #include "singletons/Toasts.hpp" #include "singletons/WindowManager.hpp" +#include "util/Helpers.hpp" #include "util/PostToThread.hpp" #include "util/QStringHash.hpp" #include "widgets/Window.hpp" -#include #include #include +#include #include #include #include #include +#include namespace chatterino { namespace { - constexpr char MAGIC_MESSAGE_SUFFIX[] = u8" \U000E0000"; - constexpr int TITLE_REFRESH_PERIOD = 10000; +#if QT_VERSION < QT_VERSION_CHECK(6, 1, 0) + const QString MAGIC_MESSAGE_SUFFIX = QString((const char *)u8" \U000E0000"); +#else + const QString MAGIC_MESSAGE_SUFFIX = QString::fromUtf8(u8" \U000E0000"); +#endif constexpr int CLIP_CREATION_COOLDOWN = 5000; const QString CLIPS_LINK("https://clips.twitch.tv/%1"); const QString CLIPS_FAILURE_CLIPS_DISABLED_TEXT( @@ -48,97 +70,58 @@ namespace { const QString LOGIN_PROMPT_TEXT("Click here to add your account again."); const Link ACCOUNTS_LINK(Link::OpenAccountsPage, QString()); - std::pair> parseChatters( - const QJsonObject &jsonRoot) - { - static QStringList categories = {"broadcaster", "vips", "moderators", - "staff", "admins", "global_mods", - "viewers"}; - - auto usernames = std::unordered_set(); - - // parse json - QJsonObject jsonCategories = jsonRoot.value("chatters").toObject(); - - for (const auto &category : categories) - { - for (auto jsonCategory : jsonCategories.value(category).toArray()) - { - usernames.insert(jsonCategory.toString()); - } - } - - return {Success, std::move(usernames)}; - } + // Maximum number of chatters to fetch when refreshing chatters + constexpr auto MAX_CHATTERS_TO_FETCH = 5000; + // From Twitch docs - expected size for a badge (1x) + constexpr QSize BASE_BADGE_SIZE(18, 18); } // namespace TwitchChannel::TwitchChannel(const QString &name) : Channel(name, Channel::Type::Twitch) , ChannelChatters(*static_cast(this)) - , nameOptions{name, name} + , nameOptions{name, name, name} , subscriptionUrl_("https://www.twitch.tv/subs/" + name) , channelUrl_("https://twitch.tv/" + name) - , popoutPlayerUrl_("https://player.twitch.tv/?parent=twitch.tv&channel=" + - name) + , popoutPlayerUrl_(TWITCH_PLAYER_URL.arg(name)) , bttvEmotes_(std::make_shared()) , ffzEmotes_(std::make_shared()) - , mod_(false) + , seventvEmotes_(std::make_shared()) { qCDebug(chatterinoTwitch) << "[TwitchChannel" << name << "] Opened"; this->bSignals_.emplace_back( - getApp()->accounts->twitch.currentUserChanged.connect([=] { + getApp()->getAccounts()->twitch.currentUserChanged.connect([this] { this->setMod(false); this->refreshPubSub(); })); this->refreshPubSub(); - this->userStateChanged.connect([this] { + // We can safely ignore this signal connection since it's a private signal, meaning + // it will only ever be invoked by TwitchChannel itself + std::ignore = this->userStateChanged.connect([this] { this->refreshPubSub(); }); - // room id loaded -> refresh live status - this->roomIdChanged.connect([this]() { - this->refreshPubSub(); - this->refreshTitle(); - this->refreshLiveStatus(); - this->refreshBadges(); - this->refreshCheerEmotes(); - this->refreshFFZChannelEmotes(false); - this->refreshBTTVChannelEmotes(false); - }); - - this->connected.connect([this]() { - if (this->roomId().isEmpty()) + // We can safely ignore this signal connection this has no external dependencies - once the signal + // is destroyed, it will no longer be able to fire + std::ignore = this->joined.connect([this]() { + if (this->disconnected_) { - // If we get a reconnected event when the room id is not set, we - // just connected for the first time. After receiving the first - // message from a channel, setRoomId is called and further - // invocations of this event will load recent messages. - return; - } - - this->loadRecentMessagesReconnect(); - }); - - this->messageRemovedFromStart.connect([this](MessagePtr &msg) { - if (msg->replyThread) - { - if (msg->replyThread->liveCount(msg) == 0) - { - this->threads_.erase(msg->replyThread->rootId()); - } + this->loadRecentMessagesReconnect(); + this->lastConnectedAt_ = std::chrono::system_clock::now(); + this->disconnected_ = false; } }); // timers - QObject::connect(&this->chattersListTimer_, &QTimer::timeout, [=] { + QObject::connect(&this->chattersListTimer_, &QTimer::timeout, [this] { this->refreshChatters(); }); + this->chattersListTimer_.start(5 * 60 * 1000); - QObject::connect(&this->threadClearTimer_, &QTimer::timeout, [=] { + QObject::connect(&this->threadClearTimer_, &QTimer::timeout, [this] { // We periodically check for any dangling reply threads that missed // being cleaned up on messageRemovedFromStart. This could occur if // some other part of the program, like a user card, held a reference @@ -153,14 +136,30 @@ TwitchChannel::TwitchChannel(const QString &name) // debugging #if 0 for (int i = 0; i < 1000; i++) { - this->addMessage(makeSystemMessage("asef")); + this->addSystemMessage("asef"); } #endif } +TwitchChannel::~TwitchChannel() +{ + getApp()->getTwitch()->dropSeventvChannel(this->seventvUserID_, + this->seventvEmoteSetID_); + + if (getApp()->getBttvLiveUpdates()) + { + getApp()->getBttvLiveUpdates()->partChannel(this->roomId()); + } + + if (getApp()->getSeventvEventAPI()) + { + getApp()->getSeventvEventAPI()->unsubscribeTwitchChannel( + this->roomId()); + } +} + void TwitchChannel::initialize() { - this->fetchDisplayName(); this->refreshChatters(); this->refreshBadges(); } @@ -207,8 +206,9 @@ void TwitchChannel::refreshBTTVChannelEmotes(bool manualRefresh) weakOf(this), this->roomId(), this->getLocalizedName(), [this, weak = weakOf(this)](auto &&emoteMap) { if (auto shared = weak.lock()) - this->bttvEmotes_.set( - std::make_shared(std::move(emoteMap))); + { + this->setBttvEmotes(std::make_shared(emoteMap)); + } }, manualRefresh); } @@ -225,34 +225,95 @@ void TwitchChannel::refreshFFZChannelEmotes(bool manualRefresh) weakOf(this), this->roomId(), [this, weak = weakOf(this)](auto &&emoteMap) { if (auto shared = weak.lock()) - this->ffzEmotes_.set( - std::make_shared(std::move(emoteMap))); + { + this->setFfzEmotes(std::make_shared(emoteMap)); + } }, [this, weak = weakOf(this)](auto &&modBadge) { if (auto shared = weak.lock()) { - this->ffzCustomModBadge_.set(std::move(modBadge)); + this->ffzCustomModBadge_.set( + std::forward(modBadge)); } }, [this, weak = weakOf(this)](auto &&vipBadge) { if (auto shared = weak.lock()) { - this->ffzCustomVipBadge_.set(std::move(vipBadge)); + this->ffzCustomVipBadge_.set( + std::forward(vipBadge)); + } + }, + [this, weak = weakOf(this)](auto &&channelBadges) { + if (auto shared = weak.lock()) + { + this->tgFfzChannelBadges_.guard(); + this->ffzChannelBadges_ = + std::forward(channelBadges); } }, manualRefresh); } +void TwitchChannel::refreshSevenTVChannelEmotes(bool manualRefresh) +{ + if (!Settings::instance().enableSevenTVChannelEmotes) + { + this->seventvEmotes_.set(EMPTY_EMOTE_MAP); + return; + } + + SeventvEmotes::loadChannelEmotes( + weakOf(this), this->roomId(), + [this, weak = weakOf(this)](auto &&emoteMap, + auto channelInfo) { + if (auto shared = weak.lock()) + { + this->setSeventvEmotes( + std::make_shared(emoteMap)); + this->updateSeventvData(channelInfo.userID, + channelInfo.emoteSetID); + this->seventvUserTwitchConnectionIndex_ = + channelInfo.twitchConnectionIndex; + } + }, + manualRefresh); +} + +void TwitchChannel::setBttvEmotes(std::shared_ptr &&map) +{ + this->bttvEmotes_.set(std::move(map)); +} + +void TwitchChannel::setFfzEmotes(std::shared_ptr &&map) +{ + this->ffzEmotes_.set(std::move(map)); +} + +void TwitchChannel::setSeventvEmotes(std::shared_ptr &&map) +{ + this->seventvEmotes_.set(std::move(map)); +} + +void TwitchChannel::addQueuedRedemption(const QString &rewardId, + const QString &originalContent, + Communi::IrcMessage *message) +{ + this->waitingRedemptions_.push_back({ + rewardId, + originalContent, + {message->clone(), {}}, + }); +} + void TwitchChannel::addChannelPointReward(const ChannelPointReward &reward) { assertInGuiThread(); if (!reward.isUserInputRequired) { - MessageBuilder builder; - TwitchMessageBuilder::appendChannelPointRewardMessage( - reward, &builder, this->isMod(), this->isBroadcaster()); - this->addMessage(builder.release()); + this->addMessage(MessageBuilder::makeChannelPointRewardMessage( + reward, this->isMod(), this->isBroadcaster()), + MessageContext::Original); return; } @@ -263,7 +324,26 @@ void TwitchChannel::addChannelPointReward(const ChannelPointReward &reward) } if (result) { - this->channelPointRewardAdded.invoke(reward); + const auto &channelName = this->getName(); + qCDebug(chatterinoTwitch) + << "[TwitchChannel" << channelName + << "] Channel point reward added:" << reward.id << "," + << reward.title << "," << reward.isUserInputRequired; + + auto *server = getApp()->getTwitch(); + auto it = std::remove_if( + this->waitingRedemptions_.begin(), this->waitingRedemptions_.end(), + [&](const QueuedRedemption &msg) { + if (reward.id == msg.rewardID) + { + IrcMessageHandler::instance().addMessage( + msg.message.get(), shared_from_this(), + msg.originalContent, *server, false, false); + return true; + } + return false; + }); + this->waitingRedemptions_.erase(it, this->waitingRedemptions_.end()); } } @@ -274,22 +354,159 @@ bool TwitchChannel::isChannelPointRewardKnown(const QString &rewardId) return it != pointRewards->end(); } -boost::optional TwitchChannel::channelPointReward( +std::optional TwitchChannel::channelPointReward( const QString &rewardId) const { auto rewards = this->channelPointRewards_.accessConst(); auto it = rewards->find(rewardId); if (it == rewards->end()) - return boost::none; + { + return std::nullopt; + } return it->second; } +void TwitchChannel::updateStreamStatus( + const std::optional &helixStream, bool isInitialUpdate) +{ + if (helixStream) + { + auto stream = *helixStream; + { + auto status = this->streamStatus_.access(); + status->streamId = stream.id; + status->viewerCount = stream.viewerCount; + status->gameId = stream.gameId; + status->game = stream.gameName; + status->title = stream.title; + QDateTime since = + QDateTime::fromString(stream.startedAt, Qt::ISODate); + auto diff = since.secsTo(QDateTime::currentDateTime()); + status->uptime = QString::number(diff / 3600) + "h " + + QString::number(diff % 3600 / 60) + "m"; + status->uptimeSeconds = diff; + + status->rerun = false; + status->streamType = stream.type; + for (const auto &tag : stream.tags) + { + if (QString::compare(tag, "Rerun", Qt::CaseInsensitive) == 0) + { + status->rerun = true; + status->streamType = "rerun"; + break; + } + } + } + if (this->setLive(true)) + { + this->onLiveStatusChanged(true, isInitialUpdate); + } + this->streamStatusChanged.invoke(); + } + else + { + if (this->setLive(false)) + { + this->onLiveStatusChanged(false, isInitialUpdate); + this->streamStatusChanged.invoke(); + } + } +} + +void TwitchChannel::onLiveStatusChanged(bool isLive, bool isInitialUpdate) +{ + // Similar code exists in NotificationController::updateFakeChannel. + // Since we're a TwitchChannel, we also send a message here. + if (isLive) + { + qCDebug(chatterinoTwitch).nospace().noquote() + << "[TwitchChannel " << this->getName() << "] Online"; + + getApp()->getNotifications()->notifyTwitchChannelLive({ + .channelId = this->roomId(), + .channelName = this->getName(), + .displayName = this->getDisplayName(), + .title = this->accessStreamStatus()->title, + .isInitialUpdate = isInitialUpdate, + }); + + // Channel live message + this->addMessage( + MessageBuilder::makeLiveMessage( + this->getDisplayName(), this->roomId(), + {MessageFlag::System, MessageFlag::DoNotTriggerNotification}), + MessageContext::Original); + } + else + { + qCDebug(chatterinoTwitch).nospace().noquote() + << "[TwitchChannel " << this->getName() << "] Offline"; + + // Channel offline message + this->addMessage(MessageBuilder::makeOfflineSystemMessage( + this->getDisplayName(), this->roomId()), + MessageContext::Original); + + getApp()->getNotifications()->notifyTwitchChannelOffline( + this->roomId()); + } +}; + +void TwitchChannel::updateStreamTitle(const QString &title) +{ + { + auto status = this->streamStatus_.access(); + if (status->title == title) + { + // Title has not changed + return; + } + status->title = title; + } + this->streamStatusChanged.invoke(); +} + +void TwitchChannel::updateDisplayName(const QString &displayName) +{ + if (displayName == this->nameOptions.actualDisplayName) + { + // Display name has not changed + return; + } + + // Display name has changed + + this->nameOptions.actualDisplayName = displayName; + + if (QString::compare(displayName, this->getName(), Qt::CaseInsensitive) == + 0) + { + // Display name is only a case variation of the login name + this->setDisplayName(displayName); + + this->setLocalizedName(displayName); + } + else + { + // Display name contains Chinese, Japanese, or Korean characters + this->setDisplayName(this->getName()); + + this->setLocalizedName( + QString("%1(%2)").arg(this->getName()).arg(displayName)); + } + + this->addRecentChatter(this->getDisplayName()); + + this->displayNameChanged.invoke(); +} + void TwitchChannel::showLoginMessage() { const auto linkColor = MessageColor(MessageColor::Link); const auto accountsLink = Link(Link::OpenAccountsPage, QString()); - const auto currentUser = getApp()->accounts->twitch.getCurrent(); + const auto currentUser = getApp()->getAccounts()->twitch.getCurrent(); const auto expirationText = QStringLiteral("You need to log in to send messages. You can link your " "Twitch account"); @@ -307,18 +524,33 @@ void TwitchChannel::showLoginMessage() linkColor) ->setLink(accountsLink); - this->addMessage(builder.release()); + this->addMessage(builder.release(), MessageContext::Original); +} + +void TwitchChannel::roomIdChanged() +{ + if (getApp()->isTest()) + { + return; + } + this->refreshPubSub(); + this->refreshBadges(); + this->refreshCheerEmotes(); + this->refreshFFZChannelEmotes(false); + this->refreshBTTVChannelEmotes(false); + this->refreshSevenTVChannelEmotes(false); + this->joinBttvChannel(); + this->listenSevenTVCosmetics(); + getApp()->getTwitchLiveController()->add( + std::dynamic_pointer_cast(shared_from_this())); } QString TwitchChannel::prepareMessage(const QString &message) const { - auto app = getApp(); - QString parsedMessage = app->emotes->emojis.replaceShortCodes(message); + auto *app = getApp(); + QString parsedMessage = + app->getEmotes()->getEmojis()->replaceShortCodes(message); - // This is to make sure that combined emoji go through properly, see - // https://github.com/Chatterino/chatterino2/issues/3384 and - // https://mm2pl.github.io/emoji_rfc.pdf for more details - parsedMessage.replace(ZERO_WIDTH_JOINER, ESCAPE_TAG); parsedMessage = parsedMessage.simplified(); if (parsedMessage.isEmpty()) @@ -362,8 +594,8 @@ QString TwitchChannel::prepareMessage(const QString &message) const void TwitchChannel::sendMessage(const QString &message) { - auto app = getApp(); - if (!app->accounts->twitch.isLoggedIn()) + auto *app = getApp(); + if (!app->getAccounts()->twitch.isLoggedIn()) { if (!message.isEmpty()) { @@ -385,6 +617,7 @@ void TwitchChannel::sendMessage(const QString &message) bool messageSent = false; this->sendMessageSignal.invoke(this->getName(), parsedMessage, messageSent); + this->updateSevenTVActivity(); if (messageSent) { @@ -395,8 +628,8 @@ void TwitchChannel::sendMessage(const QString &message) void TwitchChannel::sendReply(const QString &message, const QString &replyId) { - auto app = getApp(); - if (!app->accounts->twitch.isLoggedIn()) + auto *app = getApp(); + if (!app->getAccounts()->twitch.isLoggedIn()) { if (!message.isEmpty()) { @@ -474,9 +707,10 @@ void TwitchChannel::setStaff(bool value) bool TwitchChannel::isBroadcaster() const { - auto app = getApp(); + auto *app = getApp(); - return this->getName() == app->accounts->twitch.getCurrent()->getUserName(); + return this->getName() == + app->getAccounts()->twitch.getCurrent()->getUserName(); } bool TwitchChannel::hasHighRateLimit() const @@ -491,7 +725,18 @@ bool TwitchChannel::canReconnect() const void TwitchChannel::reconnect() { - getApp()->twitch->connect(); + getApp()->getTwitch()->connect(); +} + +QString TwitchChannel::getCurrentStreamID() const +{ + auto streamStatus = this->accessStreamStatus(); + if (streamStatus->live) + { + return streamStatus->streamId; + } + + return {}; } QString TwitchChannel::roomId() const @@ -504,27 +749,38 @@ void TwitchChannel::setRoomId(const QString &id) if (*this->roomID_.accessConst() != id) { *this->roomID_.access() = id; - this->roomIdChanged.invoke(); - this->loadRecentMessages(); + // This is intended for tests and benchmarks. See comment in constructor. + if (!getApp()->isTest()) + { + this->roomIdChanged(); + this->loadRecentMessages(); + } + this->disconnected_ = false; + this->lastConnectedAt_ = std::chrono::system_clock::now(); } } SharedAccessGuard TwitchChannel::accessRoomModes() const { - return this->roomModes_.accessConst(); + return this->roomModes.accessConst(); } -void TwitchChannel::setRoomModes(const RoomModes &_roomModes) +void TwitchChannel::setRoomModes(const RoomModes &newRoomModes) { - this->roomModes_ = _roomModes; + this->roomModes = newRoomModes; this->roomModesChanged.invoke(); } bool TwitchChannel::isLive() const { - return this->streamStatus_.access()->live; + return this->streamStatus_.accessConst()->live; +} + +bool TwitchChannel::isRerun() const +{ + return this->streamStatus_.accessConst()->rerun; } SharedAccessGuard @@ -533,23 +789,39 @@ SharedAccessGuard return this->streamStatus_.accessConst(); } -boost::optional TwitchChannel::bttvEmote(const EmoteName &name) const +std::optional TwitchChannel::bttvEmote(const EmoteName &name) const { auto emotes = this->bttvEmotes_.get(); auto it = emotes->find(name); if (it == emotes->end()) - return boost::none; + { + return std::nullopt; + } return it->second; } -boost::optional TwitchChannel::ffzEmote(const EmoteName &name) const +std::optional TwitchChannel::ffzEmote(const EmoteName &name) const { auto emotes = this->ffzEmotes_.get(); auto it = emotes->find(name); if (it == emotes->end()) - return boost::none; + { + return std::nullopt; + } + return it->second; +} + +std::optional TwitchChannel::seventvEmote(const EmoteName &name) const +{ + auto emotes = this->seventvEmotes_.get(); + auto it = emotes->find(name); + + if (it == emotes->end()) + { + return std::nullopt; + } return it->second; } @@ -563,6 +835,288 @@ std::shared_ptr TwitchChannel::ffzEmotes() const return this->ffzEmotes_.get(); } +std::shared_ptr TwitchChannel::seventvEmotes() const +{ + return this->seventvEmotes_.get(); +} + +const QString &TwitchChannel::seventvUserID() const +{ + return this->seventvUserID_; +} +const QString &TwitchChannel::seventvEmoteSetID() const +{ + return this->seventvEmoteSetID_; +} + +void TwitchChannel::joinBttvChannel() const +{ + if (getApp()->getBttvLiveUpdates()) + { + const auto currentAccount = + getApp()->getAccounts()->twitch.getCurrent(); + QString userName; + if (currentAccount && !currentAccount->isAnon()) + { + userName = currentAccount->getUserName(); + } + getApp()->getBttvLiveUpdates()->joinChannel(this->roomId(), userName); + } +} + +void TwitchChannel::addBttvEmote( + const BttvLiveUpdateEmoteUpdateAddMessage &message) +{ + auto emote = BttvEmotes::addEmote(this->getDisplayName(), this->bttvEmotes_, + message); + + this->addOrReplaceLiveUpdatesAddRemove(true, "BTTV", QString() /*actor*/, + emote->name.string); +} + +void TwitchChannel::updateBttvEmote( + const BttvLiveUpdateEmoteUpdateAddMessage &message) +{ + auto updated = BttvEmotes::updateEmote(this->getDisplayName(), + this->bttvEmotes_, message); + if (!updated) + { + return; + } + + const auto [oldEmote, newEmote] = *updated; + if (oldEmote->name == newEmote->name) + { + return; // only the creator changed + } + + auto builder = MessageBuilder(liveUpdatesUpdateEmoteMessage, "BTTV", + QString() /* actor */, newEmote->name.string, + oldEmote->name.string); + this->addMessage(builder.release(), MessageContext::Original); +} + +void TwitchChannel::removeBttvEmote( + const BttvLiveUpdateEmoteRemoveMessage &message) +{ + auto removed = BttvEmotes::removeEmote(this->bttvEmotes_, message); + if (!removed) + { + return; + } + + this->addOrReplaceLiveUpdatesAddRemove(false, "BTTV", QString() /*actor*/, + (*removed)->name.string); +} + +void TwitchChannel::addSeventvEmote( + const seventv::eventapi::EmoteAddDispatch &dispatch) +{ + if (!SeventvEmotes::addEmote(this->seventvEmotes_, dispatch)) + { + return; + } + + this->addOrReplaceLiveUpdatesAddRemove( + true, "7TV", dispatch.actorName, dispatch.emoteJson["name"].toString()); +} + +void TwitchChannel::updateSeventvEmote( + const seventv::eventapi::EmoteUpdateDispatch &dispatch) +{ + if (!SeventvEmotes::updateEmote(this->seventvEmotes_, dispatch)) + { + return; + } + + auto builder = + MessageBuilder(liveUpdatesUpdateEmoteMessage, "7TV", dispatch.actorName, + dispatch.emoteName, dispatch.oldEmoteName); + this->addMessage(builder.release(), MessageContext::Original); +} + +void TwitchChannel::removeSeventvEmote( + const seventv::eventapi::EmoteRemoveDispatch &dispatch) +{ + auto removed = SeventvEmotes::removeEmote(this->seventvEmotes_, dispatch); + if (!removed) + { + return; + } + + this->addOrReplaceLiveUpdatesAddRemove(false, "7TV", dispatch.actorName, + (*removed)->name.string); +} + +void TwitchChannel::updateSeventvUser( + const seventv::eventapi::UserConnectionUpdateDispatch &dispatch) +{ + if (dispatch.connectionIndex != this->seventvUserTwitchConnectionIndex_) + { + // A different connection was updated + return; + } + + updateSeventvData(this->seventvUserID_, dispatch.emoteSetID); + SeventvEmotes::getEmoteSet( + dispatch.emoteSetID, + [this, weak = weakOf(this), dispatch](auto &&emotes, + const auto &name) { + postToThread([this, weak, dispatch, emotes, name]() { + if (auto shared = weak.lock()) + { + this->seventvEmotes_.set( + std::make_shared(emotes)); + auto builder = + MessageBuilder(liveUpdatesUpdateEmoteSetMessage, "7TV", + dispatch.actorName, name); + this->addMessage(builder.release(), + MessageContext::Original); + } + }); + }, + [this, weak = weakOf(this)](const auto &reason) { + postToThread([this, weak, reason]() { + if (auto shared = weak.lock()) + { + this->seventvEmotes_.set(EMPTY_EMOTE_MAP); + this->addSystemMessage( + QString("Failed updating 7TV emote set (%1).") + .arg(reason)); + } + }); + }); +} + +void TwitchChannel::updateSeventvData(const QString &newUserID, + const QString &newEmoteSetID) +{ + if (this->seventvUserID_ == newUserID && + this->seventvEmoteSetID_ == newEmoteSetID) + { + return; + } + + const auto oldUserID = makeConditionedOptional( + !this->seventvUserID_.isEmpty() && this->seventvUserID_ != newUserID, + this->seventvUserID_); + const auto oldEmoteSetID = + makeConditionedOptional(!this->seventvEmoteSetID_.isEmpty() && + this->seventvEmoteSetID_ != newEmoteSetID, + this->seventvEmoteSetID_); + + this->seventvUserID_ = newUserID; + this->seventvEmoteSetID_ = newEmoteSetID; + runInGuiThread([this, oldUserID, oldEmoteSetID]() { + if (getApp()->getSeventvEventAPI()) + { + getApp()->getSeventvEventAPI()->subscribeUser( + this->seventvUserID_, this->seventvEmoteSetID_); + + if (oldUserID || oldEmoteSetID) + { + getApp()->getTwitch()->dropSeventvChannel( + oldUserID.value_or(QString()), + oldEmoteSetID.value_or(QString())); + } + } + }); +} + +void TwitchChannel::addOrReplaceLiveUpdatesAddRemove(bool isEmoteAdd, + const QString &platform, + const QString &actor, + const QString &emoteName) +{ + if (this->tryReplaceLastLiveUpdateAddOrRemove( + isEmoteAdd ? MessageFlag::LiveUpdatesAdd + : MessageFlag::LiveUpdatesRemove, + platform, actor, emoteName)) + { + return; + } + + this->lastLiveUpdateEmoteNames_ = {emoteName}; + + MessagePtr msg; + if (isEmoteAdd) + { + msg = MessageBuilder(liveUpdatesAddEmoteMessage, platform, actor, + this->lastLiveUpdateEmoteNames_) + .release(); + } + else + { + msg = MessageBuilder(liveUpdatesRemoveEmoteMessage, platform, actor, + this->lastLiveUpdateEmoteNames_) + .release(); + } + this->lastLiveUpdateEmotePlatform_ = platform; + this->lastLiveUpdateMessage_ = msg; + this->lastLiveUpdateEmoteActor_ = actor; + this->addMessage(msg, MessageContext::Original); +} + +bool TwitchChannel::tryReplaceLastLiveUpdateAddOrRemove( + MessageFlag op, const QString &platform, const QString &actor, + const QString &emoteName) +{ + if (this->lastLiveUpdateEmotePlatform_ != platform) + { + return false; + } + auto last = this->lastLiveUpdateMessage_.lock(); + if (!last || !last->flags.has(op) || + last->parseTime < QTime::currentTime().addSecs(-5) || + last->loginName != actor) + { + return false; + } + // Update the message + this->lastLiveUpdateEmoteNames_.push_back(emoteName); + + auto makeReplacement = [&](MessageFlag op) -> MessageBuilder { + if (op == MessageFlag::LiveUpdatesAdd) + { + return { + liveUpdatesAddEmoteMessage, + platform, + last->loginName, + this->lastLiveUpdateEmoteNames_, + }; + } + + // op == RemoveEmoteMessage + return { + liveUpdatesRemoveEmoteMessage, + platform, + last->loginName, + this->lastLiveUpdateEmoteNames_, + }; + }; + + auto replacement = makeReplacement(op); + + replacement->flags = last->flags; + + auto msg = replacement.release(); + this->lastLiveUpdateMessage_ = msg; + this->replaceMessage(last, msg); + + return true; +} + +void TwitchChannel::messageRemovedFromStart(const MessagePtr &msg) +{ + if (msg->replyThread) + { + if (msg->replyThread->liveCount(msg) == 0) + { + this->threads_.erase(msg->replyThread->rootId()); + } + } +} + const QString &TwitchChannel::subscriptionUrl() { return this->subscriptionUrl_; @@ -578,191 +1132,45 @@ const QString &TwitchChannel::popoutPlayerUrl() return this->popoutPlayerUrl_; } -int TwitchChannel::chatterCount() +int TwitchChannel::chatterCount() const { return this->chatterCount_; } -void TwitchChannel::setLive(bool newLiveStatus) +bool TwitchChannel::setLive(bool newLiveStatus) { - bool gotNewLiveStatus = false; + auto guard = this->streamStatus_.access(); + if (guard->live == newLiveStatus) { - auto guard = this->streamStatus_.access(); - if (guard->live != newLiveStatus) - { - gotNewLiveStatus = true; - if (newLiveStatus) - { - if (getApp()->notifications->isChannelNotified( - this->getName(), Platform::Twitch)) - { - if (Toasts::isEnabled()) - { - getApp()->toasts->sendChannelNotification( - this->getName(), Platform::Twitch); - } - if (getSettings()->notificationPlaySound) - { - getApp()->notifications->playSound(); - } - if (getSettings()->notificationFlashTaskbar) - { - getApp()->windows->sendAlert(); - } - } - // Channel live message - MessageBuilder builder; - TwitchMessageBuilder::liveSystemMessage(this->getDisplayName(), - &builder); - this->addMessage(builder.release()); - - // Message in /live channel - MessageBuilder builder2; - TwitchMessageBuilder::liveMessage(this->getDisplayName(), - &builder2); - getApp()->twitch->liveChannel->addMessage(builder2.release()); - - // Notify on all channels with a ping sound - if (getSettings()->notificationOnAnyChannel && - !(isInStreamerMode() && - getSettings()->streamerModeSuppressLiveNotifications)) - { - getApp()->notifications->playSound(); - } - } - else - { - // Channel offline message - MessageBuilder builder; - TwitchMessageBuilder::offlineSystemMessage( - this->getDisplayName(), &builder); - this->addMessage(builder.release()); - - // "delete" old 'CHANNEL is live' message - LimitedQueueSnapshot snapshot = - getApp()->twitch->liveChannel->getMessageSnapshot(); - int snapshotLength = snapshot.size(); - - // MSVC hates this code if the parens are not there - int end = (std::max)(0, snapshotLength - 200); - auto liveMessageSearchText = - QString("%1 is live!").arg(this->getDisplayName()); - - for (int i = snapshotLength - 1; i >= end; --i) - { - auto &s = snapshot[i]; - - if (s->messageText == liveMessageSearchText) - { - s->flags.set(MessageFlag::Disabled); - break; - } - } - } - guard->live = newLiveStatus; - } + return false; + } + guard->live = newLiveStatus; + if (!newLiveStatus) + { + // A rerun is just a fancy livestream + guard->rerun = false; } - if (gotNewLiveStatus) + return true; +} + +void TwitchChannel::markConnected() +{ + if (this->lastConnectedAt_.has_value() && !this->disconnected_) { - this->liveStatusChanged.invoke(); + this->lastConnectedAt_ = std::chrono::system_clock::now(); } } -void TwitchChannel::refreshTitle() +void TwitchChannel::markDisconnected() { - // timer has never started, proceed and start it - if (!this->titleRefreshedTimer_.isValid()) + if (this->roomId().isEmpty()) { - this->titleRefreshedTimer_.start(); - } - else if (this->roomId().isEmpty() || - this->titleRefreshedTimer_.elapsed() < TITLE_REFRESH_PERIOD) - { - return; - } - this->titleRefreshedTimer_.restart(); - - getHelix()->getChannel( - this->roomId(), - [this, weak = weakOf(this)](HelixChannel channel) { - ChannelPtr shared = weak.lock(); - - if (!shared) - { - return; - } - - { - auto status = this->streamStatus_.access(); - status->title = channel.title; - } - - this->liveStatusChanged.invoke(); - }, - [] { - // failure - }); -} - -void TwitchChannel::refreshLiveStatus() -{ - auto roomID = this->roomId(); - - if (roomID.isEmpty()) - { - qCDebug(chatterinoTwitch) << "[TwitchChannel" << this->getName() - << "] Refreshing live status (Missing ID)"; - this->setLive(false); + // we were never joined in the first place return; } - getHelix()->getStreamById( - roomID, - [this, weak = weakOf(this)](bool live, const auto &stream) { - ChannelPtr shared = weak.lock(); - if (!shared) - { - return; - } - - this->parseLiveStatus(live, stream); - }, - [] { - // failure - }, - [] { - // finally - }); -} - -void TwitchChannel::parseLiveStatus(bool live, const HelixStream &stream) -{ - if (!live) - { - this->setLive(false); - return; - } - - { - auto status = this->streamStatus_.access(); - status->viewerCount = stream.viewerCount; - status->gameId = stream.gameId; - status->game = stream.gameName; - status->title = stream.title; - QDateTime since = QDateTime::fromString(stream.startedAt, Qt::ISODate); - auto diff = since.secsTo(QDateTime::currentDateTime()); - status->uptime = QString::number(diff / 3600) + "h " + - QString::number(diff % 3600 / 60) + "m"; - - status->rerun = false; - status->streamType = stream.type; - } - - this->setLive(true); - - // Signal all listeners that the stream status has been updated - this->liveStatusChanged.invoke(); + this->disconnected_ = true; } void TwitchChannel::loadRecentMessages() @@ -778,31 +1186,59 @@ void TwitchChannel::loadRecentMessages() } auto weak = weakOf(this); - RecentMessagesApi::loadRecentMessages( + recentmessages::load( this->getName(), weak, [weak](const auto &messages) { auto shared = weak.lock(); if (!shared) + { return; + } - auto tc = dynamic_cast(shared.get()); + auto *tc = dynamic_cast(shared.get()); if (!tc) + { return; + } tc->addMessagesAtStart(messages); tc->loadingRecentMessages_.clear(); + + std::vector msgs; + for (const auto &msg : messages) + { + const auto highlighted = + msg->flags.has(MessageFlag::Highlighted); + const auto showInMentions = + msg->flags.has(MessageFlag::ShowInMentions); + if (highlighted && showInMentions) + { + msgs.push_back(msg); + } + + tc->addRecentChatter(msg->displayName); + } + + getApp()->getTwitch()->getMentionsChannel()->fillInMissingMessages( + msgs); }, [weak]() { auto shared = weak.lock(); if (!shared) + { return; + } - auto tc = dynamic_cast(shared.get()); + auto *tc = dynamic_cast(shared.get()); if (!tc) + { return; + } tc->loadingRecentMessages_.clear(); - }); + }, + getSettings()->twitchMessageHistoryLimit.getValue(), std::nullopt, + std::nullopt, false); } void TwitchChannel::loadRecentMessagesReconnect() @@ -817,17 +1253,36 @@ void TwitchChannel::loadRecentMessagesReconnect() return; // already loading } + const auto now = std::chrono::system_clock::now(); + int limit = getSettings()->twitchMessageHistoryLimit.getValue(); + if (this->lastConnectedAt_.has_value()) + { + // calculate how many messages could have occured + // while we were not connected to the channel + // assuming a maximum of 10 messages per second + const auto secondsSinceDisconnect = + std::chrono::duration_cast( + now - this->lastConnectedAt_.value()) + .count(); + limit = + std::min(static_cast(secondsSinceDisconnect + 1) * 10, limit); + } + auto weak = weakOf(this); - RecentMessagesApi::loadRecentMessages( + recentmessages::load( this->getName(), weak, [weak](const auto &messages) { auto shared = weak.lock(); if (!shared) + { return; + } - auto tc = dynamic_cast(shared.get()); + auto *tc = dynamic_cast(shared.get()); if (!tc) + { return; + } tc->fillInMissingMessages(messages); tc->loadingRecentMessages_.clear(); @@ -835,35 +1290,53 @@ void TwitchChannel::loadRecentMessagesReconnect() [weak]() { auto shared = weak.lock(); if (!shared) + { return; + } - auto tc = dynamic_cast(shared.get()); + auto *tc = dynamic_cast(shared.get()); if (!tc) + { return; + } tc->loadingRecentMessages_.clear(); - }); + }, + limit, this->lastConnectedAt_, now, true); } void TwitchChannel::refreshPubSub() { + if (getApp()->isTest()) + { + return; + } + auto roomId = this->roomId(); if (roomId.isEmpty()) { return; } - auto currentAccount = getApp()->accounts->twitch.getCurrent(); + auto currentAccount = getApp()->getAccounts()->twitch.getCurrent(); - getApp()->twitch->pubsub->setAccount(currentAccount); - - getApp()->twitch->pubsub->listenToChannelModerationActions(roomId); - getApp()->twitch->pubsub->listenToAutomod(roomId); - getApp()->twitch->pubsub->listenToChannelPointRewards(roomId); + getApp()->getTwitchPubSub()->listenToChannelModerationActions(roomId); + if (this->hasModRights()) + { + getApp()->getTwitchPubSub()->listenToAutomod(roomId); + getApp()->getTwitchPubSub()->listenToLowTrustUsers(roomId); + } + getApp()->getTwitchPubSub()->listenToChannelPointRewards(roomId); } void TwitchChannel::refreshChatters() { + // helix endpoint only works for mods + if (!this->hasModRights()) + { + return; + } + // setting? const auto streamStatus = this->accessStreamStatus(); const auto viewerCount = static_cast(streamStatus->viewerCount); @@ -876,58 +1349,23 @@ void TwitchChannel::refreshChatters() } } - // get viewer list - NetworkRequest("https://tmi.twitch.tv/group/user/" + this->getName() + - "/chatters") - - .onSuccess( - [this, weak = weakOf(this)](auto result) -> Outcome { - // channel still exists? - auto shared = weak.lock(); - if (!shared) - { - return Failure; - } - - auto data = result.parseJson(); - this->chatterCount_ = data.value("chatter_count").toInt(); - - auto pair = parseChatters(std::move(data)); - if (pair.first) - { - this->updateOnlineChatters(pair.second); - } - - return pair.first; - }) - .execute(); -} - -void TwitchChannel::fetchDisplayName() -{ - getHelix()->getUserByName( - this->getName(), - [weak = weakOf(this)](const auto &user) { - auto shared = weak.lock(); - if (!shared) - return; - auto channel = static_cast(shared.get()); - if (QString::compare(user.displayName, channel->getName(), - Qt::CaseInsensitive) == 0) + // Get chatter list via helix api + getHelix()->getChatters( + this->roomId(), + getApp()->getAccounts()->twitch.getCurrent()->getUserId(), + MAX_CHATTERS_TO_FETCH, + [this, weak = weakOf(this)](auto result) { + if (auto shared = weak.lock()) { - channel->setDisplayName(user.displayName); - channel->setLocalizedName(user.displayName); + this->updateOnlineChatters(result.chatters); + this->chatterCount_ = result.total; } - else - { - channel->setLocalizedName(QString("%1(%2)") - .arg(channel->getName()) - .arg(user.displayName)); - } - channel->addRecentChatter(channel->getDisplayName()); - channel->displayNameChanged.invoke(); }, - [] {}); + // Refresh chatters should only be used when failing silently is an option + [](auto error, auto message) { + (void)error; + (void)message; + }); } void TwitchChannel::addReplyThread(const std::shared_ptr &thread) @@ -935,12 +1373,28 @@ void TwitchChannel::addReplyThread(const std::shared_ptr &thread) this->threads_[thread->rootId()] = thread; } -const std::unordered_map> - &TwitchChannel::threads() const +const std::unordered_map> & + TwitchChannel::threads() const { return this->threads_; } +std::shared_ptr TwitchChannel::getOrCreateThread( + const MessagePtr &message) +{ + assert(message != nullptr); + + auto threadIt = this->threads_.find(message->id); + if (threadIt != this->threads_.end() && !threadIt->second.expired()) + { + return threadIt->second.lock(); + } + + auto thread = std::make_shared(message); + this->addReplyThread(thread); + return thread; +} + void TwitchChannel::cleanUpReplyThreads() { for (auto it = this->threads_.begin(), last = this->threads_.end(); @@ -965,50 +1419,75 @@ void TwitchChannel::cleanUpReplyThreads() void TwitchChannel::refreshBadges() { - auto url = Url{"https://badges.twitch.tv/v1/badges/channels/" + - this->roomId() + "/display?language=en"}; - NetworkRequest(url.string) + if (this->roomId().isEmpty()) + { + return; + } - .onSuccess([this, - weak = weakOf(this)](auto result) -> Outcome { + getHelix()->getChannelBadges( + this->roomId(), + // successCallback + [this, weak = weakOf(this)](auto channelBadges) { auto shared = weak.lock(); if (!shared) - return Failure; + { + // The channel has been closed inbetween us making the request and the request finishing + return; + } auto badgeSets = this->badgeSets_.access(); - auto jsonRoot = result.parseJson(); - - auto _ = jsonRoot["badge_sets"].toObject(); - for (auto jsonBadgeSet = _.begin(); jsonBadgeSet != _.end(); - jsonBadgeSet++) + for (const auto &badgeSet : channelBadges.badgeSets) { - auto &versions = (*badgeSets)[jsonBadgeSet.key()]; - - auto _set = jsonBadgeSet->toObject()["versions"].toObject(); - for (auto jsonVersion_ = _set.begin(); - jsonVersion_ != _set.end(); jsonVersion_++) + const auto &setID = badgeSet.setID; + for (const auto &version : badgeSet.versions) { - auto jsonVersion = jsonVersion_->toObject(); - auto emote = std::make_shared(Emote{ - EmoteName{}, - ImageSet{ - Image::fromUrl( - {jsonVersion["image_url_1x"].toString()}, 1), - Image::fromUrl( - {jsonVersion["image_url_2x"].toString()}, .5), - Image::fromUrl( - {jsonVersion["image_url_4x"].toString()}, .25)}, - Tooltip{jsonVersion["description"].toString()}, - Url{jsonVersion["clickURL"].toString()}}); - - versions.emplace(jsonVersion_.key(), emote); - }; + auto emote = Emote{ + .name = EmoteName{}, + .images = + ImageSet{ + Image::fromUrl(version.imageURL1x, 1, + BASE_BADGE_SIZE), + Image::fromUrl(version.imageURL2x, .5, + BASE_BADGE_SIZE * 2), + Image::fromUrl(version.imageURL4x, .25, + BASE_BADGE_SIZE * 4), + }, + .tooltip = Tooltip{version.title}, + .homePage = version.clickURL, + }; + (*badgeSets)[setID][version.id] = + std::make_shared(emote); + } + } + }, + // failureCallback + [this, weak = weakOf(this)](auto error, auto message) { + auto shared = weak.lock(); + if (!shared) + { + // The channel has been closed inbetween us making the request and the request finishing + return; } - return Success; - }) - .execute(); + QString errorMessage("Failed to load channel badges - "); + + switch (error) + { + case HelixGetChannelBadgesError::Forwarded: { + errorMessage += message; + } + break; + + // This would most likely happen if the service is down, or if the JSON payload returned has changed format + case HelixGetChannelBadgesError::Unknown: { + errorMessage += "An unknown error has occurred."; + } + break; + } + + this->addSystemMessage(errorMessage); + }); } void TwitchChannel::refreshCheerEmotes() @@ -1016,11 +1495,11 @@ void TwitchChannel::refreshCheerEmotes() getHelix()->getCheermotes( this->roomId(), [this, weak = weakOf(this)]( - const std::vector &cheermoteSets) -> Outcome { + const std::vector &cheermoteSets) { auto shared = weak.lock(); if (!shared) { - return Failure; + return; } std::vector emoteSets; @@ -1047,22 +1526,28 @@ void TwitchChannel::refreshCheerEmotes() // Combine the prefix (e.g. BibleThump) with the tier (1, 100 etc.) auto emoteTooltip = set.prefix + tier.id + "
Twitch Cheer Emote"; - cheerEmote.animatedEmote = std::make_shared( - Emote{EmoteName{"cheer emote"}, - ImageSet{ - tier.darkAnimated.imageURL1x, - tier.darkAnimated.imageURL2x, - tier.darkAnimated.imageURL4x, - }, - Tooltip{emoteTooltip}, Url{}}); - cheerEmote.staticEmote = std::make_shared( - Emote{EmoteName{"cheer emote"}, - ImageSet{ - tier.darkStatic.imageURL1x, - tier.darkStatic.imageURL2x, - tier.darkStatic.imageURL4x, - }, - Tooltip{emoteTooltip}, Url{}}); + auto makeImageSet = [](const HelixCheermoteImage &image) { + return ImageSet{ + Image::fromUrl(image.imageURL1x, 1.0, + BASE_BADGE_SIZE), + Image::fromUrl(image.imageURL2x, 0.5, + BASE_BADGE_SIZE * 2), + Image::fromUrl(image.imageURL4x, 0.25, + BASE_BADGE_SIZE * 4), + }; + }; + cheerEmote.animatedEmote = std::make_shared(Emote{ + .name = EmoteName{"cheer emote"}, + .images = makeImageSet(tier.darkAnimated), + .tooltip = Tooltip{emoteTooltip}, + .homePage = Url{}, + }); + cheerEmote.staticEmote = std::make_shared(Emote{ + .name = EmoteName{"cheer emote"}, + .images = makeImageSet(tier.darkStatic), + .tooltip = Tooltip{emoteTooltip}, + .homePage = Url{}, + }); cheerEmoteSet.cheerEmotes.emplace_back( std::move(cheerEmote)); @@ -1079,12 +1564,9 @@ void TwitchChannel::refreshCheerEmotes() } *this->cheerEmoteSets_.access() = std::move(emoteSets); - - return Success; }, [] { // Failure - return Failure; }); } @@ -1092,8 +1574,8 @@ void TwitchChannel::createClip() { if (!this->isLive()) { - this->addMessage(makeSystemMessage( - "Cannot create clip while the channel is offline!")); + this->addSystemMessage( + "Cannot create clip while the channel is offline!"); return; } @@ -1108,7 +1590,7 @@ void TwitchChannel::createClip() return; } - this->addMessage(makeSystemMessage("Creating clip...")); + this->addSystemMessage("Creating clip..."); this->isClipCreationInProgress = true; getHelix()->createClip( @@ -1143,7 +1625,7 @@ void TwitchChannel::createClip() MessageColor::Link) ->setLink(Link(Link::Url, clip.editUrl)); - this->addMessage(builder.release()); + this->addMessage(builder.release(), MessageContext::Original); }, // failureCallback [this](auto error) { @@ -1192,7 +1674,7 @@ void TwitchChannel::createClip() builder.message().messageText = text; builder.message().searchText = text; - this->addMessage(builder.release()); + this->addMessage(builder.release(), MessageContext::Original); }, // finallyCallback - this will always execute, so clip creation won't ever be stuck [this] { @@ -1201,8 +1683,8 @@ void TwitchChannel::createClip() }); } -boost::optional TwitchChannel::twitchBadge( - const QString &set, const QString &version) const +std::optional TwitchChannel::twitchBadge(const QString &set, + const QString &version) const { auto badgeSets = this->badgeSets_.access(); auto it = badgeSets->find(set); @@ -1214,20 +1696,47 @@ boost::optional TwitchChannel::twitchBadge( return it2->second; } } - return boost::none; + return std::nullopt; } -boost::optional TwitchChannel::ffzCustomModBadge() const +std::vector TwitchChannel::ffzChannelBadges( + const QString &userID) const +{ + this->tgFfzChannelBadges_.guard(); + + auto it = this->ffzChannelBadges_.find(userID); + if (it == this->ffzChannelBadges_.end()) + { + return {}; + } + + std::vector badges; + + const auto *ffzBadges = getApp()->getFfzBadges(); + + for (const auto &badgeID : it->second) + { + auto badge = ffzBadges->getBadge(badgeID); + if (badge.has_value()) + { + badges.emplace_back(*badge); + } + } + + return badges; +} + +std::optional TwitchChannel::ffzCustomModBadge() const { return this->ffzCustomModBadge_.get(); } -boost::optional TwitchChannel::ffzCustomVipBadge() const +std::optional TwitchChannel::ffzCustomVipBadge() const { return this->ffzCustomVipBadge_.get(); } -boost::optional TwitchChannel::cheerEmote(const QString &string) +std::optional TwitchChannel::cheerEmote(const QString &string) { auto sets = this->cheerEmoteSets_.access(); for (const auto &set : *sets) @@ -1253,7 +1762,61 @@ boost::optional TwitchChannel::cheerEmote(const QString &string) } } } - return boost::none; + return std::nullopt; +} + +void TwitchChannel::updateSevenTVActivity() +{ + static const QString seventvActivityUrl = + QStringLiteral("https://7tv.io/v3/users/%1/presences"); + + const auto currentSeventvUserID = + getApp()->getAccounts()->twitch.getCurrent()->getSeventvUserID(); + if (currentSeventvUserID.isEmpty()) + { + return; + } + + if (!getSettings()->enableSevenTVEventAPI || + !getSettings()->sendSevenTVActivity) + { + return; + } + + if (this->nextSeventvActivity_.isValid() && + QDateTime::currentDateTimeUtc() < this->nextSeventvActivity_) + { + return; + } + // Make sure to not send activity again before receiving the response + this->nextSeventvActivity_ = this->nextSeventvActivity_.addSecs(300); + + qCDebug(chatterinoSeventv) << "Sending activity in" << this->getName(); + + getApp()->getSeventvAPI()->updatePresence( + this->roomId(), currentSeventvUserID, + [chan = weakOf(this)]() { + const auto self = + std::dynamic_pointer_cast(chan.lock()); + if (!self) + { + return; + } + self->nextSeventvActivity_ = + QDateTime::currentDateTimeUtc().addSecs(60); + }, + [](const auto &result) { + qCDebug(chatterinoSeventv) + << "Failed to update 7TV activity:" << result.formatError(); + }); +} + +void TwitchChannel::listenSevenTVCosmetics() const +{ + if (getApp()->getSeventvEventAPI()) + { + getApp()->getSeventvEventAPI()->subscribeTwitchChannel(this->roomId()); + } } } // namespace chatterino diff --git a/src/providers/twitch/TwitchChannel.hpp b/src/providers/twitch/TwitchChannel.hpp index eb475e3e1..d031ee5b6 100644 --- a/src/providers/twitch/TwitchChannel.hpp +++ b/src/providers/twitch/TwitchChannel.hpp @@ -1,34 +1,36 @@ #pragma once -#include "Application.hpp" #include "common/Aliases.hpp" #include "common/Atomic.hpp" #include "common/Channel.hpp" #include "common/ChannelChatters.hpp" -#include "common/ChatterSet.hpp" -#include "common/Outcome.hpp" +#include "common/Common.hpp" #include "common/UniqueAccess.hpp" -#include "messages/MessageThread.hpp" -#include "providers/twitch/ChannelPointReward.hpp" +#include "providers/ffz/FfzBadges.hpp" +#include "providers/ffz/FfzEmotes.hpp" #include "providers/twitch/TwitchEmotes.hpp" -#include "providers/twitch/api/Helix.hpp" #include "util/QStringHash.hpp" +#include "util/ThreadGuard.hpp" +#include +#include +#include +#include #include #include #include -#include -#include -#include #include #include +#include #include namespace chatterino { -// This is to make sure that combined emoji go through properly, see -// https://github.com/Chatterino/chatterino2/issues/3384 and +// This is for compatibility with older Chatterino versions. Twitch didn't use +// to allow ZERO WIDTH JOINER unicode character, so Chatterino used ESCAPE_TAG +// instead. +// See https://github.com/Chatterino/chatterino2/issues/3384 and // https://mm2pl.github.io/emoji_rfc.pdf for more details const QString ZERO_WIDTH_JOINER = QString(QChar(0x200D)); @@ -52,10 +54,27 @@ class EmoteMap; class TwitchBadges; class FfzEmotes; class BttvEmotes; +struct BttvLiveUpdateEmoteUpdateAddMessage; +struct BttvLiveUpdateEmoteRemoveMessage; + +class SeventvEmotes; +namespace seventv::eventapi { + struct EmoteAddDispatch; + struct EmoteUpdateDispatch; + struct EmoteRemoveDispatch; + struct UserConnectionUpdateDispatch; +} // namespace seventv::eventapi + +struct ChannelPointReward; +class MessageThread; +struct CheerEmoteSet; +struct HelixStream; class TwitchIrcServer; -class TwitchChannel : public Channel, public ChannelChatters +const int MAX_QUEUED_REDEMPTIONS = 16; + +class TwitchChannel final : public Channel, public ChannelChatters { public: struct StreamStatus { @@ -66,63 +85,133 @@ public: QString game; QString gameId; QString uptime; + int uptimeSeconds = 0; QString streamType; + QString streamId; }; struct RoomModes { bool submode = false; bool r9k = false; bool emoteOnly = false; + + /** + * @brief Number of minutes required for users to be followed before typing in chat + * + * Special cases: + * -1 = follower mode off + * 0 = follower mode on, no time requirement + **/ int followerOnly = -1; + + /** + * @brief Number of seconds required to wait before typing emotes + * + * 0 = slow mode off + **/ int slowMode = 0; }; explicit TwitchChannel(const QString &channelName); + ~TwitchChannel() override; + + TwitchChannel(const TwitchChannel &) = delete; + TwitchChannel(TwitchChannel &&) = delete; + TwitchChannel &operator=(const TwitchChannel &) = delete; + TwitchChannel &operator=(TwitchChannel &&) = delete; void initialize(); // Channel methods - virtual bool isEmpty() const override; - virtual bool canSendMessage() const override; - virtual void sendMessage(const QString &message) override; - virtual void sendReply(const QString &message, const QString &replyId); - virtual bool isMod() const override; + bool isEmpty() const override; + bool canSendMessage() const override; + void sendMessage(const QString &message) override; + void sendReply(const QString &message, const QString &replyId); + bool isMod() const override; bool isVip() const; bool isStaff() const; - virtual bool isBroadcaster() const override; - virtual bool hasHighRateLimit() const override; - virtual bool canReconnect() const override; - virtual void reconnect() override; - void refreshTitle(); + bool isBroadcaster() const override; + bool hasHighRateLimit() const override; + bool canReconnect() const override; + void reconnect() override; + QString getCurrentStreamID() const override; void createClip(); // Data const QString &subscriptionUrl(); const QString &channelUrl(); const QString &popoutPlayerUrl(); - int chatterCount(); - virtual bool isLive() const override; + int chatterCount() const; + bool isLive() const override; + bool isRerun() const override; QString roomId() const; SharedAccessGuard accessRoomModes() const; SharedAccessGuard accessStreamStatus() const; + /** + * Records that the channel is no longer joined. + */ + void markDisconnected(); + + /** + * Records that the channel's read connection is healthy. + */ + void markConnected(); + // Emotes - boost::optional bttvEmote(const EmoteName &name) const; - boost::optional ffzEmote(const EmoteName &name) const; + std::optional bttvEmote(const EmoteName &name) const; + std::optional ffzEmote(const EmoteName &name) const; + std::optional seventvEmote(const EmoteName &name) const; std::shared_ptr bttvEmotes() const; std::shared_ptr ffzEmotes() const; + std::shared_ptr seventvEmotes() const; - virtual void refreshBTTVChannelEmotes(bool manualRefresh); - virtual void refreshFFZChannelEmotes(bool manualRefresh); + void refreshBTTVChannelEmotes(bool manualRefresh); + void refreshFFZChannelEmotes(bool manualRefresh); + void refreshSevenTVChannelEmotes(bool manualRefresh); + + void setBttvEmotes(std::shared_ptr &&map); + void setFfzEmotes(std::shared_ptr &&map); + void setSeventvEmotes(std::shared_ptr &&map); + + const QString &seventvUserID() const; + const QString &seventvEmoteSetID() const; + + /** Adds a BTTV channel emote to this channel. */ + void addBttvEmote(const BttvLiveUpdateEmoteUpdateAddMessage &message); + /** Updates a BTTV channel emote in this channel. */ + void updateBttvEmote(const BttvLiveUpdateEmoteUpdateAddMessage &message); + /** Removes a BTTV channel emote from this channel. */ + void removeBttvEmote(const BttvLiveUpdateEmoteRemoveMessage &message); + + /** Adds a 7TV channel emote to this channel. */ + void addSeventvEmote(const seventv::eventapi::EmoteAddDispatch &dispatch); + /** Updates a 7TV channel emote's name in this channel */ + void updateSeventvEmote( + const seventv::eventapi::EmoteUpdateDispatch &dispatch); + /** Removes a 7TV channel emote from this channel */ + void removeSeventvEmote( + const seventv::eventapi::EmoteRemoveDispatch &dispatch); + /** Updates the current 7TV user. Currently, only the emote-set is updated. */ + void updateSeventvUser( + const seventv::eventapi::UserConnectionUpdateDispatch &dispatch); + + // Update the channel's 7TV information (the channel's 7TV user ID and emote set ID) + void updateSeventvData(const QString &newUserID, + const QString &newEmoteSetID); // Badges - boost::optional ffzCustomModBadge() const; - boost::optional ffzCustomVipBadge() const; - boost::optional twitchBadge(const QString &set, - const QString &version) const; + std::optional ffzCustomModBadge() const; + std::optional ffzCustomVipBadge() const; + std::optional twitchBadge(const QString &set, + const QString &version) const; + /** + * Returns a list of channel-specific FrankerFaceZ badges for the given user + */ + std::vector ffzChannelBadges(const QString &userID) const; // Cheers - boost::optional cheerEmote(const QString &string); + std::optional cheerEmote(const QString &string); // Replies /** @@ -135,69 +224,181 @@ public: const std::unordered_map> &threads() const; - // Signals - pajlada::Signals::NoArgSignal roomIdChanged; + /** + * Get the thread for the given message + * If no thread can be found for the message, create one + */ + std::shared_ptr getOrCreateThread(const MessagePtr &message); + + /** + * This signal fires when the local user has joined the channel + **/ + pajlada::Signals::NoArgSignal joined; + + // Only TwitchChannel may invoke this signal pajlada::Signals::NoArgSignal userStateChanged; - pajlada::Signals::NoArgSignal liveStatusChanged; + + /** + * This signal fires whenever the stream status is changed + * + * This includes when the stream goes from offline to online, + * or the viewer count changes, or the title has been updated + **/ + pajlada::Signals::NoArgSignal streamStatusChanged; + pajlada::Signals::NoArgSignal roomModesChanged; // Channel point rewards - pajlada::Signals::SelfDisconnectingSignal - channelPointRewardAdded; + void addQueuedRedemption(const QString &rewardId, + const QString &originalContent, + Communi::IrcMessage *message); + /** + * A rich & hydrated redemption from PubSub has arrived, add it to the channel. + * This will look at queued up partial messages, and if one is found it will add the queued up partial messages fully hydrated. + **/ void addChannelPointReward(const ChannelPointReward &reward); bool isChannelPointRewardKnown(const QString &rewardId); - boost::optional channelPointReward( + std::optional channelPointReward( const QString &rewardId) const; + // Live status + void updateStreamStatus(const std::optional &helixStream, + bool isInitialUpdate); + void updateStreamTitle(const QString &title); + + /** + * Returns the display name of the user + * + * If the display name contained chinese, japenese, or korean characters, the user's login name is returned instead + **/ + const QString &getDisplayName() const override; + void updateDisplayName(const QString &displayName); + private: struct NameOptions { + // displayName is the non-CJK-display name for this user + // This will always be the same as their `name_`, but potentially with different casing QString displayName; + + // localizedName is their display name that *may* contain CJK characters + // If the display name does not contain any CJK characters, this will be + // the same as `displayName` QString localizedName; + + // actualDisplayName is the raw display name string received from Twitch + QString actualDisplayName; } nameOptions; -private: - // Methods - void refreshLiveStatus(); - void parseLiveStatus(bool live, const HelixStream &stream); + struct QueuedRedemption { + QString rewardID; + QString originalContent; + QObjectPtr message; + }; + void refreshPubSub(); void refreshChatters(); void refreshBadges(); void refreshCheerEmotes(); void loadRecentMessages(); void loadRecentMessagesReconnect(); - void fetchDisplayName(); void cleanUpReplyThreads(); void showLoginMessage(); - void setLive(bool newLiveStatus); + /// roomIdChanged is called whenever this channel's ID has been changed + /// This should only happen once per channel, whenever the ID goes from unset to set + void roomIdChanged(); + + /** Joins (subscribes to) a Twitch channel for updates on BTTV. */ + void joinBttvChannel() const; + /** + * Indicates an activity to 7TV in this channel for this user. + * This is done at most once every 60s. + */ + void updateSevenTVActivity(); + void listenSevenTVCosmetics() const; + + /** + * @brief Sets the live status of this Twitch channel + * + * Returns true if the live status changed with this call + **/ + bool setLive(bool newLiveStatus); void setMod(bool value); void setVIP(bool value); void setStaff(bool value); void setRoomId(const QString &id); - void setRoomModes(const RoomModes &roomModes_); + void setRoomModes(const RoomModes &newRoomModes); void setDisplayName(const QString &name); void setLocalizedName(const QString &name); - const QString &getDisplayName() const override; + void onLiveStatusChanged(bool isLive, bool isInitialUpdate); + + /** + * Returns the localized name of the user + **/ const QString &getLocalizedName() const override; QString prepareMessage(const QString &message) const; + /** + * Either adds a message mentioning the updated emotes + * or replaces an existing message. For criteria on existing messages, + * see `tryReplaceLastLiveUpdateAddOrRemove`. + * + * @param isEmoteAdd true if the emote was added, false if it was removed. + * @param platform The platform the emote was updated on ("7TV", "BTTV", "FFZ") + * @param actor The actor performing the update (possibly empty) + * @param emoteName The emote's name + */ + void addOrReplaceLiveUpdatesAddRemove(bool isEmoteAdd, + const QString &platform, + const QString &actor, + const QString &emoteName); + + /** + * Tries to replace the last emote update message. + * + * A last message is valid if: + * * The actors match + * * The operations match + * * The platform matches + * * The last message isn't older than 5s + * + * @param op The emote operation (LiveUpdatesAdd or LiveUpdatesRemove) + * @param platform The emote platform ("7TV", "BTTV", "FFZ") + * @param actor The actor performing the action (possibly empty) + * @param emoteName The updated emote's name + * @return true, if the last message was replaced + */ + bool tryReplaceLastLiveUpdateAddOrRemove(MessageFlag op, + const QString &platform, + const QString &actor, + const QString &emoteName); + // Data const QString subscriptionUrl_; const QString channelUrl_; const QString popoutPlayerUrl_; - int chatterCount_; + int chatterCount_{}; UniqueAccess streamStatus_; - UniqueAccess roomModes_; + UniqueAccess roomModes; + bool disconnected_{}; + std::optional> + lastConnectedAt_{}; std::atomic_flag loadingRecentMessages_ = ATOMIC_FLAG_INIT; std::unordered_map> threads_; protected: + void messageRemovedFromStart(const MessagePtr &msg) override; + Atomic> bttvEmotes_; Atomic> ffzEmotes_; - Atomic> ffzCustomModBadge_; - Atomic> ffzCustomVipBadge_; + Atomic> seventvEmotes_; + Atomic> ffzCustomModBadge_; + Atomic> ffzCustomVipBadge_; + + FfzChannelBadgeMap ffzChannelBadges_; + ThreadGuard tgFfzChannelBadges_; private: // Badges @@ -205,6 +406,8 @@ private: badgeSets_; // "subscribers": { "0": ... "3": ... "6": ... UniqueAccess> cheerEmoteSets_; UniqueAccess> channelPointRewards_; + boost::circular_buffer_space_optimized + waitingRedemptions_{MAX_QUEUED_REDEMPTIONS}; bool mod_ = false; bool vip_ = false; @@ -220,12 +423,44 @@ private: QElapsedTimer clipCreationTimer_; bool isClipCreationInProgress{false}; + /** + * This channels 7TV user-id, + * empty if this channel isn't connected with 7TV. + */ + QString seventvUserID_; + /** + * This channels current 7TV emote-set-id, + * empty if this channel isn't connected with 7TV + */ + QString seventvEmoteSetID_; + /** + * The index of the twitch connection in + * 7TV's user representation. + */ + size_t seventvUserTwitchConnectionIndex_{}; + + /** + * The next moment in time to signal activity in this channel to 7TV. + * Or: Up until this moment we don't need to send activity. + */ + QDateTime nextSeventvActivity_; + + /** The platform of the last live emote update ("7TV", "BTTV", "FFZ"). */ + QString lastLiveUpdateEmotePlatform_; + /** The actor name of the last live emote update. */ + QString lastLiveUpdateEmoteActor_; + /** A weak reference to the last live emote update message. */ + std::weak_ptr lastLiveUpdateMessage_; + /** A list of the emotes listed in the lat live emote update message. */ + std::vector lastLiveUpdateEmoteNames_; + pajlada::Signals::SignalHolder signalHolder_; std::vector bSignals_; friend class TwitchIrcServer; - friend class TwitchMessageBuilder; + friend class MessageBuilder; friend class IrcMessageHandler; + friend class Commands_E2E_Test; }; } // namespace chatterino diff --git a/src/providers/twitch/TwitchCommon.hpp b/src/providers/twitch/TwitchCommon.hpp index 3882ee588..a5c388245 100644 --- a/src/providers/twitch/TwitchCommon.hpp +++ b/src/providers/twitch/TwitchCommon.hpp @@ -3,6 +3,8 @@ #include #include +#include + namespace chatterino { #ifndef ATTR_UNUSED @@ -15,6 +17,8 @@ namespace chatterino { static const char *ANONYMOUS_USERNAME ATTR_UNUSED = "justinfan64537"; +static constexpr int TWITCH_MESSAGE_LIMIT = 500; + inline QByteArray getDefaultClientID() { return QByteArray("7ue61iz46fz11y3cugd0l3tawb4taal"); @@ -73,6 +77,7 @@ static const QStringList TWITCH_DEFAULT_COMMANDS{ "delete", "announce", "requests", + "warn", }; static const QStringList TWITCH_WHISPER_COMMANDS{"/w", ".w"}; diff --git a/src/providers/twitch/TwitchEmotes.cpp b/src/providers/twitch/TwitchEmotes.cpp index 9a84bcabf..b7ba9f54a 100644 --- a/src/providers/twitch/TwitchEmotes.cpp +++ b/src/providers/twitch/TwitchEmotes.cpp @@ -1,17 +1,404 @@ #include "providers/twitch/TwitchEmotes.hpp" -#include "common/NetworkRequest.hpp" -#include "debug/Benchmark.hpp" +#include "common/QLogging.hpp" #include "messages/Emote.hpp" #include "messages/Image.hpp" -#include "util/RapidjsonHelpers.hpp" +#include "util/QStringHash.hpp" + +namespace { + +using namespace chatterino; + +Url getEmoteLink(const EmoteId &id, const QString &emoteScale) +{ + return {TWITCH_EMOTE_TEMPLATE.arg(id.string, emoteScale)}; +} + +QSize getEmoteExpectedBaseSize(const EmoteId &id) +{ + // From Twitch docs - expected size for an emote (1x) + constexpr QSize defaultBaseSize(28, 28); + static std::unordered_map outliers{ + {"555555635", {21, 18}}, /* ;p */ + {"555555636", {21, 18}}, /* ;-p */ + {"555555614", {21, 18}}, /* O_o */ + {"555555641", {21, 18}}, /* :z */ + {"555555604", {21, 18}}, /* :\\ */ + {"444", {21, 18}}, /* :| */ + {"555555634", {21, 18}}, /* ;-P */ + {"439", {21, 18}}, /* ;) */ + {"555555642", {21, 18}}, /* :-z */ + {"555555613", {21, 18}}, /* :-o */ + {"555555625", {21, 18}}, /* :-p */ + {"433", {21, 18}}, /* :/ */ + {"555555622", {21, 18}}, /* :P */ + {"555555640", {21, 18}}, /* :-| */ + {"555555623", {21, 18}}, /* :-P */ + {"555555628", {21, 18}}, /* :) */ + {"555555632", {21, 18}}, /* 8-) */ + {"555555667", {20, 18}}, /* ;p */ + {"445", {21, 18}}, /* <3 */ + {"555555668", {20, 18}}, /* ;-p */ + {"555555679", {20, 18}}, /* :z */ + {"483", {20, 18}}, /* <3 */ + {"555555666", {20, 18}}, /* ;-P */ + {"497", {20, 18}}, /* O_o */ + {"555555664", {20, 18}}, /* :-p */ + {"555555671", {20, 18}}, /* :o */ + {"555555681", {20, 18}}, /* :Z */ + {"555555672", {20, 18}}, /* :-o */ + {"555555676", {20, 18}}, /* :-\\ */ + {"555555611", {21, 18}}, /* :-O */ + {"555555670", {20, 18}}, /* :-O */ + {"555555688", {20, 18}}, /* :-D */ + {"441", {21, 18}}, /* B) */ + {"555555601", {21, 18}}, /* >( */ + {"491", {20, 18}}, /* ;P */ + {"496", {20, 18}}, /* :D */ + {"492", {20, 18}}, /* :O */ + {"555555573", {24, 18}}, /* o_O */ + {"555555643", {21, 18}}, /* :Z */ + {"1898", {26, 28}}, /* ThunBeast */ + {"555555682", {20, 18}}, /* :-Z */ + {"1896", {20, 30}}, /* WholeWheat */ + {"1906", {24, 30}}, /* SoBayed */ + {"555555607", {21, 18}}, /* :-( */ + {"555555660", {20, 18}}, /* :-( */ + {"489", {20, 18}}, /* :( */ + {"495", {20, 18}}, /* :s */ + {"555555638", {21, 18}}, /* :-D */ + {"357", {28, 30}}, /* HotPokket */ + {"555555624", {21, 18}}, /* :p */ + {"73", {21, 30}}, /* DBstyle */ + {"555555674", {20, 18}}, /* :-/ */ + {"555555629", {21, 18}}, /* :-) */ + {"555555600", {24, 18}}, /* R-) */ + {"41", {19, 27}}, /* Kreygasm */ + {"555555612", {21, 18}}, /* :o */ + {"488", {29, 24}}, /* :7 */ + {"69", {41, 28}}, /* BloodTrail */ + {"555555608", {21, 18}}, /* R) */ + {"501", {20, 18}}, /* ;) */ + {"50", {18, 27}}, /* ArsonNoSexy */ + {"443", {21, 18}}, /* :D */ + {"1904", {24, 30}}, /* BigBrother */ + {"555555595", {24, 18}}, /* ;P */ + {"555555663", {20, 18}}, /* :p */ + {"555555576", {24, 18}}, /* o.o */ + {"360", {22, 30}}, /* FailFish */ + {"500", {20, 18}}, /* B) */ + {"3", {24, 18}}, /* :D */ + {"484", {20, 22}}, /* R) */ + {"555555678", {20, 18}}, /* :-| */ + {"7", {24, 18}}, /* B) */ + {"52", {32, 32}}, /* SMOrc */ + {"555555644", {21, 18}}, /* :-Z */ + {"18", {20, 27}}, /* TheRinger */ + {"49106", {27, 28}}, /* CorgiDerp */ + {"6", {24, 18}}, /* O_o */ + {"10", {24, 18}}, /* :/ */ + {"47", {24, 24}}, /* PunchTrees */ + {"555555561", {24, 18}}, /* :-D */ + {"555555564", {24, 18}}, /* :-| */ + {"13", {24, 18}}, /* ;P */ + {"555555593", {24, 18}}, /* :p */ + {"555555589", {24, 18}}, /* ;) */ + {"555555590", {24, 18}}, /* ;-) */ + {"486", {27, 42}}, /* :> */ + {"40", {21, 27}}, /* KevinTurtle */ + {"555555558", {24, 18}}, /* :( */ + {"555555597", {24, 18}}, /* ;p */ + {"555555580", {24, 18}}, /* :O */ + {"555555567", {24, 18}}, /* :Z */ + {"1", {24, 18}}, /* :) */ + {"11", {24, 18}}, /* ;) */ + {"33", {25, 32}}, /* DansGame */ + {"555555586", {24, 18}}, /* :-/ */ + {"4", {24, 18}}, /* >( */ + {"555555588", {24, 18}}, /* :-\\ */ + {"12", {24, 18}}, /* :P */ + {"555555563", {24, 18}}, /* :| */ + {"555555581", {24, 18}}, /* :-O */ + {"555555598", {24, 18}}, /* ;-p */ + {"555555596", {24, 18}}, /* ;-P */ + {"555555557", {24, 18}}, /* :-) */ + {"498", {20, 18}}, /* >( */ + {"555555680", {20, 18}}, /* :-z */ + {"555555587", {24, 18}}, /* :\\ */ + {"5", {24, 18}}, /* :| */ + {"354", {20, 30}}, /* 4Head */ + {"555555562", {24, 18}}, /* >( */ + {"555555594", {24, 18}}, /* :-p */ + {"490", {20, 18}}, /* :P */ + {"555555662", {20, 18}}, /* :-P */ + {"2", {24, 18}}, /* :( */ + {"1902", {27, 29}}, /* Keepo */ + {"555555627", {21, 18}}, /* ;-) */ + {"555555566", {24, 18}}, /* :-z */ + {"555555559", {24, 18}}, /* :-( */ + {"555555592", {24, 18}}, /* :-P */ + {"28", {39, 27}}, /* MrDestructoid */ + {"8", {24, 18}}, /* :O */ + {"244", {24, 30}}, /* FUNgineer */ + {"555555591", {24, 18}}, /* :P */ + {"555555585", {24, 18}}, /* :/ */ + {"494", {20, 18}}, /* :| */ + {"9", {24, 18}}, /* <3 */ + {"555555584", {24, 18}}, /* <3 */ + {"555555579", {24, 18}}, /* 8-) */ + {"14", {24, 18}}, /* R) */ + {"485", {27, 18}}, /* #/ */ + {"555555560", {24, 18}}, /* :D */ + {"86", {36, 30}}, /* BibleThump */ + {"555555578", {24, 18}}, /* B-) */ + {"17", {20, 27}}, /* StoneLightning */ + {"436", {21, 18}}, /* :O */ + {"555555675", {20, 18}}, /* :\\ */ + {"22", {19, 27}}, /* RedCoat */ + {"555555574", {24, 18}}, /* o.O */ + {"555555603", {21, 18}}, /* :-/ */ + {"1901", {24, 28}}, /* Kippa */ + {"15", {21, 27}}, /* JKanStyle */ + {"555555605", {21, 18}}, /* :-\\ */ + {"555555701", {20, 18}}, /* ;-) */ + {"487", {20, 42}}, /* <] */ + {"555555572", {24, 18}}, /* O.O */ + {"65", {40, 30}}, /* FrankerZ */ + {"25", {25, 28}}, /* Kappa */ + {"36", {36, 30}}, /* PJSalt */ + {"499", {20, 18}}, /* :) */ + {"555555565", {24, 18}}, /* :z */ + {"434", {21, 18}}, /* :( */ + {"555555577", {24, 18}}, /* B) */ + {"34", {21, 28}}, /* SwiftRage */ + {"555555575", {24, 18}}, /* o_o */ + {"92", {23, 30}}, /* PMSTwin */ + {"555555570", {24, 18}}, /* O.o */ + {"555555569", {24, 18}}, /* O_o */ + {"493", {20, 18}}, /* :/ */ + {"26", {20, 27}}, /* JonCarnage */ + {"66", {20, 27}}, /* OneHand */ + {"555555568", {24, 18}}, /* :-Z */ + {"555555599", {24, 18}}, /* R) */ + {"1900", {33, 30}}, /* RalpherZ */ + {"555555582", {24, 18}}, /* :o */ + {"1899", {22, 30}}, /* TF2John */ + {"555555633", {21, 18}}, /* ;P */ + {"16", {22, 27}}, /* OptimizePrime */ + {"30", {29, 27}}, /* BCWarrior */ + {"555555583", {24, 18}}, /* :-o */ + {"32", {21, 27}}, /* GingerPower */ + {"87", {24, 30}}, /* ShazBotstix */ + {"74", {24, 30}}, /* AsianGlow */ + {"555555571", {24, 18}}, /* O_O */ + {"46", {24, 24}}, /* SSSsss */ + }; + + auto it = outliers.find(id.string); + if (it != outliers.end()) + { + return it->second; + } + + return defaultBaseSize; +} + +qreal getEmote3xScaleFactor(const EmoteId &id) +{ + // From Twitch docs - expected size for an emote (1x) + constexpr qreal default3xScaleFactor = 0.25; + static std::unordered_map outliers{ + {"555555635", 0.3333333333333333}, /* ;p */ + {"555555636", 0.3333333333333333}, /* ;-p */ + {"555555614", 0.3333333333333333}, /* O_o */ + {"555555641", 0.3333333333333333}, /* :z */ + {"555555604", 0.3333333333333333}, /* :\\ */ + {"444", 0.3333333333333333}, /* :| */ + {"555555634", 0.3333333333333333}, /* ;-P */ + {"439", 0.3333333333333333}, /* ;) */ + {"555555642", 0.3333333333333333}, /* :-z */ + {"555555613", 0.3333333333333333}, /* :-o */ + {"555555625", 0.3333333333333333}, /* :-p */ + {"433", 0.3333333333333333}, /* :/ */ + {"555555622", 0.3333333333333333}, /* :P */ + {"555555640", 0.3333333333333333}, /* :-| */ + {"555555623", 0.3333333333333333}, /* :-P */ + {"555555628", 0.3333333333333333}, /* :) */ + {"555555632", 0.3333333333333333}, /* 8-) */ + {"555555667", 0.3333333333333333}, /* ;p */ + {"445", 0.3333333333333333}, /* <3 */ + {"555555668", 0.3333333333333333}, /* ;-p */ + {"555555679", 0.3333333333333333}, /* :z */ + {"483", 0.3333333333333333}, /* <3 */ + {"555555666", 0.3333333333333333}, /* ;-P */ + {"497", 0.3333333333333333}, /* O_o */ + {"555555664", 0.3333333333333333}, /* :-p */ + {"555555671", 0.3333333333333333}, /* :o */ + {"555555681", 0.3333333333333333}, /* :Z */ + {"555555672", 0.3333333333333333}, /* :-o */ + {"555555676", 0.3333333333333333}, /* :-\\ */ + {"555555611", 0.3333333333333333}, /* :-O */ + {"555555670", 0.3333333333333333}, /* :-O */ + {"555555688", 0.3333333333333333}, /* :-D */ + {"441", 0.3333333333333333}, /* B) */ + {"555555601", 0.3333333333333333}, /* >( */ + {"491", 0.3333333333333333}, /* ;P */ + {"496", 0.3333333333333333}, /* :D */ + {"492", 0.3333333333333333}, /* :O */ + {"555555573", 0.3333333333333333}, /* o_O */ + {"555555643", 0.3333333333333333}, /* :Z */ + {"1898", 0.3333333333333333}, /* ThunBeast */ + {"555555682", 0.3333333333333333}, /* :-Z */ + {"1896", 0.3333333333333333}, /* WholeWheat */ + {"1906", 0.3333333333333333}, /* SoBayed */ + {"555555607", 0.3333333333333333}, /* :-( */ + {"555555660", 0.3333333333333333}, /* :-( */ + {"489", 0.3333333333333333}, /* :( */ + {"495", 0.3333333333333333}, /* :s */ + {"555555638", 0.3333333333333333}, /* :-D */ + {"357", 0.3333333333333333}, /* HotPokket */ + {"555555624", 0.3333333333333333}, /* :p */ + {"73", 0.3333333333333333}, /* DBstyle */ + {"555555674", 0.3333333333333333}, /* :-/ */ + {"555555629", 0.3333333333333333}, /* :-) */ + {"555555600", 0.3333333333333333}, /* R-) */ + {"41", 0.3333333333333333}, /* Kreygasm */ + {"555555612", 0.3333333333333333}, /* :o */ + {"488", 0.3333333333333333}, /* :7 */ + {"69", 0.3333333333333333}, /* BloodTrail */ + {"555555608", 0.3333333333333333}, /* R) */ + {"501", 0.3333333333333333}, /* ;) */ + {"50", 0.3333333333333333}, /* ArsonNoSexy */ + {"443", 0.3333333333333333}, /* :D */ + {"1904", 0.3333333333333333}, /* BigBrother */ + {"555555595", 0.3333333333333333}, /* ;P */ + {"555555663", 0.3333333333333333}, /* :p */ + {"555555576", 0.3333333333333333}, /* o.o */ + {"360", 0.3333333333333333}, /* FailFish */ + {"500", 0.3333333333333333}, /* B) */ + {"3", 0.3333333333333333}, /* :D */ + {"484", 0.3333333333333333}, /* R) */ + {"555555678", 0.3333333333333333}, /* :-| */ + {"7", 0.3333333333333333}, /* B) */ + {"52", 0.3333333333333333}, /* SMOrc */ + {"555555644", 0.3333333333333333}, /* :-Z */ + {"18", 0.3333333333333333}, /* TheRinger */ + {"49106", 0.3333333333333333}, /* CorgiDerp */ + {"6", 0.3333333333333333}, /* O_o */ + {"10", 0.3333333333333333}, /* :/ */ + {"47", 0.3333333333333333}, /* PunchTrees */ + {"555555561", 0.3333333333333333}, /* :-D */ + {"555555564", 0.3333333333333333}, /* :-| */ + {"13", 0.3333333333333333}, /* ;P */ + {"555555593", 0.3333333333333333}, /* :p */ + {"555555589", 0.3333333333333333}, /* ;) */ + {"555555590", 0.3333333333333333}, /* ;-) */ + {"486", 0.3333333333333333}, /* :> */ + {"40", 0.3333333333333333}, /* KevinTurtle */ + {"555555558", 0.3333333333333333}, /* :( */ + {"555555597", 0.3333333333333333}, /* ;p */ + {"555555580", 0.3333333333333333}, /* :O */ + {"555555567", 0.3333333333333333}, /* :Z */ + {"1", 0.3333333333333333}, /* :) */ + {"11", 0.3333333333333333}, /* ;) */ + {"33", 0.3333333333333333}, /* DansGame */ + {"555555586", 0.3333333333333333}, /* :-/ */ + {"4", 0.3333333333333333}, /* >( */ + {"555555588", 0.3333333333333333}, /* :-\\ */ + {"12", 0.3333333333333333}, /* :P */ + {"555555563", 0.3333333333333333}, /* :| */ + {"555555581", 0.3333333333333333}, /* :-O */ + {"555555598", 0.3333333333333333}, /* ;-p */ + {"555555596", 0.3333333333333333}, /* ;-P */ + {"555555557", 0.3333333333333333}, /* :-) */ + {"498", 0.3333333333333333}, /* >( */ + {"555555680", 0.3333333333333333}, /* :-z */ + {"555555587", 0.3333333333333333}, /* :\\ */ + {"5", 0.3333333333333333}, /* :| */ + {"354", 0.3333333333333333}, /* 4Head */ + {"555555562", 0.3333333333333333}, /* >( */ + {"555555594", 0.3333333333333333}, /* :-p */ + {"490", 0.3333333333333333}, /* :P */ + {"555555662", 0.3333333333333333}, /* :-P */ + {"2", 0.3333333333333333}, /* :( */ + {"1902", 0.3333333333333333}, /* Keepo */ + {"555555627", 0.3333333333333333}, /* ;-) */ + {"555555566", 0.3333333333333333}, /* :-z */ + {"555555559", 0.3333333333333333}, /* :-( */ + {"555555592", 0.3333333333333333}, /* :-P */ + {"28", 0.3333333333333333}, /* MrDestructoid */ + {"8", 0.3333333333333333}, /* :O */ + {"244", 0.3333333333333333}, /* FUNgineer */ + {"555555591", 0.3333333333333333}, /* :P */ + {"555555585", 0.3333333333333333}, /* :/ */ + {"494", 0.3333333333333333}, /* :| */ + {"9", 0.21428571428571427}, /* <3 */ + {"555555584", 0.21428571428571427}, /* <3 */ + {"555555579", 0.3333333333333333}, /* 8-) */ + {"14", 0.3333333333333333}, /* R) */ + {"485", 0.3333333333333333}, /* #/ */ + {"555555560", 0.3333333333333333}, /* :D */ + {"86", 0.3333333333333333}, /* BibleThump */ + {"555555578", 0.3333333333333333}, /* B-) */ + {"17", 0.3333333333333333}, /* StoneLightning */ + {"436", 0.3333333333333333}, /* :O */ + {"555555675", 0.3333333333333333}, /* :\\ */ + {"22", 0.3333333333333333}, /* RedCoat */ + {"245", 0.3333333333333333}, /* ResidentSleeper */ + {"555555574", 0.3333333333333333}, /* o.O */ + {"555555603", 0.3333333333333333}, /* :-/ */ + {"1901", 0.3333333333333333}, /* Kippa */ + {"15", 0.3333333333333333}, /* JKanStyle */ + {"555555605", 0.3333333333333333}, /* :-\\ */ + {"555555701", 0.3333333333333333}, /* ;-) */ + {"487", 0.3333333333333333}, /* <] */ + {"22639", 0.3333333333333333}, /* BabyRage */ + {"555555572", 0.3333333333333333}, /* O.O */ + {"65", 0.3333333333333333}, /* FrankerZ */ + {"25", 0.3333333333333333}, /* Kappa */ + {"36", 0.3333333333333333}, /* PJSalt */ + {"499", 0.3333333333333333}, /* :) */ + {"555555565", 0.3333333333333333}, /* :z */ + {"434", 0.3333333333333333}, /* :( */ + {"555555577", 0.3333333333333333}, /* B) */ + {"34", 0.3333333333333333}, /* SwiftRage */ + {"555555575", 0.3333333333333333}, /* o_o */ + {"92", 0.3333333333333333}, /* PMSTwin */ + {"555555570", 0.3333333333333333}, /* O.o */ + {"555555569", 0.3333333333333333}, /* O_o */ + {"493", 0.3333333333333333}, /* :/ */ + {"26", 0.3333333333333333}, /* JonCarnage */ + {"66", 0.3333333333333333}, /* OneHand */ + {"973", 0.3333333333333333}, /* DAESuppy */ + {"555555568", 0.3333333333333333}, /* :-Z */ + {"555555599", 0.3333333333333333}, /* R) */ + {"1900", 0.3333333333333333}, /* RalpherZ */ + {"555555582", 0.3333333333333333}, /* :o */ + {"1899", 0.3333333333333333}, /* TF2John */ + {"555555633", 0.3333333333333333}, /* ;P */ + {"16", 0.3333333333333333}, /* OptimizePrime */ + {"30", 0.3333333333333333}, /* BCWarrior */ + {"555555583", 0.3333333333333333}, /* :-o */ + {"32", 0.3333333333333333}, /* GingerPower */ + {"87", 0.3333333333333333}, /* ShazBotstix */ + {"74", 0.3333333333333333}, /* AsianGlow */ + {"555555571", 0.3333333333333333}, /* O_O */ + {"46", 0.3333333333333333}, /* SSSsss */ + }; + + auto it = outliers.find(id.string); + if (it != outliers.end()) + { + return it->second; + } + + return default3xScaleFactor; +} + +} // namespace namespace chatterino { -TwitchEmotes::TwitchEmotes() -{ -} - QString TwitchEmotes::cleanUpEmoteCode(const QString &dirtyEmoteCode) { auto cleanCode = dirtyEmoteCode; @@ -49,12 +436,15 @@ EmotePtr TwitchEmotes::getOrCreateEmote(const EmoteId &id, if (!shared) { + auto baseSize = getEmoteExpectedBaseSize(id); + auto emote3xScaleFactor = getEmote3xScaleFactor(id); (*cache)[id] = shared = std::make_shared(Emote{ EmoteName{name}, ImageSet{ - Image::fromUrl(getEmoteLink(id, "1.0"), 1), - Image::fromUrl(getEmoteLink(id, "2.0"), 0.5), - Image::fromUrl(getEmoteLink(id, "3.0"), 0.25), + Image::fromUrl(getEmoteLink(id, "1.0"), 1, baseSize), + Image::fromUrl(getEmoteLink(id, "2.0"), 0.5, baseSize * 2), + Image::fromUrl(getEmoteLink(id, "3.0"), emote3xScaleFactor, + baseSize * (1.0 / emote3xScaleFactor)), }, Tooltip{name.toHtmlEscaped() + "
Twitch Emote"}, }); @@ -63,11 +453,4 @@ EmotePtr TwitchEmotes::getOrCreateEmote(const EmoteId &id, return shared; } -Url TwitchEmotes::getEmoteLink(const EmoteId &id, const QString &emoteScale) -{ - return {QString(TWITCH_EMOTE_TEMPLATE) - .replace("{id}", id.string) - .replace("{scale}", emoteScale)}; -} - } // namespace chatterino diff --git a/src/providers/twitch/TwitchEmotes.hpp b/src/providers/twitch/TwitchEmotes.hpp index 0f0234a47..f7691131c 100644 --- a/src/providers/twitch/TwitchEmotes.hpp +++ b/src/providers/twitch/TwitchEmotes.hpp @@ -1,21 +1,24 @@ #pragma once -#include -#include -#include -#include - #include "common/Aliases.hpp" #include "common/UniqueAccess.hpp" +#include +#include +#include + #include +#include + +namespace chatterino { // NB: "default" can be replaced with "static" to always get a non-animated // variant -#define TWITCH_EMOTE_TEMPLATE \ - "https://static-cdn.jtvnw.net/emoticons/v2/{id}/default/dark/{scale}" +/// %1 <-> {id} +/// %2 <-> {scale} (1.0, 2.0, 3.0) +constexpr QStringView TWITCH_EMOTE_TEMPLATE = + u"https://static-cdn.jtvnw.net/emoticons/v2/%1/default/dark/%2"; -namespace chatterino { struct Emote; using EmotePtr = std::shared_ptr; @@ -33,20 +36,27 @@ struct CheerEmoteSet { std::vector cheerEmotes; }; -class TwitchEmotes +class ITwitchEmotes +{ +public: + virtual ~ITwitchEmotes() = default; + + virtual EmotePtr getOrCreateEmote(const EmoteId &id, + const EmoteName &name) = 0; +}; + +class TwitchEmotes : public ITwitchEmotes { public: static QString cleanUpEmoteCode(const QString &dirtyEmoteCode); - TwitchEmotes(); + TwitchEmotes() = default; - EmotePtr getOrCreateEmote(const EmoteId &id, const EmoteName &name); + EmotePtr getOrCreateEmote(const EmoteId &id, + const EmoteName &name) override; private: - Url getEmoteLink(const EmoteId &id, const QString &emoteScale); UniqueAccess>> twitchEmotesCache_; - - std::mutex mutex_; }; } // namespace chatterino diff --git a/src/providers/twitch/TwitchHelpers.cpp b/src/providers/twitch/TwitchHelpers.cpp index e5fdddfef..2cf8853c6 100644 --- a/src/providers/twitch/TwitchHelpers.cpp +++ b/src/providers/twitch/TwitchHelpers.cpp @@ -1,4 +1,5 @@ #include "providers/twitch/TwitchHelpers.hpp" + #include "common/QLogging.hpp" namespace chatterino { diff --git a/src/providers/twitch/TwitchIrcServer.cpp b/src/providers/twitch/TwitchIrcServer.cpp index b052134db..6fbbb63d7 100644 --- a/src/providers/twitch/TwitchIrcServer.cpp +++ b/src/providers/twitch/TwitchIrcServer.cpp @@ -1,72 +1,715 @@ -#include "TwitchIrcServer.hpp" - -#include -#include +#include "providers/twitch/TwitchIrcServer.hpp" #include "Application.hpp" +#include "common/Channel.hpp" #include "common/Common.hpp" #include "common/Env.hpp" #include "common/QLogging.hpp" #include "controllers/accounts/AccountController.hpp" +#include "messages/LimitedQueueSnapshot.hpp" #include "messages/Message.hpp" #include "messages/MessageBuilder.hpp" +#include "providers/bttv/BttvEmotes.hpp" +#include "providers/ffz/FfzEmotes.hpp" +#include "providers/irc/IrcConnection2.hpp" +#include "providers/seventv/SeventvEmotes.hpp" +#include "providers/seventv/SeventvEventAPI.hpp" +#include "providers/twitch/api/Helix.hpp" #include "providers/twitch/IrcMessageHandler.hpp" +#include "providers/twitch/PubSubActions.hpp" #include "providers/twitch/PubSubManager.hpp" +#include "providers/twitch/pubsubmessages/AutoMod.hpp" #include "providers/twitch/TwitchAccount.hpp" #include "providers/twitch/TwitchChannel.hpp" -#include "providers/twitch/TwitchHelpers.hpp" -#include "util/Helpers.hpp" +#include "singletons/Settings.hpp" +#include "singletons/StreamerMode.hpp" #include "util/PostToThread.hpp" +#include "util/RatelimitBucket.hpp" +#include +#include +#include +#include +#include #include -// using namespace Communi; +#include +#include +#include + using namespace std::chrono_literals; -#define TWITCH_PUBSUB_URL "wss://pubsub-edge.twitch.tv" +namespace { + +// Ratelimits for joinBucket_ +constexpr int JOIN_RATELIMIT_BUDGET = 18; +constexpr int JOIN_RATELIMIT_COOLDOWN = 12500; + +using namespace chatterino; + +void sendHelixMessage(const std::shared_ptr &channel, + const QString &message, const QString &replyParentId = {}) +{ + auto broadcasterID = channel->roomId(); + if (broadcasterID.isEmpty()) + { + channel->addSystemMessage( + "Sending messages in this channel isn't possible."); + return; + } + + getHelix()->sendChatMessage( + { + .broadcasterID = broadcasterID, + .senderID = + getApp()->getAccounts()->twitch.getCurrent()->getUserId(), + .message = message, + .replyParentMessageID = replyParentId, + }, + [weak = std::weak_ptr(channel)](const auto &res) { + auto chan = weak.lock(); + if (!chan) + { + return; + } + + if (res.isSent) + { + return; + } + + if (res.dropReason) + { + chan->addSystemMessage(res.dropReason->message); + } + else + { + chan->addSystemMessage("Your message was not sent."); + } + }, + [weak = std::weak_ptr(channel)](auto error, auto message) { + auto chan = weak.lock(); + if (!chan) + { + return; + } + + if (message.isEmpty()) + { + message = "(empty message)"; + } + + using Error = decltype(error); + + auto errorMessage = [&]() -> QString { + switch (error) + { + case Error::MissingText: + return "You can't send an empty message."; + case Error::BadRequest: + return "Failed to send message: " + message; + case Error::Forbidden: + return "You are not allowed to send messages in this " + "channel."; + case Error::MessageTooLarge: + return "Your message was too long."; + case Error::UserMissingScope: + return "Missing required scope. Re-login with your " + "account and try again."; + case Error::Forwarded: + return message; + case Error::Unknown: + default: + return "Unknown error: " + message; + } + }(); + chan->addSystemMessage(errorMessage); + }); +} + +/// Returns true if chat messages should be sent over Helix +bool shouldSendHelixChat() +{ + switch (getSettings()->chatSendProtocol) + { + case ChatSendProtocol::Helix: + return true; + case ChatSendProtocol::Default: + case ChatSendProtocol::IRC: + return false; + default: + assert(false && "Invalid chat protocol value"); + return false; + } +} + +} // namespace namespace chatterino { TwitchIrcServer::TwitchIrcServer() : whispersChannel(new Channel("/whispers", Channel::Type::TwitchWhispers)) , mentionsChannel(new Channel("/mentions", Channel::Type::TwitchMentions)) - , watchingChannel(Channel::getEmpty(), Channel::Type::TwitchWatching) , liveChannel(new Channel("/live", Channel::Type::TwitchLive)) + , automodChannel(new Channel("/automod", Channel::Type::TwitchAutomod)) + , watchingChannel(Channel::getEmpty(), Channel::Type::TwitchWatching) { - this->initializeIrc(); + // Initialize the connections + // XXX: don't create write connection if there is no separate write connection. + this->writeConnection_.reset(new IrcConnection); + this->writeConnection_->moveToThread( + QCoreApplication::instance()->thread()); - this->pubsub = new PubSub(TWITCH_PUBSUB_URL); + // Apply a leaky bucket rate limiting to JOIN messages + auto actuallyJoin = [&](QString message) { + if (!this->channels.contains(message)) + { + return; + } + this->readConnection_->sendRaw("JOIN #" + message); + }; + this->joinBucket_.reset(new RatelimitBucket( + JOIN_RATELIMIT_BUDGET, JOIN_RATELIMIT_COOLDOWN, actuallyJoin, this)); - // getSettings()->twitchSeperateWriteConnection.connect([this](auto, auto) { - // this->connect(); }, - // this->signalHolder_, - // false); + QObject::connect(this->writeConnection_.get(), + &Communi::IrcConnection::messageReceived, this, + [this](auto msg) { + this->writeConnectionMessageReceived(msg); + }); + QObject::connect(this->writeConnection_.get(), + &Communi::IrcConnection::connected, this, [this] { + this->onWriteConnected(this->writeConnection_.get()); + }); + this->connections_.managedConnect( + this->writeConnection_->connectionLost, [this](bool timeout) { + qCDebug(chatterinoIrc) + << "Write connection reconnect requested. Timeout:" << timeout; + this->writeConnection_->smartReconnect(); + }); + + // Listen to read connection message signals + this->readConnection_.reset(new IrcConnection); + this->readConnection_->moveToThread(QCoreApplication::instance()->thread()); + + QObject::connect(this->readConnection_.get(), + &Communi::IrcConnection::messageReceived, this, + [this](auto msg) { + this->readConnectionMessageReceived(msg); + }); + QObject::connect(this->readConnection_.get(), + &Communi::IrcConnection::privateMessageReceived, this, + [this](auto msg) { + this->privateMessageReceived(msg); + }); + QObject::connect(this->readConnection_.get(), + &Communi::IrcConnection::connected, this, [this] { + this->onReadConnected(this->readConnection_.get()); + }); + QObject::connect(this->readConnection_.get(), + &Communi::IrcConnection::disconnected, this, [this] { + this->onDisconnected(); + }); + this->connections_.managedConnect( + this->readConnection_->connectionLost, [this](bool timeout) { + qCDebug(chatterinoIrc) + << "Read connection reconnect requested. Timeout:" << timeout; + if (timeout) + { + // Show additional message since this is going to interrupt a + // connection that is still "connected" + this->addGlobalSystemMessage( + "Server connection timed out, reconnecting"); + } + this->readConnection_->smartReconnect(); + }); + this->connections_.managedConnect(this->readConnection_->heartbeat, [this] { + this->markChannelsConnected(); + }); } -void TwitchIrcServer::initialize(Settings &settings, Paths &paths) +void TwitchIrcServer::initialize() { - getApp()->accounts->twitch.currentUserChanged.connect([this]() { + getApp()->getAccounts()->twitch.currentUserChanged.connect([this]() { postToThread([this] { this->connect(); - this->pubsub->setAccount(getApp()->accounts->twitch.getCurrent()); }); }); - this->reloadBTTVGlobalEmotes(); - this->reloadFFZGlobalEmotes(); + this->connections_.managedConnect( + getApp()->getTwitchPubSub()->moderation.chatCleared, + [this](const auto &action) { + auto chan = this->getChannelOrEmptyByID(action.roomID); + if (chan->isEmpty()) + { + return; + } - /* Refresh all twitch channel's live status in bulk every 30 seconds after starting chatterino */ - QObject::connect(&this->bulkLiveStatusTimer_, &QTimer::timeout, [=] { - this->bulkRefreshLiveStatus(); - }); - this->bulkLiveStatusTimer_.start(30 * 1000); + QString text = + QString("%1 cleared the chat.").arg(action.source.login); + + postToThread([chan, text] { + chan->addSystemMessage(text); + }); + }); + + this->connections_.managedConnect( + getApp()->getTwitchPubSub()->moderation.modeChanged, + [this](const auto &action) { + auto chan = this->getChannelOrEmptyByID(action.roomID); + if (chan->isEmpty()) + { + return; + } + + QString text = + QString("%1 turned %2 %3 mode.") + .arg(action.source.login) + .arg(action.state == ModeChangedAction::State::On ? "on" + : "off") + .arg(action.getModeName()); + + if (action.duration > 0) + { + text += QString(" (%1 seconds)").arg(action.duration); + } + + postToThread([chan, text] { + chan->addSystemMessage(text); + }); + }); + + this->connections_.managedConnect( + getApp()->getTwitchPubSub()->moderation.moderationStateChanged, + [this](const auto &action) { + auto chan = this->getChannelOrEmptyByID(action.roomID); + if (chan->isEmpty()) + { + return; + } + + QString text; + + text = QString("%1 %2 %3.") + .arg(action.source.login, + (action.modded ? "modded" : "unmodded"), + action.target.login); + + postToThread([chan, text] { + chan->addSystemMessage(text); + }); + }); + + this->connections_.managedConnect( + getApp()->getTwitchPubSub()->moderation.userBanned, + [this](const auto &action) { + auto chan = this->getChannelOrEmptyByID(action.roomID); + + if (chan->isEmpty()) + { + return; + } + + postToThread([chan, action] { + MessageBuilder msg(action); + msg->flags.set(MessageFlag::PubSub); + chan->addOrReplaceTimeout(msg.release()); + }); + }); + + this->connections_.managedConnect( + getApp()->getTwitchPubSub()->moderation.userWarned, + [this](const auto &action) { + auto chan = this->getChannelOrEmptyByID(action.roomID); + + if (chan->isEmpty()) + { + return; + } + + // TODO: Resolve the moderator's user ID into a full user here, so message can look better + postToThread([chan, action] { + MessageBuilder msg(action); + msg->flags.set(MessageFlag::PubSub); + chan->addMessage(msg.release(), MessageContext::Original); + }); + }); + + this->connections_.managedConnect( + getApp()->getTwitchPubSub()->moderation.messageDeleted, + [this](const auto &action) { + auto chan = this->getChannelOrEmptyByID(action.roomID); + + if (chan->isEmpty() || getSettings()->hideDeletionActions) + { + return; + } + + auto msg = MessageBuilder::makeDeletionMessageFromPubSub(action); + + postToThread([chan, msg] { + auto replaced = false; + LimitedQueueSnapshot snapshot = + chan->getMessageSnapshot(); + int snapshotLength = snapshot.size(); + + // without parens it doesn't build on windows + int end = (std::max)(0, snapshotLength - 200); + + for (int i = snapshotLength - 1; i >= end; --i) + { + const auto &s = snapshot[i]; + if (!s->flags.has(MessageFlag::PubSub) && + s->timeoutUser == msg->timeoutUser) + { + chan->replaceMessage(s, msg); + replaced = true; + break; + } + } + if (!replaced) + { + chan->addMessage(msg, MessageContext::Original); + } + }); + }); + + this->connections_.managedConnect( + getApp()->getTwitchPubSub()->moderation.userUnbanned, + [this](const auto &action) { + auto chan = this->getChannelOrEmptyByID(action.roomID); + + if (chan->isEmpty()) + { + return; + } + + auto msg = MessageBuilder(action).release(); + + postToThread([chan, msg] { + chan->addMessage(msg, MessageContext::Original); + }); + }); + + this->connections_.managedConnect( + getApp()->getTwitchPubSub()->moderation.suspiciousMessageReceived, + [this](const auto &action) { + if (action.treatment == + PubSubLowTrustUsersMessage::Treatment::INVALID) + { + qCWarning(chatterinoTwitch) + << "Received suspicious message with unknown " + "treatment:" + << action.treatmentString; + return; + } + + // monitored chats are received over irc; in the future, we will use pubsub instead + if (action.treatment != + PubSubLowTrustUsersMessage::Treatment::Restricted) + { + return; + } + + if (getSettings()->streamerModeHideModActions && + getApp()->getStreamerMode()->isEnabled()) + { + return; + } + + auto chan = this->getChannelOrEmptyByID(action.channelID); + + if (chan->isEmpty()) + { + return; + } + + auto twitchChannel = std::dynamic_pointer_cast(chan); + if (!twitchChannel) + { + return; + } + + postToThread([twitchChannel, action] { + const auto p = MessageBuilder::makeLowTrustUserMessage( + action, twitchChannel->getName(), twitchChannel.get()); + twitchChannel->addMessage(p.first, MessageContext::Original); + twitchChannel->addMessage(p.second, MessageContext::Original); + }); + }); + + this->connections_.managedConnect( + getApp()->getTwitchPubSub()->moderation.suspiciousTreatmentUpdated, + [this](const auto &action) { + if (action.treatment == + PubSubLowTrustUsersMessage::Treatment::INVALID) + { + qCWarning(chatterinoTwitch) + << "Received suspicious user update with unknown " + "treatment:" + << action.treatmentString; + return; + } + + if (action.updatedByUserLogin.isEmpty()) + { + return; + } + + if (getSettings()->streamerModeHideModActions && + getApp()->getStreamerMode()->isEnabled()) + { + return; + } + + auto chan = this->getChannelOrEmptyByID(action.channelID); + if (chan->isEmpty()) + { + return; + } + + postToThread([chan, action] { + auto msg = MessageBuilder::makeLowTrustUpdateMessage(action); + chan->addMessage(msg, MessageContext::Original); + }); + }); + + this->connections_.managedConnect( + getApp()->getTwitchPubSub()->moderation.autoModMessageCaught, + [this](const auto &msg, const QString &channelID) { + auto chan = this->getChannelOrEmptyByID(channelID); + if (chan->isEmpty()) + { + return; + } + + switch (msg.type) + { + case PubSubAutoModQueueMessage::Type::AutoModCaughtMessage: { + if (msg.status == "PENDING") + { + AutomodAction action(msg.data, channelID); + action.reason = QString("%1 level %2") + .arg(msg.contentCategory) + .arg(msg.contentLevel); + + action.msgID = msg.messageID; + action.message = msg.messageText; + + // this message also contains per-word automod data, which could be implemented + + // extract sender data manually because Twitch loves not being consistent + QString senderDisplayName = + msg.senderUserDisplayName; // Might be transformed later + bool hasLocalizedName = false; + if (!msg.senderUserDisplayName.isEmpty()) + { + // check for non-ascii display names + if (QString::compare(msg.senderUserDisplayName, + msg.senderUserLogin, + Qt::CaseInsensitive) != 0) + { + hasLocalizedName = true; + } + } + QColor senderColor = msg.senderUserChatColor; + QString senderColor_; + if (!senderColor.isValid() && + getSettings()->colorizeNicknames) + { + // color may be not present if user is a grey-name + senderColor = getRandomColor(msg.senderUserID); + } + + // handle username style based on prefered setting + switch (getSettings()->usernameDisplayMode.getValue()) + { + case UsernameDisplayMode::Username: { + if (hasLocalizedName) + { + senderDisplayName = msg.senderUserLogin; + } + break; + } + case UsernameDisplayMode::LocalizedName: { + break; + } + case UsernameDisplayMode:: + UsernameAndLocalizedName: { + if (hasLocalizedName) + { + senderDisplayName = QString("%1(%2)").arg( + msg.senderUserLogin, + msg.senderUserDisplayName); + } + break; + } + } + + action.target = + ActionUser{msg.senderUserID, msg.senderUserLogin, + senderDisplayName, senderColor}; + postToThread([chan, action] { + const auto p = MessageBuilder::makeAutomodMessage( + action, chan->getName()); + chan->addMessage(p.first, MessageContext::Original); + chan->addMessage(p.second, + MessageContext::Original); + + getApp() + ->getTwitch() + ->getAutomodChannel() + ->addMessage(p.first, MessageContext::Original); + getApp() + ->getTwitch() + ->getAutomodChannel() + ->addMessage(p.second, + MessageContext::Original); + + if (getSettings()->showAutomodInMentions) + { + getApp() + ->getTwitch() + ->getMentionsChannel() + ->addMessage(p.first, + MessageContext::Original); + getApp() + ->getTwitch() + ->getMentionsChannel() + ->addMessage(p.second, + MessageContext::Original); + } + }); + } + // "ALLOWED" and "DENIED" statuses remain unimplemented + // They are versions of automod_message_(denied|approved) but for mods. + } + break; + + case PubSubAutoModQueueMessage::Type::INVALID: + default: { + } + break; + } + }); + + this->connections_.managedConnect( + getApp()->getTwitchPubSub()->moderation.autoModMessageBlocked, + [this](const auto &action) { + auto chan = this->getChannelOrEmptyByID(action.roomID); + if (chan->isEmpty()) + { + return; + } + + postToThread([chan, action] { + const auto p = + MessageBuilder::makeAutomodMessage(action, chan->getName()); + chan->addMessage(p.first, MessageContext::Original); + chan->addMessage(p.second, MessageContext::Original); + }); + }); + + this->connections_.managedConnect( + getApp()->getTwitchPubSub()->moderation.automodUserMessage, + [this](const auto &action) { + if (getSettings()->streamerModeHideModActions && + getApp()->getStreamerMode()->isEnabled()) + { + return; + } + auto chan = this->getChannelOrEmptyByID(action.roomID); + + if (chan->isEmpty()) + { + return; + } + + auto msg = MessageBuilder(action).release(); + + postToThread([chan, msg] { + chan->addMessage(msg, MessageContext::Original); + }); + chan->deleteMessage(msg->id); + }); + + this->connections_.managedConnect( + getApp()->getTwitchPubSub()->moderation.automodInfoMessage, + [this](const auto &action) { + auto chan = this->getChannelOrEmptyByID(action.roomID); + + if (chan->isEmpty()) + { + return; + } + + postToThread([chan, action] { + const auto p = MessageBuilder::makeAutomodInfoMessage(action); + chan->addMessage(p, MessageContext::Original); + }); + }); + + this->connections_.managedConnect( + getApp()->getTwitchPubSub()->moderation.raidStarted, + [this](const auto &action) { + auto chan = this->getChannelOrEmptyByID(action.roomID); + + if (chan->isEmpty()) + { + return; + } + + auto msg = MessageBuilder(action).release(); + + postToThread([chan, msg] { + chan->addMessage(msg, MessageContext::Original); + }); + }); + + this->connections_.managedConnect( + getApp()->getTwitchPubSub()->moderation.raidCanceled, + [this](const auto &action) { + auto chan = this->getChannelOrEmptyByID(action.roomID); + + if (chan->isEmpty()) + { + return; + } + + auto msg = MessageBuilder(action).release(); + + postToThread([chan, msg] { + chan->addMessage(msg, MessageContext::Original); + }); + }); + + this->connections_.managedConnect( + getApp()->getTwitchPubSub()->pointReward.redeemed, [this](auto &data) { + QString channelId = data.value("channel_id").toString(); + if (channelId.isEmpty()) + { + qCDebug(chatterinoApp) + << "Couldn't find channel id of point reward"; + return; + } + + auto chan = this->getChannelOrEmptyByID(channelId); + + auto reward = ChannelPointReward(data); + + postToThread([chan, reward] { + if (auto *channel = dynamic_cast(chan.get())) + { + channel->addChannelPointReward(reward); + } + }); + }); } void TwitchIrcServer::initializeConnection(IrcConnection *connection, ConnectionType type) { std::shared_ptr account = - getApp()->accounts->twitch.getCurrent(); + getApp()->getAccounts()->twitch.getCurrent(); qCDebug(chatterinoTwitch) << "logging in as" << account->getUserName(); @@ -100,7 +743,7 @@ void TwitchIrcServer::initializeConnection(IrcConnection *connection, connection->setPassword(oauthToken); } - // https://dev.twitch.tv/docs/irc/guide/#connecting-to-twitch-irc + // https://dev.twitch.tv/docs/irc#connecting-to-the-twitch-irc-server // SSL disabled: irc://irc.chat.twitch.tv:6667 (or port 80) // SSL enabled: irc://irc.chat.twitch.tv:6697 (or port 443) connection->setHost(Env::get().twitchServerHost); @@ -113,21 +756,35 @@ void TwitchIrcServer::initializeConnection(IrcConnection *connection, std::shared_ptr TwitchIrcServer::createChannel( const QString &channelName) { - auto channel = - std::shared_ptr(new TwitchChannel(channelName)); + auto channel = std::make_shared(channelName); channel->initialize(); - channel->sendMessageSignal.connect( - [this, channel = channel.get()](auto &chan, auto &msg, bool &sent) { - this->onMessageSendRequested(channel, msg, sent); + // We can safely ignore these signal connections since the TwitchIrcServer is only + // ever destroyed when the full Application state is about to be destroyed, at which point + // no Channel's should live + // NOTE: CHANNEL_LIFETIME + std::ignore = channel->sendMessageSignal.connect( + [this, channel = std::weak_ptr(channel)](auto &chan, auto &msg, + bool &sent) { + auto c = channel.lock(); + if (!c) + { + return; + } + this->onMessageSendRequested(c, msg, sent); }); - channel->sendReplySignal.connect( - [this, channel = channel.get()](auto &chan, auto &msg, auto &replyId, - bool &sent) { - this->onReplySendRequested(channel, msg, replyId, sent); + std::ignore = channel->sendReplySignal.connect( + [this, channel = std::weak_ptr(channel)](auto &chan, auto &msg, + auto &replyId, bool &sent) { + auto c = channel.lock(); + if (!c) + { + return; + } + this->onReplySendRequested(c, msg, replyId, sent); }); - return std::shared_ptr(channel); + return channel; } void TwitchIrcServer::privateMessageReceived( @@ -139,8 +796,6 @@ void TwitchIrcServer::privateMessageReceived( void TwitchIrcServer::readConnectionMessageReceived( Communi::IrcMessage *message) { - AbstractIrcServer::readConnectionMessageReceived(message); - if (message->type() == Communi::IrcMessage::Type::Private) { // We already have a handler for private messages @@ -195,6 +850,7 @@ void TwitchIrcServer::readConnectionMessageReceived( { this->addGlobalSystemMessage( "Twitch Servers requested us to reconnect, reconnecting"); + this->markChannelsConnected(); this->connect(); } else if (command == "GLOBALUSERSTATE") @@ -230,6 +886,84 @@ void TwitchIrcServer::writeConnectionMessageReceived( } } +void TwitchIrcServer::onReadConnected(IrcConnection *connection) +{ + (void)connection; + + std::lock_guard lock(this->channelMutex); + + // join channels + for (auto &&weak : this->channels) + { + if (auto channel = weak.lock()) + { + this->joinBucket_->send(channel->getName()); + } + } + + // connected/disconnected message + auto connectedMsg = makeSystemMessage("connected"); + connectedMsg->flags.set(MessageFlag::ConnectedMessage); + auto reconnected = makeSystemMessage("reconnected"); + reconnected->flags.set(MessageFlag::ConnectedMessage); + + for (std::weak_ptr &weak : this->channels.values()) + { + std::shared_ptr chan = weak.lock(); + if (!chan) + { + continue; + } + + LimitedQueueSnapshot snapshot = chan->getMessageSnapshot(); + + bool replaceMessage = + snapshot.size() > 0 && snapshot[snapshot.size() - 1]->flags.has( + MessageFlag::DisconnectedMessage); + + if (replaceMessage) + { + chan->replaceMessage(snapshot[snapshot.size() - 1], reconnected); + } + else + { + chan->addMessage(connectedMsg, MessageContext::Original); + } + } + + this->falloffCounter_ = 1; +} + +void TwitchIrcServer::onWriteConnected(IrcConnection *connection) +{ + (void)connection; +} + +void TwitchIrcServer::onDisconnected() +{ + std::lock_guard lock(this->channelMutex); + + MessageBuilder b(systemMessage, "disconnected"); + b->flags.set(MessageFlag::DisconnectedMessage); + auto disconnectedMsg = b.release(); + + for (std::weak_ptr &weak : this->channels.values()) + { + std::shared_ptr chan = weak.lock(); + if (!chan) + { + continue; + } + + chan->addMessage(disconnectedMsg, MessageContext::Original); + + if (auto *channel = dynamic_cast(chan.get())) + { + channel->markDisconnected(); + } + } +} + std::shared_ptr TwitchIrcServer::getCustomChannel( const QString &channelName) { @@ -248,24 +982,106 @@ std::shared_ptr TwitchIrcServer::getCustomChannel( return this->liveChannel; } - if (channelName == "$$$") + if (channelName == "/automod") { - static auto channel = - std::make_shared("$$$", chatterino::Channel::Type::Misc); - static auto getTimer = [&] { + return this->automodChannel; + } + + static auto getTimer = [](ChannelPtr channel, int msBetweenMessages, + bool addInitialMessages) { + if (addInitialMessages) + { for (auto i = 0; i < 1000; i++) { - channel->addMessage(makeSystemMessage(QString::number(i + 1))); + channel->addSystemMessage(QString::number(i + 1)); } + } - auto timer = new QTimer; - QObject::connect(timer, &QTimer::timeout, [] { - channel->addMessage( - makeSystemMessage(QTime::currentTime().toString())); - }); - timer->start(500); - return timer; - }(); + auto *timer = new QTimer; + QObject::connect(timer, &QTimer::timeout, [channel] { + channel->addSystemMessage(QTime::currentTime().toString()); + }); + timer->start(msBetweenMessages); + return timer; + }; + + if (channelName == "$$$") + { + static auto channel = std::make_shared( + channelName, chatterino::Channel::Type::Misc); + getTimer(channel, 500, true); + + return channel; + } + if (channelName == "$$$:e") + { + static auto channel = std::make_shared( + channelName, chatterino::Channel::Type::Misc); + getTimer(channel, 500, false); + + return channel; + } + if (channelName == "$$$$") + { + static auto channel = std::make_shared( + channelName, chatterino::Channel::Type::Misc); + getTimer(channel, 250, true); + + return channel; + } + if (channelName == "$$$$:e") + { + static auto channel = std::make_shared( + channelName, chatterino::Channel::Type::Misc); + getTimer(channel, 250, false); + + return channel; + } + if (channelName == "$$$$$") + { + static auto channel = std::make_shared( + channelName, chatterino::Channel::Type::Misc); + getTimer(channel, 100, true); + + return channel; + } + if (channelName == "$$$$$:e") + { + static auto channel = std::make_shared( + channelName, chatterino::Channel::Type::Misc); + getTimer(channel, 100, false); + + return channel; + } + if (channelName == "$$$$$$") + { + static auto channel = std::make_shared( + channelName, chatterino::Channel::Type::Misc); + getTimer(channel, 50, true); + + return channel; + } + if (channelName == "$$$$$$:e") + { + static auto channel = std::make_shared( + channelName, chatterino::Channel::Type::Misc); + getTimer(channel, 50, false); + + return channel; + } + if (channelName == "$$$$$$$") + { + static auto channel = std::make_shared( + channelName, chatterino::Channel::Type::Misc); + getTimer(channel, 25, true); + + return channel; + } + if (channelName == "$$$$$$$:e") + { + static auto channel = std::make_shared( + channelName, chatterino::Channel::Type::Misc); + getTimer(channel, 25, false); return channel; } @@ -281,6 +1097,7 @@ void TwitchIrcServer::forEachChannelAndSpecialChannels( func(this->whispersChannel); func(this->mentionsChannel); func(this->liveChannel); + func(this->automodChannel); } std::shared_ptr TwitchIrcServer::getChannelOrEmptyByID( @@ -292,14 +1109,18 @@ std::shared_ptr TwitchIrcServer::getChannelOrEmptyByID( { auto channel = weakChannel.lock(); if (!channel) + { continue; + } auto twitchChannel = std::dynamic_pointer_cast(channel); if (!twitchChannel) + { continue; + } if (twitchChannel->roomId() == channelId && - twitchChannel->getName().splitRef(":").size() < 3) + twitchChannel->getName().count(':') < 2) { return twitchChannel; } @@ -308,74 +1129,20 @@ std::shared_ptr TwitchIrcServer::getChannelOrEmptyByID( return Channel::getEmpty(); } -void TwitchIrcServer::bulkRefreshLiveStatus() -{ - auto twitchChans = std::make_shared>(); - - this->forEachChannel([twitchChans](ChannelPtr chan) { - auto tc = dynamic_cast(chan.get()); - if (tc && !tc->roomId().isEmpty()) - { - twitchChans->insert(tc->roomId(), tc); - } - }); - - // iterate over batches of channel IDs - for (const auto &batch : splitListIntoBatches(twitchChans->keys())) - { - getHelix()->fetchStreams( - batch, {}, - [twitchChans](std::vector streams) { - for (const auto &stream : streams) - { - // remaining channels will be used later to set their stream status as offline - // so we use take(id) to remove it - auto tc = twitchChans->take(stream.userId); - if (tc == nullptr) - { - continue; - } - - tc->parseLiveStatus(true, stream); - } - }, - []() { - // failure - }, - [batch, twitchChans] { - // All the channels that were not present in fetchStreams response should be assumed to be offline - // It is necessary to update their stream status in case they've gone live -> offline - // Otherwise some of them will be marked as live forever - for (const auto &chID : batch) - { - auto tc = twitchChans->value(chID); - // early out in case channel does not exist anymore - if (tc == nullptr) - { - continue; - } - - tc->parseLiveStatus(false, {}); - } - }); - } -} - QString TwitchIrcServer::cleanChannelName(const QString &dirtyChannelName) { if (dirtyChannelName.startsWith('#')) + { return dirtyChannelName.mid(1).toLower(); + } else + { return dirtyChannelName.toLower(); + } } -bool TwitchIrcServer::hasSeparateWriteConnection() const -{ - return true; - // return getSettings()->twitchSeperateWriteConnection; -} - -bool TwitchIrcServer::prepareToSend(TwitchChannel *channel) +bool TwitchIrcServer::prepareToSend( + const std::shared_ptr &channel) { std::lock_guard guard(this->lastMessageMutex_); @@ -391,10 +1158,7 @@ bool TwitchIrcServer::prepareToSend(TwitchChannel *channel) { if (this->lastErrorTimeSpeed_ + 30s < now) { - auto errorMessage = - makeSystemMessage("You are sending messages too quickly."); - - channel->addMessage(errorMessage); + channel->addSystemMessage("You are sending messages too quickly."); this->lastErrorTimeSpeed_ = now; } @@ -412,10 +1176,7 @@ bool TwitchIrcServer::prepareToSend(TwitchChannel *channel) { if (this->lastErrorTimeAmount_ + 30s < now) { - auto errorMessage = - makeSystemMessage("You are sending too many messages."); - - channel->addMessage(errorMessage); + channel->addSystemMessage("You are sending too many messages."); this->lastErrorTimeAmount_ = now; } @@ -426,8 +1187,9 @@ bool TwitchIrcServer::prepareToSend(TwitchChannel *channel) return true; } -void TwitchIrcServer::onMessageSendRequested(TwitchChannel *channel, - const QString &message, bool &sent) +void TwitchIrcServer::onMessageSendRequested( + const std::shared_ptr &channel, const QString &message, + bool &sent) { sent = false; @@ -437,13 +1199,21 @@ void TwitchIrcServer::onMessageSendRequested(TwitchChannel *channel, return; } - this->sendMessage(channel->getName(), message); + if (shouldSendHelixChat()) + { + sendHelixMessage(channel, message); + } + else + { + this->sendMessage(channel->getName(), message); + } + sent = true; } -void TwitchIrcServer::onReplySendRequested(TwitchChannel *channel, - const QString &message, - const QString &replyId, bool &sent) +void TwitchIrcServer::onReplySendRequested( + const std::shared_ptr &channel, const QString &message, + const QString &replyId, bool &sent) { sent = false; @@ -453,24 +1223,56 @@ void TwitchIrcServer::onReplySendRequested(TwitchChannel *channel, return; } - this->sendRawMessage("@reply-parent-msg-id=" + replyId + " PRIVMSG #" + - channel->getName() + " :" + message); - + if (shouldSendHelixChat()) + { + sendHelixMessage(channel, message, replyId); + } + else + { + this->sendRawMessage("@reply-parent-msg-id=" + replyId + " PRIVMSG #" + + channel->getName() + " :" + message); + } sent = true; } -const BttvEmotes &TwitchIrcServer::getBttvEmotes() const +const IndirectChannel &TwitchIrcServer::getWatchingChannel() const { - return this->bttv; -} -const FfzEmotes &TwitchIrcServer::getFfzEmotes() const -{ - return this->ffz; + return this->watchingChannel; } -void TwitchIrcServer::reloadBTTVGlobalEmotes() +void TwitchIrcServer::setWatchingChannel(ChannelPtr newWatchingChannel) { - this->bttv.loadEmotes(); + this->watchingChannel.reset(newWatchingChannel); +} + +ChannelPtr TwitchIrcServer::getWhispersChannel() const +{ + return this->whispersChannel; +} + +ChannelPtr TwitchIrcServer::getMentionsChannel() const +{ + return this->mentionsChannel; +} + +ChannelPtr TwitchIrcServer::getLiveChannel() const +{ + return this->liveChannel; +} + +ChannelPtr TwitchIrcServer::getAutomodChannel() const +{ + return this->automodChannel; +} + +QString TwitchIrcServer::getLastUserThatWhisperedMe() const +{ + return this->lastUserThatWhisperedMe.get(); +} + +void TwitchIrcServer::setLastUserThatWhisperedMe(const QString &user) +{ + this->lastUserThatWhisperedMe.set(user); } void TwitchIrcServer::reloadAllBTTVChannelEmotes() @@ -483,11 +1285,6 @@ void TwitchIrcServer::reloadAllBTTVChannelEmotes() }); } -void TwitchIrcServer::reloadFFZGlobalEmotes() -{ - this->ffz.loadEmotes(); -} - void TwitchIrcServer::reloadAllFFZChannelEmotes() { this->forEachChannel([](const auto &chan) { @@ -497,4 +1294,272 @@ void TwitchIrcServer::reloadAllFFZChannelEmotes() } }); } + +void TwitchIrcServer::reloadAllSevenTVChannelEmotes() +{ + this->forEachChannel([](const auto &chan) { + if (auto *channel = dynamic_cast(chan.get())) + { + channel->refreshSevenTVChannelEmotes(false); + } + }); +} + +void TwitchIrcServer::forEachSeventvEmoteSet( + const QString &emoteSetId, std::function func) +{ + this->forEachChannel([emoteSetId, func](const auto &chan) { + if (auto *channel = dynamic_cast(chan.get()); + channel->seventvEmoteSetID() == emoteSetId) + { + func(*channel); + } + }); +} +void TwitchIrcServer::forEachSeventvUser( + const QString &userId, std::function func) +{ + this->forEachChannel([userId, func](const auto &chan) { + if (auto *channel = dynamic_cast(chan.get()); + channel->seventvUserID() == userId) + { + func(*channel); + } + }); +} + +void TwitchIrcServer::dropSeventvChannel(const QString &userID, + const QString &emoteSetID) +{ + if (!getApp()->getSeventvEventAPI()) + { + return; + } + + std::lock_guard lock(this->channelMutex); + + // ignore empty values + bool skipUser = userID.isEmpty(); + bool skipSet = emoteSetID.isEmpty(); + + bool foundUser = skipUser; + bool foundSet = skipSet; + for (std::weak_ptr &weak : this->channels) + { + ChannelPtr chan = weak.lock(); + if (!chan) + { + continue; + } + + auto *channel = dynamic_cast(chan.get()); + if (!foundSet && channel->seventvEmoteSetID() == emoteSetID) + { + foundSet = true; + } + if (!foundUser && channel->seventvUserID() == userID) + { + foundUser = true; + } + + if (foundSet && foundUser) + { + break; + } + } + + if (!foundUser) + { + getApp()->getSeventvEventAPI()->unsubscribeUser(userID); + } + if (!foundSet) + { + getApp()->getSeventvEventAPI()->unsubscribeEmoteSet(emoteSetID); + } +} + +void TwitchIrcServer::markChannelsConnected() +{ + this->forEachChannel([](const ChannelPtr &chan) { + if (auto *channel = dynamic_cast(chan.get())) + { + channel->markConnected(); + } + }); +} + +void TwitchIrcServer::addFakeMessage(const QString &data) +{ + auto *fakeMessage = Communi::IrcMessage::fromData( + data.toUtf8(), this->readConnection_.get()); + + if (fakeMessage->command() == "PRIVMSG") + { + this->privateMessageReceived( + static_cast(fakeMessage)); + } + else + { + this->readConnectionMessageReceived(fakeMessage); + } +} + +void TwitchIrcServer::addGlobalSystemMessage(const QString &messageText) +{ + std::lock_guard lock(this->channelMutex); + + MessageBuilder b(systemMessage, messageText); + auto message = b.release(); + + for (std::weak_ptr &weak : this->channels.values()) + { + std::shared_ptr chan = weak.lock(); + if (!chan) + { + continue; + } + + chan->addMessage(message, MessageContext::Original); + } +} + +void TwitchIrcServer::forEachChannel(std::function func) +{ + std::lock_guard lock(this->channelMutex); + + for (std::weak_ptr &weak : this->channels.values()) + { + ChannelPtr chan = weak.lock(); + if (!chan) + { + continue; + } + + func(chan); + } +} + +void TwitchIrcServer::connect() +{ + this->disconnect(); + + this->initializeConnection(this->writeConnection_.get(), + ConnectionType::Write); + this->initializeConnection(this->readConnection_.get(), + ConnectionType::Read); +} + +void TwitchIrcServer::disconnect() +{ + std::lock_guard locker(this->connectionMutex_); + + this->readConnection_->close(); + this->writeConnection_->close(); +} + +void TwitchIrcServer::sendMessage(const QString &channelName, + const QString &message) +{ + this->sendRawMessage("PRIVMSG #" + channelName + " :" + message); +} + +void TwitchIrcServer::sendRawMessage(const QString &rawMessage) +{ + std::lock_guard locker(this->connectionMutex_); + + this->writeConnection_->sendRaw(rawMessage); +} + +ChannelPtr TwitchIrcServer::getOrAddChannel(const QString &dirtyChannelName) +{ + auto channelName = this->cleanChannelName(dirtyChannelName); + + // try get channel + ChannelPtr chan = this->getChannelOrEmpty(channelName); + if (chan != Channel::getEmpty()) + { + return chan; + } + + std::lock_guard lock(this->channelMutex); + + // value doesn't exist + chan = this->createChannel(channelName); + if (!chan) + { + return Channel::getEmpty(); + } + + this->channels.insert(channelName, chan); + this->connections_.managedConnect(chan->destroyed, [this, channelName] { + // fourtf: issues when the server itself is destroyed + + qCDebug(chatterinoIrc) << "[TwitchIrcServer::addChannel]" << channelName + << "was destroyed"; + this->channels.remove(channelName); + + if (this->readConnection_) + { + this->readConnection_->sendRaw("PART #" + channelName); + } + }); + + // join IRC channel + { + std::lock_guard lock2(this->connectionMutex_); + + if (this->readConnection_) + { + if (this->readConnection_->isConnected()) + { + this->joinBucket_->send(channelName); + } + } + } + + return chan; +} + +ChannelPtr TwitchIrcServer::getChannelOrEmpty(const QString &dirtyChannelName) +{ + auto channelName = this->cleanChannelName(dirtyChannelName); + + std::lock_guard lock(this->channelMutex); + + // try get special channel + ChannelPtr chan = this->getCustomChannel(channelName); + if (chan) + { + return chan; + } + + // value exists + auto it = this->channels.find(channelName); + if (it != this->channels.end()) + { + chan = it.value().lock(); + + if (chan) + { + return chan; + } + } + + return Channel::getEmpty(); +} + +void TwitchIrcServer::open(ConnectionType type) +{ + std::lock_guard lock(this->connectionMutex_); + + if (type == ConnectionType::Write) + { + this->writeConnection_->open(); + } + if (type == ConnectionType::Read) + { + this->readConnection_->open(); + } +} + } // namespace chatterino diff --git a/src/providers/twitch/TwitchIrcServer.hpp b/src/providers/twitch/TwitchIrcServer.hpp index 7aa4212a5..fc3b888ef 100644 --- a/src/providers/twitch/TwitchIrcServer.hpp +++ b/src/providers/twitch/TwitchIrcServer.hpp @@ -2,92 +2,206 @@ #include "common/Atomic.hpp" #include "common/Channel.hpp" -#include "common/Singleton.hpp" -#include "pajlada/signals/signalholder.hpp" -#include "providers/bttv/BttvEmotes.hpp" -#include "providers/ffz/FfzEmotes.hpp" -#include "providers/irc/AbstractIrcServer.hpp" +#include "common/Common.hpp" +#include "providers/irc/IrcConnection2.hpp" +#include "util/RatelimitBucket.hpp" + +#include +#include +#include #include +#include #include +#include #include namespace chatterino { class Settings; class Paths; -class PubSub; class TwitchChannel; +class BttvEmotes; +class FfzEmotes; +class SeventvEmotes; +class RatelimitBucket; -class TwitchIrcServer final : public AbstractIrcServer, public Singleton +class ITwitchIrcServer { public: + ITwitchIrcServer() = default; + virtual ~ITwitchIrcServer() = default; + ITwitchIrcServer(const ITwitchIrcServer &) = delete; + ITwitchIrcServer(ITwitchIrcServer &&) = delete; + ITwitchIrcServer &operator=(const ITwitchIrcServer &) = delete; + ITwitchIrcServer &operator=(ITwitchIrcServer &&) = delete; + + virtual void connect() = 0; + + virtual void sendRawMessage(const QString &rawMessage) = 0; + + virtual ChannelPtr getOrAddChannel(const QString &dirtyChannelName) = 0; + virtual ChannelPtr getChannelOrEmpty(const QString &dirtyChannelName) = 0; + + virtual void addFakeMessage(const QString &data) = 0; + + virtual void addGlobalSystemMessage(const QString &messageText) = 0; + + virtual void forEachChannel(std::function func) = 0; + + virtual void forEachChannelAndSpecialChannels( + std::function func) = 0; + + virtual std::shared_ptr getChannelOrEmptyByID( + const QString &channelID) = 0; + + virtual void dropSeventvChannel(const QString &userID, + const QString &emoteSetID) = 0; + + virtual const IndirectChannel &getWatchingChannel() const = 0; + virtual void setWatchingChannel(ChannelPtr newWatchingChannel) = 0; + virtual ChannelPtr getWhispersChannel() const = 0; + virtual ChannelPtr getMentionsChannel() const = 0; + virtual ChannelPtr getLiveChannel() const = 0; + virtual ChannelPtr getAutomodChannel() const = 0; + + virtual QString getLastUserThatWhisperedMe() const = 0; + virtual void setLastUserThatWhisperedMe(const QString &user) = 0; + + // Update this interface with TwitchIrcServer methods as needed +}; + +class TwitchIrcServer final : public ITwitchIrcServer, public QObject +{ +public: + enum class ConnectionType { + Read, + Write, + }; + TwitchIrcServer(); - virtual ~TwitchIrcServer() override = default; + ~TwitchIrcServer() override = default; - virtual void initialize(Settings &settings, Paths &paths) override; + TwitchIrcServer(const TwitchIrcServer &) = delete; + TwitchIrcServer(TwitchIrcServer &&) = delete; + TwitchIrcServer &operator=(const TwitchIrcServer &) = delete; + TwitchIrcServer &operator=(TwitchIrcServer &&) = delete; - void forEachChannelAndSpecialChannels(std::function func); + void initialize(); - std::shared_ptr getChannelOrEmptyByID(const QString &channelID); + void forEachChannelAndSpecialChannels( + std::function func) override; - void bulkRefreshLiveStatus(); + std::shared_ptr getChannelOrEmptyByID( + const QString &channelID) override; - void reloadBTTVGlobalEmotes(); void reloadAllBTTVChannelEmotes(); - void reloadFFZGlobalEmotes(); void reloadAllFFZChannelEmotes(); + void reloadAllSevenTVChannelEmotes(); + /** Calls `func` with all twitch channels that have `emoteSetId` added. */ + void forEachSeventvEmoteSet(const QString &emoteSetId, + std::function func); + /** Calls `func` with all twitch channels where the seventv-user-id is `userId`. */ + void forEachSeventvUser(const QString &userId, + std::function func); + /** + * Checks if any channel still needs this `userID` or `emoteSetID`. + * If not, it unsubscribes from the respective messages. + * + * It's currently not possible to share emote sets among users, + * but it's a commonly requested feature. + */ + void dropSeventvChannel(const QString &userID, + const QString &emoteSetID) override; + + void addFakeMessage(const QString &data) override; + + void addGlobalSystemMessage(const QString &messageText) override; + + // iteration + void forEachChannel(std::function func) override; + + void connect() override; + void disconnect(); + + void sendMessage(const QString &channelName, const QString &message); + void sendRawMessage(const QString &rawMessage) override; + + ChannelPtr getOrAddChannel(const QString &dirtyChannelName) override; + + ChannelPtr getChannelOrEmpty(const QString &dirtyChannelName) override; + + void open(ConnectionType type); + +private: Atomic lastUserThatWhisperedMe; const ChannelPtr whispersChannel; const ChannelPtr mentionsChannel; const ChannelPtr liveChannel; + const ChannelPtr automodChannel; IndirectChannel watchingChannel; - PubSub *pubsub; +public: + const IndirectChannel &getWatchingChannel() const override; + void setWatchingChannel(ChannelPtr newWatchingChannel) override; + ChannelPtr getWhispersChannel() const override; + ChannelPtr getMentionsChannel() const override; + ChannelPtr getLiveChannel() const override; + ChannelPtr getAutomodChannel() const override; - const BttvEmotes &getBttvEmotes() const; - const FfzEmotes &getFfzEmotes() const; + QString getLastUserThatWhisperedMe() const override; + void setLastUserThatWhisperedMe(const QString &user) override; protected: - virtual void initializeConnection(IrcConnection *connection, - ConnectionType type) override; - virtual std::shared_ptr createChannel( - const QString &channelName) override; + void initializeConnection(IrcConnection *connection, ConnectionType type); + std::shared_ptr createChannel(const QString &channelName); - virtual void privateMessageReceived( - Communi::IrcPrivateMessage *message) override; - virtual void readConnectionMessageReceived( - Communi::IrcMessage *message) override; - virtual void writeConnectionMessageReceived( - Communi::IrcMessage *message) override; + void privateMessageReceived(Communi::IrcPrivateMessage *message); + void readConnectionMessageReceived(Communi::IrcMessage *message); + void writeConnectionMessageReceived(Communi::IrcMessage *message); - virtual std::shared_ptr getCustomChannel( - const QString &channelname) override; + void onReadConnected(IrcConnection *connection); + void onWriteConnected(IrcConnection *connection); + void onDisconnected(); + void markChannelsConnected(); - virtual QString cleanChannelName(const QString &dirtyChannelName) override; - virtual bool hasSeparateWriteConnection() const override; + std::shared_ptr getCustomChannel(const QString &channelname); + + QString cleanChannelName(const QString &dirtyChannelName); private: - void onMessageSendRequested(TwitchChannel *channel, const QString &message, - bool &sent); - void onReplySendRequested(TwitchChannel *channel, const QString &message, - const QString &replyId, bool &sent); + void onMessageSendRequested(const std::shared_ptr &channel, + const QString &message, bool &sent); + void onReplySendRequested(const std::shared_ptr &channel, + const QString &message, const QString &replyId, + bool &sent); - bool prepareToSend(TwitchChannel *channel); + bool prepareToSend(const std::shared_ptr &channel); + + QMap> channels; + std::mutex channelMutex; + + QObjectPtr writeConnection_ = nullptr; + QObjectPtr readConnection_ = nullptr; + + // Our rate limiting bucket for the Twitch join rate limits + // https://dev.twitch.tv/docs/irc/guide#rate-limits + QObjectPtr joinBucket_; + + QTimer reconnectTimer_; + int falloffCounter_ = 1; + + std::mutex connectionMutex_; + + pajlada::Signals::SignalHolder connections_; std::mutex lastMessageMutex_; std::queue lastMessagePleb_; std::queue lastMessageMod_; std::chrono::steady_clock::time_point lastErrorTimeSpeed_; std::chrono::steady_clock::time_point lastErrorTimeAmount_; - - BttvEmotes bttv; - FfzEmotes ffz; - QTimer bulkLiveStatusTimer_; - - pajlada::Signals::SignalHolder signalHolder_; }; } // namespace chatterino diff --git a/src/providers/twitch/TwitchMessageBuilder.cpp b/src/providers/twitch/TwitchMessageBuilder.cpp deleted file mode 100644 index b2eb7488d..000000000 --- a/src/providers/twitch/TwitchMessageBuilder.cpp +++ /dev/null @@ -1,1582 +0,0 @@ -#include "providers/twitch/TwitchMessageBuilder.hpp" - -#include "Application.hpp" -#include "controllers/accounts/AccountController.hpp" -#include "controllers/ignores/IgnoreController.hpp" -#include "controllers/ignores/IgnorePhrase.hpp" -#include "messages/Message.hpp" -#include "providers/chatterino/ChatterinoBadges.hpp" -#include "providers/ffz/FfzBadges.hpp" -#include "providers/twitch/TwitchBadge.hpp" -#include "providers/twitch/TwitchBadges.hpp" -#include "providers/twitch/TwitchChannel.hpp" -#include "providers/twitch/TwitchIrcServer.hpp" -#include "singletons/Emotes.hpp" -#include "singletons/Resources.hpp" -#include "singletons/Settings.hpp" -#include "singletons/Theme.hpp" -#include "singletons/WindowManager.hpp" -#include "util/Helpers.hpp" -#include "util/IrcHelpers.hpp" -#include "util/Qt.hpp" -#include "widgets/Window.hpp" - -#include -#include -#include -#include -#include -#include -#include "common/QLogging.hpp" - -namespace { - -const QString regexHelpString("(\\w+)[.,!?;:]*?$"); - -// matches a mention with punctuation at the end, like "@username," or "@username!!!" where capture group would return "username" -const QRegularExpression mentionRegex("^@" + regexHelpString); - -// if findAllUsernames setting is enabled, matches strings like in the examples above, but without @ symbol at the beginning -const QRegularExpression allUsernamesMentionRegex("^" + regexHelpString); - -const QSet zeroWidthEmotes{ - "SoSnowy", "IceCold", "SantaHat", "TopHat", - "ReinDeer", "CandyCane", "cvMask", "cvHazmat", -}; - -} // namespace - -namespace chatterino { - -namespace { - - QString stylizeUsername(const QString &username, const Message &message) - { - auto app = getApp(); - - const QString &localizedName = message.localizedName; - bool hasLocalizedName = !localizedName.isEmpty(); - - // The full string that will be rendered in the chat widget - QString usernameText; - - switch (getSettings()->usernameDisplayMode.getValue()) - { - case UsernameDisplayMode::Username: { - usernameText = username; - } - break; - - case UsernameDisplayMode::LocalizedName: { - if (hasLocalizedName) - { - usernameText = localizedName; - } - else - { - usernameText = username; - } - } - break; - - default: - case UsernameDisplayMode::UsernameAndLocalizedName: { - if (hasLocalizedName) - { - usernameText = username + "(" + localizedName + ")"; - } - else - { - usernameText = username; - } - } - break; - } - - auto nicknames = getCSettings().nicknames.readOnly(); - - for (const auto &nickname : *nicknames) - { - if (nickname.match(usernameText)) - { - break; - } - } - - return usernameText; - } - -} // namespace - -TwitchMessageBuilder::TwitchMessageBuilder( - Channel *_channel, const Communi::IrcPrivateMessage *_ircMessage, - const MessageParseArgs &_args) - : SharedMessageBuilder(_channel, _ircMessage, _args) - , twitchChannel(dynamic_cast(_channel)) -{ -} - -TwitchMessageBuilder::TwitchMessageBuilder( - Channel *_channel, const Communi::IrcMessage *_ircMessage, - const MessageParseArgs &_args, QString content, bool isAction) - : SharedMessageBuilder(_channel, _ircMessage, _args, content, isAction) - , twitchChannel(dynamic_cast(_channel)) -{ -} - -bool TwitchMessageBuilder::isIgnored() const -{ - return isIgnoredMessage({ - /*.message = */ this->originalMessage_, - /*.twitchUserID = */ this->tags.value("user-id").toString(), - /*.isMod = */ this->channel->isMod(), - /*.isBroadcaster = */ this->channel->isBroadcaster(), - }); -} - -void TwitchMessageBuilder::triggerHighlights() -{ - if (this->historicalMessage_) - { - // Do nothing. Highlights should not be triggered on historical messages. - return; - } - - SharedMessageBuilder::triggerHighlights(); -} - -MessagePtr TwitchMessageBuilder::build() -{ - // PARSE - this->userId_ = this->ircMessage->tag("user-id").toString(); - - this->parse(); - - if (this->userName == this->channel->getName()) - { - this->senderIsBroadcaster = true; - } - - this->message().channelName = this->channel->getName(); - - this->parseMessageID(); - - this->parseRoomID(); - - // If it is a reward it has to be appended first - if (this->args.channelPointRewardId != "") - { - const auto &reward = this->twitchChannel->channelPointReward( - this->args.channelPointRewardId); - if (reward) - { - this->appendChannelPointRewardMessage( - reward.get(), this, this->channel->isMod(), - this->channel->isBroadcaster()); - } - } - - this->appendChannelName(); - - if (this->tags.contains("rm-deleted")) - { - this->message().flags.set(MessageFlag::Disabled); - } - - this->historicalMessage_ = this->tags.contains("historical"); - - if (this->tags.contains("msg-id") && - this->tags["msg-id"].toString().split(';').contains( - "highlighted-message")) - { - this->message().flags.set(MessageFlag::RedeemedHighlight); - } - - if (this->tags.contains("first-msg") && - this->tags["first-msg"].toString() == "1") - { - this->message().flags.set(MessageFlag::FirstMessage); - } - - // reply threads - if (this->thread_) - { - // set references - this->message().replyThread = this->thread_; - this->thread_->addToThread(this->weakOf()); - - // enable reply flag - this->message().flags.set(MessageFlag::ReplyMessage); - - const auto &threadRoot = this->thread_->root(); - - QString usernameText = - stylizeUsername(threadRoot->loginName, *threadRoot.get()); - - this->emplace(); - - // construct reply elements - this->emplace( - "Replying to", MessageElementFlag::RepliedMessage, - MessageColor::System, FontStyle::ChatMediumSmall) - ->setLink({Link::ViewThread, this->thread_->rootId()}); - - this->emplace( - "@" + usernameText + ":", MessageElementFlag::RepliedMessage, - threadRoot->usernameColor, FontStyle::ChatMediumSmall) - ->setLink({Link::UserInfo, threadRoot->displayName}); - - this->emplace( - threadRoot->messageText, MessageElementFlag::RepliedMessage, - this->textColor_, FontStyle::ChatMediumSmall) - ->setLink({Link::ViewThread, this->thread_->rootId()}); - } - else if (this->tags.find("reply-parent-msg-id") != this->tags.end()) - { - // Message is a reply but we couldn't find the original message. - // Render the message using the additional reply tags - - auto replyDisplayName = this->tags.find("reply-parent-display-name"); - auto replyBody = this->tags.find("reply-parent-msg-body"); - - if (replyDisplayName != this->tags.end() && - replyBody != this->tags.end()) - { - auto name = replyDisplayName->toString(); - auto body = parseTagString(replyBody->toString()); - - this->emplace(); - - this->emplace( - "Replying to", MessageElementFlag::RepliedMessage, - MessageColor::System, FontStyle::ChatMediumSmall); - - this->emplace( - "@" + name + ":", MessageElementFlag::RepliedMessage, - this->textColor_, FontStyle::ChatMediumSmall) - ->setLink({Link::UserInfo, name}); - - this->emplace( - body, MessageElementFlag::RepliedMessage, this->textColor_, - FontStyle::ChatMediumSmall); - } - } - - // timestamp - this->message().serverReceivedTime = calculateMessageTime(this->ircMessage); - this->emplace(this->message().serverReceivedTime.time()); - - if (this->shouldAddModerationElements()) - { - this->emplace(); - } - - this->appendTwitchBadges(); - - this->appendChatterinoBadges(); - this->appendFfzBadges(); - - this->appendUsername(); - - // QString bits; - auto iterator = this->tags.find("bits"); - if (iterator != this->tags.end()) - { - this->hasBits_ = true; - this->bitsLeft = iterator.value().toInt(); - this->bits = iterator.value().toString(); - } - - // Twitch emotes - std::vector twitchEmotes; - - iterator = this->tags.find("emotes"); - if (iterator != this->tags.end()) - { - QStringList emoteString = iterator.value().toString().split('/'); - std::vector correctPositions; - for (int i = 0; i < this->originalMessage_.size(); ++i) - { - if (!this->originalMessage_.at(i).isLowSurrogate()) - { - correctPositions.push_back(i); - } - } - for (QString emote : emoteString) - { - this->appendTwitchEmote(emote, twitchEmotes, correctPositions); - } - } - - // This runs through all ignored phrases and runs its replacements on this->originalMessage_ - this->runIgnoreReplaces(twitchEmotes); - - std::sort(twitchEmotes.begin(), twitchEmotes.end(), - [](const auto &a, const auto &b) { - return a.start < b.start; - }); - twitchEmotes.erase(std::unique(twitchEmotes.begin(), twitchEmotes.end(), - [](const auto &first, const auto &second) { - return first.start == second.start; - }), - twitchEmotes.end()); - - // words - QStringList splits = this->originalMessage_.split(' '); - - this->addWords(splits, twitchEmotes); - - this->message().messageText = this->originalMessage_; - this->message().searchText = this->message().localizedName + " " + - this->userName + ": " + this->originalMessage_; - - // highlights - this->parseHighlights(); - - // highlighting incoming whispers if requested per setting - if (this->args.isReceivedWhisper && getSettings()->highlightInlineWhispers) - { - this->message().flags.set(MessageFlag::HighlightedWhisper, true); - this->message().highlightColor = - ColorProvider::instance().color(ColorType::Whisper); - } - - if (this->thread_) - { - auto &img = getResources().buttons.replyThreadDark; - this->emplace(Image::fromPixmap(img, 0.15), 2, - Qt::gray, - MessageElementFlag::ReplyButton) - ->setLink({Link::ViewThread, this->thread_->rootId()}); - } - else - { - auto &img = getResources().buttons.replyDark; - this->emplace(Image::fromPixmap(img, 0.15), 2, - Qt::gray, - MessageElementFlag::ReplyButton) - ->setLink({Link::ReplyToMessage, this->message().id}); - } - - return this->release(); -} - -bool doesWordContainATwitchEmote( - int cursor, const QString &word, - const std::vector &twitchEmotes, - std::vector::const_iterator ¤tTwitchEmoteIt) -{ - if (currentTwitchEmoteIt == twitchEmotes.end()) - { - // No emote to add! - return false; - } - - const auto ¤tTwitchEmote = *currentTwitchEmoteIt; - - auto wordEnd = cursor + word.length(); - - // Check if this emote fits within the word boundaries - if (currentTwitchEmote.start < cursor || currentTwitchEmote.end > wordEnd) - { - // this emote does not fit xd - return false; - } - - return true; -} - -void TwitchMessageBuilder::addWords( - const QStringList &words, - const std::vector &twitchEmotes) -{ - // cursor currently indicates what character index we're currently operating in the full list of words - int cursor = 0; - auto currentTwitchEmoteIt = twitchEmotes.begin(); - - for (auto word : words) - { - if (word.isEmpty()) - { - cursor++; - continue; - } - - while (doesWordContainATwitchEmote(cursor, word, twitchEmotes, - currentTwitchEmoteIt)) - { - const auto ¤tTwitchEmote = *currentTwitchEmoteIt; - - if (currentTwitchEmote.start == cursor) - { - // This emote exists right at the start of the word! - this->emplace(currentTwitchEmote.ptr, - MessageElementFlag::TwitchEmote, - this->textColor_); - - auto len = currentTwitchEmote.name.string.length(); - cursor += len; - word = word.mid(len); - - ++currentTwitchEmoteIt; - - if (word.isEmpty()) - { - // space - cursor += 1; - break; - } - else - { - this->message().elements.back()->setTrailingSpace(false); - } - - continue; - } - - // Emote is not at the start - - // 1. Add text before the emote - QString preText = word.left(currentTwitchEmote.start - cursor); - for (auto &variant : getApp()->emotes->emojis.parse(preText)) - { - boost::apply_visitor( - [&](auto &&arg) { - this->addTextOrEmoji(arg); - }, - variant); - } - - cursor += preText.size(); - - word = word.mid(preText.size()); - } - - if (word.isEmpty()) - { - continue; - } - - // split words - for (auto &variant : getApp()->emotes->emojis.parse(word)) - { - boost::apply_visitor( - [&](auto &&arg) { - this->addTextOrEmoji(arg); - }, - variant); - } - - cursor += word.size() + 1; - } -} - -void TwitchMessageBuilder::addTextOrEmoji(EmotePtr emote) -{ - return SharedMessageBuilder::addTextOrEmoji(emote); -} - -void TwitchMessageBuilder::addTextOrEmoji(const QString &string_) -{ - auto string = QString(string_); - - if (this->hasBits_ && this->tryParseCheermote(string)) - { - // This string was parsed as a cheermote - return; - } - - // TODO: Implement ignored emotes - // Format of ignored emotes: - // Emote name: "forsenPuke" - if string in ignoredEmotes - // Will match emote regardless of source (i.e. bttv, ffz) - // Emote source + name: "bttv:nyanPls" - if (this->tryAppendEmote({string})) - { - // Successfully appended an emote - return; - } - - // Actually just text - auto linkString = this->matchLink(string); - auto textColor = this->textColor_; - - if (!linkString.isEmpty()) - { - this->addLink(string, linkString); - return; - } - - if (string.startsWith('@')) - { - auto match = mentionRegex.match(string); - // Only treat as @mention if valid username - if (match.hasMatch()) - { - QString username = match.captured(1); - auto originalTextColor = textColor; - - if (this->twitchChannel != nullptr && getSettings()->colorUsernames) - { - if (auto userColor = - this->twitchChannel->getUserColor(username); - userColor.isValid()) - { - textColor = userColor; - } - } - - auto prefixedUsername = '@' + username; - this->emplace(prefixedUsername, - MessageElementFlag::BoldUsername, - textColor, FontStyle::ChatMediumBold) - ->setLink({Link::UserInfo, username}) - ->setTrailingSpace(false); - - this->emplace(prefixedUsername, - MessageElementFlag::NonBoldUsername, - textColor) - ->setLink({Link::UserInfo, username}) - ->setTrailingSpace(false); - - this->emplace(string.remove(prefixedUsername), - MessageElementFlag::Text, - originalTextColor); - - return; - } - } - - if (this->twitchChannel != nullptr && getSettings()->findAllUsernames) - { - auto match = allUsernamesMentionRegex.match(string); - QString username = match.captured(1); - - if (match.hasMatch() && - this->twitchChannel->accessChatters()->contains(username)) - { - auto originalTextColor = textColor; - - if (getSettings()->colorUsernames) - { - if (auto userColor = - this->twitchChannel->getUserColor(username); - userColor.isValid()) - { - textColor = userColor; - } - } - - this->emplace(username, - MessageElementFlag::BoldUsername, - textColor, FontStyle::ChatMediumBold) - ->setLink({Link::UserInfo, username}) - ->setTrailingSpace(false); - - this->emplace( - username, MessageElementFlag::NonBoldUsername, textColor) - ->setLink({Link::UserInfo, username}) - ->setTrailingSpace(false); - - this->emplace(string.remove(username), - MessageElementFlag::Text, - originalTextColor); - - return; - } - } - - this->emplace(string, MessageElementFlag::Text, textColor); -} - -void TwitchMessageBuilder::parseMessageID() -{ - auto iterator = this->tags.find("id"); - - if (iterator != this->tags.end()) - { - this->message().id = iterator.value().toString(); - } -} - -void TwitchMessageBuilder::parseRoomID() -{ - if (this->twitchChannel == nullptr) - { - return; - } - - auto iterator = this->tags.find("room-id"); - - if (iterator != std::end(this->tags)) - { - this->roomID_ = iterator.value().toString(); - - if (this->twitchChannel->roomId().isEmpty()) - { - this->twitchChannel->setRoomId(this->roomID_); - } - } -} - -void TwitchMessageBuilder::parseUsernameColor() -{ - const auto iterator = this->tags.find("color"); - if (iterator != this->tags.end()) - { - if (const auto color = iterator.value().toString(); !color.isEmpty()) - { - this->usernameColor_ = QColor(color); - this->message().usernameColor = this->usernameColor_; - return; - } - } - - if (getSettings()->colorizeNicknames && this->tags.contains("user-id")) - { - this->usernameColor_ = - getRandomColor(this->tags.value("user-id").toString()); - this->message().usernameColor = this->usernameColor_; - } -} - -void TwitchMessageBuilder::parseUsername() -{ - SharedMessageBuilder::parseUsername(); - - if (this->userName.isEmpty() || this->args.trimSubscriberUsername) - { - this->userName = this->tags.value(QLatin1String("login")).toString(); - } - - // display name - // auto displayNameVariant = this->tags.value("display-name"); - // if (displayNameVariant.isValid()) { - // this->userName = displayNameVariant.toString() + " (" + - // this->userName + ")"; - // } - - this->message().loginName = this->userName; - if (this->twitchChannel != nullptr) - { - this->twitchChannel->setUserColor(this->userName, this->usernameColor_); - } - - // Update current user color if this is our message - auto currentUser = getApp()->accounts->twitch.getCurrent(); - if (this->ircMessage->nick() == currentUser->getUserName()) - { - currentUser->setColor(this->usernameColor_); - } -} - -void TwitchMessageBuilder::appendUsername() -{ - auto app = getApp(); - - QString username = this->userName; - this->message().loginName = username; - QString localizedName; - - auto iterator = this->tags.find("display-name"); - if (iterator != this->tags.end()) - { - QString displayName = - parseTagString(iterator.value().toString()).trimmed(); - - if (QString::compare(displayName, this->userName, - Qt::CaseInsensitive) == 0) - { - username = displayName; - - this->message().displayName = displayName; - } - else - { - localizedName = displayName; - - this->message().displayName = username; - this->message().localizedName = displayName; - } - } - - QString usernameText = stylizeUsername(username, this->message()); - - if (this->args.isSentWhisper) - { - // TODO(pajlada): Re-implement - // userDisplayString += - // IrcManager::instance().getUser().getUserName(); - } - else if (this->args.isReceivedWhisper) - { - // Sender username - this->emplace(usernameText, MessageElementFlag::Username, - this->usernameColor_, - FontStyle::ChatMediumBold) - ->setLink({Link::UserWhisper, this->message().displayName}); - - auto currentUser = app->accounts->twitch.getCurrent(); - - // Separator - this->emplace("->", MessageElementFlag::Username, - MessageColor::System, FontStyle::ChatMedium); - - QColor selfColor = currentUser->color(); - MessageColor selfMsgColor = - selfColor.isValid() ? selfColor : MessageColor::System; - - // Your own username - this->emplace(currentUser->getUserName() + ":", - MessageElementFlag::Username, selfMsgColor, - FontStyle::ChatMediumBold); - } - else - { - if (!this->action_) - { - usernameText += ":"; - } - - this->emplace(usernameText, MessageElementFlag::Username, - this->usernameColor_, - FontStyle::ChatMediumBold) - ->setLink({Link::UserInfo, this->message().displayName}); - } -} - -void TwitchMessageBuilder::runIgnoreReplaces( - std::vector &twitchEmotes) -{ - auto phrases = getCSettings().ignoredMessages.readOnly(); - auto removeEmotesInRange = [](int pos, int len, - auto &twitchEmotes) mutable { - auto it = std::partition( - twitchEmotes.begin(), twitchEmotes.end(), - [pos, len](const auto &item) { - return !((item.start >= pos) && item.start < (pos + len)); - }); - for (auto copy = it; copy != twitchEmotes.end(); ++copy) - { - if ((*copy).ptr == nullptr) - { - qCDebug(chatterinoTwitch) - << "remem nullptr" << (*copy).name.string; - } - } - std::vector v(it, twitchEmotes.end()); - twitchEmotes.erase(it, twitchEmotes.end()); - return v; - }; - - auto shiftIndicesAfter = [&twitchEmotes](int pos, int by) mutable { - for (auto &item : twitchEmotes) - { - auto &index = item.start; - if (index >= pos) - { - index += by; - item.end += by; - } - } - }; - - auto addReplEmotes = [&twitchEmotes](const IgnorePhrase &phrase, - const QStringRef &midrepl, - int startIndex) mutable { - if (!phrase.containsEmote()) - { - return; - } - - QVector words = midrepl.split(' '); - int pos = 0; - for (const auto &word : words) - { - for (const auto &emote : phrase.getEmotes()) - { - if (word == emote.first.string) - { - if (emote.second == nullptr) - { - qCDebug(chatterinoTwitch) - << "emote null" << emote.first.string; - } - twitchEmotes.push_back(TwitchEmoteOccurence{ - startIndex + pos, - startIndex + pos + emote.first.string.length(), - emote.second, - emote.first, - }); - } - } - pos += word.length() + 1; - } - }; - - for (const auto &phrase : *phrases) - { - if (phrase.isBlock()) - { - continue; - } - const auto &pattern = phrase.getPattern(); - if (pattern.isEmpty()) - { - continue; - } - if (phrase.isRegex()) - { - const auto ®ex = phrase.getRegex(); - if (!regex.isValid()) - { - continue; - } - QRegularExpressionMatch match; - int from = 0; - while ((from = this->originalMessage_.indexOf(regex, from, - &match)) != -1) - { - int len = match.capturedLength(); - auto vret = removeEmotesInRange(from, len, twitchEmotes); - auto mid = this->originalMessage_.mid(from, len); - mid.replace(regex, phrase.getReplace()); - - int midsize = mid.size(); - this->originalMessage_.replace(from, len, mid); - int pos1 = from; - while (pos1 > 0) - { - if (this->originalMessage_[pos1 - 1] == ' ') - { - break; - } - --pos1; - } - int pos2 = from + midsize; - while (pos2 < this->originalMessage_.length()) - { - if (this->originalMessage_[pos2] == ' ') - { - break; - } - ++pos2; - } - - shiftIndicesAfter(from + len, midsize - len); - - auto midExtendedRef = - this->originalMessage_.midRef(pos1, pos2 - pos1); - - for (auto &tup : vret) - { - if (tup.ptr == nullptr) - { - qCDebug(chatterinoTwitch) - << "v nullptr" << tup.name.string; - continue; - } - QRegularExpression emoteregex( - "\\b" + tup.name.string + "\\b", - QRegularExpression::UseUnicodePropertiesOption); - auto _match = emoteregex.match(midExtendedRef); - if (_match.hasMatch()) - { - int last = _match.lastCapturedIndex(); - for (int i = 0; i <= last; ++i) - { - tup.start = from + _match.capturedStart(); - twitchEmotes.push_back(std::move(tup)); - } - } - } - - addReplEmotes(phrase, midExtendedRef, pos1); - - from += midsize; - } - } - else - { - int from = 0; - while ((from = this->originalMessage_.indexOf( - pattern, from, phrase.caseSensitivity())) != -1) - { - int len = pattern.size(); - auto vret = removeEmotesInRange(from, len, twitchEmotes); - auto replace = phrase.getReplace(); - - int replacesize = replace.size(); - this->originalMessage_.replace(from, len, replace); - - int pos1 = from; - while (pos1 > 0) - { - if (this->originalMessage_[pos1 - 1] == ' ') - { - break; - } - --pos1; - } - int pos2 = from + replacesize; - while (pos2 < this->originalMessage_.length()) - { - if (this->originalMessage_[pos2] == ' ') - { - break; - } - ++pos2; - } - - shiftIndicesAfter(from + len, replacesize - len); - - auto midExtendedRef = - this->originalMessage_.midRef(pos1, pos2 - pos1); - - for (auto &tup : vret) - { - if (tup.ptr == nullptr) - { - qCDebug(chatterinoTwitch) - << "v nullptr" << tup.name.string; - continue; - } - QRegularExpression emoteregex( - "\\b" + tup.name.string + "\\b", - QRegularExpression::UseUnicodePropertiesOption); - auto match = emoteregex.match(midExtendedRef); - if (match.hasMatch()) - { - int last = match.lastCapturedIndex(); - for (int i = 0; i <= last; ++i) - { - tup.start = from + match.capturedStart(); - twitchEmotes.push_back(std::move(tup)); - } - } - } - - addReplEmotes(phrase, midExtendedRef, pos1); - - from += replacesize; - } - } - } -} - -void TwitchMessageBuilder::appendTwitchEmote( - const QString &emote, std::vector &vec, - std::vector &correctPositions) -{ - auto app = getApp(); - if (!emote.contains(':')) - { - return; - } - - auto parameters = emote.split(':'); - - if (parameters.length() < 2) - { - return; - } - - auto id = EmoteId{parameters.at(0)}; - - auto occurences = parameters.at(1).split(','); - - for (QString occurence : occurences) - { - auto coords = occurence.split('-'); - - if (coords.length() < 2) - { - return; - } - - auto start = correctPositions[coords.at(0).toUInt()]; - auto end = correctPositions[coords.at(1).toUInt()]; - - if (start >= end || start < 0 || end > this->originalMessage_.length()) - { - return; - } - - auto name = - EmoteName{this->originalMessage_.mid(start, end - start + 1)}; - TwitchEmoteOccurence emoteOccurence{ - start, end, app->emotes->twitch.getOrCreateEmote(id, name), name}; - if (emoteOccurence.ptr == nullptr) - { - qCDebug(chatterinoTwitch) - << "nullptr" << emoteOccurence.name.string; - } - vec.push_back(std::move(emoteOccurence)); - } -} - -Outcome TwitchMessageBuilder::tryAppendEmote(const EmoteName &name) -{ - auto *app = getApp(); - - const auto &globalBttvEmotes = app->twitch->getBttvEmotes(); - const auto &globalFfzEmotes = app->twitch->getFfzEmotes(); - - auto flags = MessageElementFlags(); - auto emote = boost::optional{}; - - // Emote order: - // - FrankerFaceZ Channel - // - BetterTTV Channel - // - FrankerFaceZ Global - // - BetterTTV Global - if (this->twitchChannel && (emote = this->twitchChannel->ffzEmote(name))) - { - flags = MessageElementFlag::FfzEmote; - } - else if (this->twitchChannel && - (emote = this->twitchChannel->bttvEmote(name))) - { - flags = MessageElementFlag::BttvEmote; - } - else if ((emote = globalFfzEmotes.emote(name))) - { - flags = MessageElementFlag::FfzEmote; - } - else if ((emote = globalBttvEmotes.emote(name))) - { - flags = MessageElementFlag::BttvEmote; - - if (zeroWidthEmotes.contains(name.string)) - { - flags.set(MessageElementFlag::ZeroWidthEmote); - } - } - - if (emote) - { - this->emplace(emote.get(), flags, this->textColor_); - return Success; - } - - return Failure; -} - -boost::optional TwitchMessageBuilder::getTwitchBadge( - const Badge &badge) -{ - if (auto channelBadge = - this->twitchChannel->twitchBadge(badge.key_, badge.value_)) - { - return channelBadge; - } - - if (auto globalBadge = - TwitchBadges::instance()->badge(badge.key_, badge.value_)) - { - return globalBadge; - } - - return boost::none; -} - -std::unordered_map TwitchMessageBuilder::parseBadgeInfoTag( - const QVariantMap &tags) -{ - std::unordered_map infoMap; - - auto infoIt = tags.constFind("badge-info"); - if (infoIt == tags.end()) - return infoMap; - - auto info = infoIt.value().toString().split(',', Qt::SkipEmptyParts); - - for (const QString &badge : info) - { - infoMap.emplace(SharedMessageBuilder::slashKeyValue(badge)); - } - - return infoMap; -} - -void TwitchMessageBuilder::appendTwitchBadges() -{ - if (this->twitchChannel == nullptr) - { - return; - } - - auto badgeInfos = TwitchMessageBuilder::parseBadgeInfoTag(this->tags); - auto badges = this->parseBadgeTag(this->tags); - - for (const auto &badge : badges) - { - auto badgeEmote = this->getTwitchBadge(badge); - if (!badgeEmote) - { - continue; - } - auto tooltip = (*badgeEmote)->tooltip.string; - - if (badge.key_ == "bits") - { - const auto &cheerAmount = badge.value_; - tooltip = QString("Twitch cheer %0").arg(cheerAmount); - } - else if (badge.key_ == "moderator" && - getSettings()->useCustomFfzModeratorBadges) - { - if (auto customModBadge = this->twitchChannel->ffzCustomModBadge()) - { - this->emplace( - customModBadge.get(), - MessageElementFlag::BadgeChannelAuthority) - ->setTooltip((*customModBadge)->tooltip.string); - // early out, since we have to add a custom badge element here - continue; - } - } - else if (badge.key_ == "vip" && getSettings()->useCustomFfzVipBadges) - { - if (auto customVipBadge = this->twitchChannel->ffzCustomVipBadge()) - { - this->emplace( - customVipBadge.get(), - MessageElementFlag::BadgeChannelAuthority) - ->setTooltip((*customVipBadge)->tooltip.string); - // early out, since we have to add a custom badge element here - continue; - } - } - else if (badge.flag_ == MessageElementFlag::BadgeSubscription) - { - auto badgeInfoIt = badgeInfos.find(badge.key_); - if (badgeInfoIt != badgeInfos.end()) - { - // badge.value_ is 4 chars long if user is subbed on higher tier - // (tier + amount of months with leading zero if less than 100) - // e.g. 3054 - tier 3 4,5-year sub. 2108 - tier 2 9-year sub - const auto &subTier = - badge.value_.length() > 3 ? badge.value_.at(0) : '1'; - const auto &subMonths = badgeInfoIt->second; - tooltip += - QString(" (%1%2 months)") - .arg(subTier != '1' ? QString("Tier %1, ").arg(subTier) - : "") - .arg(subMonths); - } - } - else if (badge.flag_ == MessageElementFlag::BadgePredictions) - { - auto badgeInfoIt = badgeInfos.find(badge.key_); - if (badgeInfoIt != badgeInfos.end()) - { - auto predictionText = - badgeInfoIt->second - .replace(R"(\s)", " ") // standard IRC escapes - .replace(R"(\:)", ";") - .replace(R"(\\)", R"(\)") - .replace("⸝", ","); // twitch's comma escape - // Careful, the first character is RIGHT LOW PARAPHRASE BRACKET or U+2E1D, which just looks like a comma - - tooltip = QString("Predicted %1").arg(predictionText); - } - } - - this->emplace(badgeEmote.get(), badge.flag_) - ->setTooltip(tooltip); - } - - this->message().badges = badges; - this->message().badgeInfos = badgeInfos; -} - -void TwitchMessageBuilder::appendChatterinoBadges() -{ - if (auto badge = getApp()->chatterinoBadges->getBadge({this->userId_})) - { - this->emplace(*badge, - MessageElementFlag::BadgeChatterino); - } -} - -void TwitchMessageBuilder::appendFfzBadges() -{ - for (const auto &badge : - getApp()->ffzBadges->getUserBadges({this->userId_})) - { - this->emplace( - badge.emote, MessageElementFlag::BadgeFfz, badge.color); - } -} - -Outcome TwitchMessageBuilder::tryParseCheermote(const QString &string) -{ - if (this->bitsLeft == 0) - { - return Failure; - } - - auto cheerOpt = this->twitchChannel->cheerEmote(string); - - if (!cheerOpt) - { - return Failure; - } - - auto &cheerEmote = *cheerOpt; - auto match = cheerEmote.regex.match(string); - - if (!match.hasMatch()) - { - return Failure; - } - - int cheerValue = match.captured(1).toInt(); - - if (getSettings()->stackBits) - { - if (this->bitsStacked) - { - return Success; - } - if (cheerEmote.staticEmote) - { - this->emplace(cheerEmote.staticEmote, - MessageElementFlag::BitsStatic, - this->textColor_); - } - if (cheerEmote.animatedEmote) - { - this->emplace(cheerEmote.animatedEmote, - MessageElementFlag::BitsAnimated, - this->textColor_); - } - if (cheerEmote.color != QColor()) - { - this->emplace(QString::number(this->bitsLeft), - MessageElementFlag::BitsAmount, - cheerEmote.color); - } - this->bitsStacked = true; - return Success; - } - - if (this->bitsLeft >= cheerValue) - { - this->bitsLeft -= cheerValue; - } - else - { - QString newString = string; - newString.chop(QString::number(cheerValue).length()); - newString += QString::number(cheerValue - this->bitsLeft); - - return tryParseCheermote(newString); - } - - if (cheerEmote.staticEmote) - { - this->emplace(cheerEmote.staticEmote, - MessageElementFlag::BitsStatic, - this->textColor_); - } - if (cheerEmote.animatedEmote) - { - this->emplace(cheerEmote.animatedEmote, - MessageElementFlag::BitsAnimated, - this->textColor_); - } - if (cheerEmote.color != QColor()) - { - this->emplace(match.captured(1), - MessageElementFlag::BitsAmount, - cheerEmote.color); - } - - return Success; -} - -bool TwitchMessageBuilder::shouldAddModerationElements() const -{ - if (this->senderIsBroadcaster) - { - // You cannot timeout the broadcaster - return false; - } - - if (this->tags.value("user-type").toString() == "mod" && - !this->args.isStaffOrBroadcaster) - { - // You cannot timeout moderators UNLESS you are Twitch Staff or the broadcaster of the channel - return false; - } - - return true; -} - -void TwitchMessageBuilder::appendChannelPointRewardMessage( - const ChannelPointReward &reward, MessageBuilder *builder, bool isMod, - bool isBroadcaster) -{ - if (isIgnoredMessage({ - /*.message = */ "", - /*.twitchUserID = */ reward.user.id, - /*.isMod = */ isMod, - /*.isBroadcaster = */ isBroadcaster, - })) - { - return; - } - - builder->emplace(); - QString redeemed = "Redeemed"; - QStringList textList; - if (!reward.isUserInputRequired) - { - builder - ->emplace( - reward.user.login, MessageElementFlag::ChannelPointReward, - MessageColor::Text, FontStyle::ChatMediumBold) - ->setLink({Link::UserInfo, reward.user.login}); - redeemed = "redeemed"; - textList.append(reward.user.login); - } - builder->emplace(redeemed, - MessageElementFlag::ChannelPointReward); - builder->emplace( - reward.title, MessageElementFlag::ChannelPointReward, - MessageColor::Text, FontStyle::ChatMediumBold); - builder->emplace( - reward.image, MessageElementFlag::ChannelPointRewardImage); - builder->emplace( - QString::number(reward.cost), MessageElementFlag::ChannelPointReward, - MessageColor::Text, FontStyle::ChatMediumBold); - if (reward.isUserInputRequired) - { - builder->emplace( - MessageElementFlag::ChannelPointReward); - } - - builder->message().flags.set(MessageFlag::RedeemedChannelPointReward); - - textList.append({redeemed, reward.title, QString::number(reward.cost)}); - builder->message().messageText = textList.join(" "); - builder->message().searchText = textList.join(" "); -} - -void TwitchMessageBuilder::liveMessage(const QString &channelName, - MessageBuilder *builder) -{ - builder->emplace(); - builder - ->emplace(channelName, MessageElementFlag::Username, - MessageColor::Text, FontStyle::ChatMediumBold) - ->setLink({Link::UserInfo, channelName}); - builder->emplace("is live!", MessageElementFlag::Text, - MessageColor::Text); - auto text = QString("%1 is live!").arg(channelName); - builder->message().messageText = text; - builder->message().searchText = text; -} - -void TwitchMessageBuilder::liveSystemMessage(const QString &channelName, - MessageBuilder *builder) -{ - builder->emplace(); - builder->message().flags.set(MessageFlag::System); - builder->message().flags.set(MessageFlag::DoNotTriggerNotification); - builder - ->emplace(channelName, MessageElementFlag::Username, - MessageColor::System, FontStyle::ChatMediumBold) - ->setLink({Link::UserInfo, channelName}); - builder->emplace("is live!", MessageElementFlag::Text, - MessageColor::System); - auto text = QString("%1 is live!").arg(channelName); - builder->message().messageText = text; - builder->message().searchText = text; -} - -void TwitchMessageBuilder::offlineSystemMessage(const QString &channelName, - MessageBuilder *builder) -{ - builder->emplace(); - builder->message().flags.set(MessageFlag::System); - builder->message().flags.set(MessageFlag::DoNotTriggerNotification); - builder - ->emplace(channelName, MessageElementFlag::Username, - MessageColor::System, FontStyle::ChatMediumBold) - ->setLink({Link::UserInfo, channelName}); - builder->emplace("is now offline.", MessageElementFlag::Text, - MessageColor::System); - auto text = QString("%1 is now offline.").arg(channelName); - builder->message().messageText = text; - builder->message().searchText = text; -} - -void TwitchMessageBuilder::hostingSystemMessage(const QString &channelName, - MessageBuilder *builder, - bool hostOn) -{ - QString text; - builder->emplace(); - builder->message().flags.set(MessageFlag::System); - builder->message().flags.set(MessageFlag::DoNotTriggerNotification); - if (hostOn) - { - builder->emplace("Now hosting", MessageElementFlag::Text, - MessageColor::System); - builder - ->emplace( - channelName + ".", MessageElementFlag::Username, - MessageColor::System, FontStyle::ChatMediumBold) - ->setLink({Link::UserInfo, channelName}); - text = QString("Now hosting %1.").arg(channelName); - } - else - { - builder - ->emplace(channelName, MessageElementFlag::Username, - MessageColor::System, - FontStyle::ChatMediumBold) - ->setLink({Link::UserInfo, channelName}); - builder->emplace("has gone offline. Exiting host mode.", - MessageElementFlag::Text, - MessageColor::System); - text = - QString("%1 has gone offline. Exiting host mode.").arg(channelName); - } - builder->message().messageText = text; - builder->message().searchText = text; -} - -// IRC variant -void TwitchMessageBuilder::deletionMessage(const MessagePtr originalMessage, - MessageBuilder *builder) -{ - builder->emplace(); - builder->message().flags.set(MessageFlag::System); - builder->message().flags.set(MessageFlag::DoNotTriggerNotification); - builder->message().flags.set(MessageFlag::Timeout); - // TODO(mm2pl): If or when jumping to a single message gets implemented a link, - // add a link to the originalMessage - builder->emplace("A message from", MessageElementFlag::Text, - MessageColor::System); - builder - ->emplace(originalMessage->displayName, - MessageElementFlag::Username, - MessageColor::System, FontStyle::ChatMediumBold) - ->setLink({Link::UserInfo, originalMessage->loginName}); - builder->emplace("was deleted:", MessageElementFlag::Text, - MessageColor::System); - if (originalMessage->messageText.length() > 50) - { - builder->emplace( - originalMessage->messageText.left(50) + "…", - MessageElementFlag::Text, MessageColor::Text); - } - else - { - builder->emplace(originalMessage->messageText, - MessageElementFlag::Text, - MessageColor::Text); - } - builder->message().timeoutUser = "msg:" + originalMessage->id; -} - -// pubsub variant -void TwitchMessageBuilder::deletionMessage(const DeleteAction &action, - MessageBuilder *builder) -{ - builder->emplace(); - builder->message().flags.set(MessageFlag::System); - builder->message().flags.set(MessageFlag::DoNotTriggerNotification); - builder->message().flags.set(MessageFlag::Timeout); - - builder - ->emplace(action.source.login, - MessageElementFlag::Username, - MessageColor::System, FontStyle::ChatMediumBold) - ->setLink({Link::UserInfo, action.source.login}); - // TODO(mm2pl): If or when jumping to a single message gets implemented a link, - // add a link to the originalMessage - builder->emplace( - "deleted message from", MessageElementFlag::Text, MessageColor::System); - builder - ->emplace(action.target.login, - MessageElementFlag::Username, - MessageColor::System, FontStyle::ChatMediumBold) - ->setLink({Link::UserInfo, action.target.login}); - builder->emplace("saying:", MessageElementFlag::Text, - MessageColor::System); - if (action.messageText.length() > 50) - { - builder->emplace(action.messageText.left(50) + "…", - MessageElementFlag::Text, - MessageColor::Text); - } - else - { - builder->emplace( - action.messageText, MessageElementFlag::Text, MessageColor::Text); - } - builder->message().timeoutUser = "msg:" + action.messageId; -} - -void TwitchMessageBuilder::listOfUsersSystemMessage(QString prefix, - QStringList users, - Channel *channel, - MessageBuilder *builder) -{ - QString text = prefix + users.join(", "); - - builder->message().messageText = text; - builder->message().searchText = text; - - builder->emplace(); - builder->message().flags.set(MessageFlag::System); - builder->message().flags.set(MessageFlag::DoNotTriggerNotification); - builder->emplace(prefix, MessageElementFlag::Text, - MessageColor::System); - bool isFirst = true; - auto tc = dynamic_cast(channel); - for (const QString &username : users) - { - if (!isFirst) - { - // this is used to add the ", " after each but the last entry - builder->emplace(",", MessageElementFlag::Text, - MessageColor::System); - } - isFirst = false; - - MessageColor color = MessageColor::System; - - if (tc && getSettings()->colorUsernames) - { - if (auto userColor = tc->getUserColor(username); - userColor.isValid()) - { - color = MessageColor(userColor); - } - } - - builder - ->emplace(username, MessageElementFlag::BoldUsername, - color, FontStyle::ChatMediumBold) - ->setLink({Link::UserInfo, username}) - ->setTrailingSpace(false); - builder - ->emplace(username, - MessageElementFlag::NonBoldUsername, color) - ->setLink({Link::UserInfo, username}) - ->setTrailingSpace(false); - } -} - -void TwitchMessageBuilder::setThread(std::shared_ptr thread) -{ - this->thread_ = std::move(thread); -} - -} // namespace chatterino diff --git a/src/providers/twitch/TwitchMessageBuilder.hpp b/src/providers/twitch/TwitchMessageBuilder.hpp deleted file mode 100644 index 130c89c3c..000000000 --- a/src/providers/twitch/TwitchMessageBuilder.hpp +++ /dev/null @@ -1,117 +0,0 @@ -#pragma once - -#include "common/Aliases.hpp" -#include "common/Outcome.hpp" -#include "messages/MessageThread.hpp" -#include "messages/SharedMessageBuilder.hpp" -#include "providers/twitch/ChannelPointReward.hpp" -#include "providers/twitch/PubSubActions.hpp" -#include "providers/twitch/TwitchBadge.hpp" - -#include -#include -#include - -namespace chatterino { - -struct Emote; -using EmotePtr = std::shared_ptr; - -class Channel; -class TwitchChannel; - -struct TwitchEmoteOccurence { - int start; - int end; - EmotePtr ptr; - EmoteName name; -}; - -class TwitchMessageBuilder : public SharedMessageBuilder -{ -public: - TwitchMessageBuilder() = delete; - - explicit TwitchMessageBuilder(Channel *_channel, - const Communi::IrcPrivateMessage *_ircMessage, - const MessageParseArgs &_args); - explicit TwitchMessageBuilder(Channel *_channel, - const Communi::IrcMessage *_ircMessage, - const MessageParseArgs &_args, - QString content, bool isAction); - - TwitchChannel *twitchChannel; - - [[nodiscard]] bool isIgnored() const override; - void triggerHighlights() override; - MessagePtr build() override; - - void setThread(std::shared_ptr thread); - - static void appendChannelPointRewardMessage( - const ChannelPointReward &reward, MessageBuilder *builder, bool isMod, - bool isBroadcaster); - - // Message in the /live chat for channel going live - static void liveMessage(const QString &channelName, - MessageBuilder *builder); - - // Messages in normal chat for channel stuff - static void liveSystemMessage(const QString &channelName, - MessageBuilder *builder); - static void offlineSystemMessage(const QString &channelName, - MessageBuilder *builder); - static void hostingSystemMessage(const QString &channelName, - MessageBuilder *builder, bool hostOn); - static void deletionMessage(const MessagePtr originalMessage, - MessageBuilder *builder); - static void deletionMessage(const DeleteAction &action, - MessageBuilder *builder); - static void listOfUsersSystemMessage(QString prefix, QStringList users, - Channel *channel, - MessageBuilder *builder); - - // Shares some common logic from SharedMessageBuilder::parseBadgeTag - static std::unordered_map parseBadgeInfoTag( - const QVariantMap &tags); - -private: - void parseUsernameColor() override; - void parseUsername() override; - void parseMessageID(); - void parseRoomID(); - void appendUsername(); - - void runIgnoreReplaces(std::vector &twitchEmotes); - - boost::optional getTwitchBadge(const Badge &badge); - void appendTwitchEmote(const QString &emote, - std::vector &vec, - std::vector &correctPositions); - Outcome tryAppendEmote(const EmoteName &name) override; - - void addWords(const QStringList &words, - const std::vector &twitchEmotes); - void addTextOrEmoji(EmotePtr emote) override; - void addTextOrEmoji(const QString &value) override; - - void appendTwitchBadges(); - void appendChatterinoBadges(); - void appendFfzBadges(); - Outcome tryParseCheermote(const QString &string); - - bool shouldAddModerationElements() const; - - QString roomID_; - bool hasBits_ = false; - QString bits; - int bitsLeft; - bool bitsStacked = false; - bool historicalMessage_ = false; - std::shared_ptr thread_; - - QString userId_; - bool senderIsBroadcaster{}; -}; - -} // namespace chatterino diff --git a/src/providers/twitch/TwitchUser.cpp b/src/providers/twitch/TwitchUser.cpp index e24e9c781..f1b625509 100644 --- a/src/providers/twitch/TwitchUser.cpp +++ b/src/providers/twitch/TwitchUser.cpp @@ -1,7 +1,15 @@ #include "providers/twitch/TwitchUser.hpp" +#include "providers/twitch/api/Helix.hpp" #include "util/RapidjsonHelpers.hpp" namespace chatterino { +void TwitchUser::fromHelixBlock(const HelixBlock &ignore) +{ + this->id = ignore.userId; + this->name = ignore.userName; + this->displayName = ignore.displayName; +} + } // namespace chatterino diff --git a/src/providers/twitch/TwitchUser.hpp b/src/providers/twitch/TwitchUser.hpp index 53dde8d7a..d615c95b0 100644 --- a/src/providers/twitch/TwitchUser.hpp +++ b/src/providers/twitch/TwitchUser.hpp @@ -1,16 +1,18 @@ #pragma once -#include "providers/twitch/api/Helix.hpp" +#include "util/QStringHash.hpp" #include "util/RapidjsonHelpers.hpp" -#include -#include #include +#include +#include #include namespace chatterino { +struct HelixBlock; + struct TwitchUser { QString id; mutable QString name; @@ -24,17 +26,22 @@ struct TwitchUser { this->displayName = other.displayName; } - void fromHelixBlock(const HelixBlock &ignore) - { - this->id = ignore.userId; - this->name = ignore.userName; - this->displayName = ignore.displayName; - } + void fromHelixBlock(const HelixBlock &ignore); bool operator<(const TwitchUser &rhs) const { return this->id < rhs.id; } + + bool operator==(const TwitchUser &rhs) const + { + return this->id == rhs.id; + } + + bool operator!=(const TwitchUser &rhs) const + { + return !(*this == rhs); + } }; } // namespace chatterino @@ -79,3 +86,11 @@ struct Deserialize { }; } // namespace pajlada + +template <> +struct std::hash { + inline size_t operator()(const chatterino::TwitchUser &user) const noexcept + { + return std::hash{}(user.id); + } +}; diff --git a/src/providers/twitch/api/Helix.cpp b/src/providers/twitch/api/Helix.cpp index 953d7d989..5b8c9fbfd 100644 --- a/src/providers/twitch/api/Helix.cpp +++ b/src/providers/twitch/api/Helix.cpp @@ -1,14 +1,44 @@ #include "providers/twitch/api/Helix.hpp" -#include "common/Outcome.hpp" +#include "common/Literals.hpp" +#include "common/network/NetworkRequest.hpp" +#include "common/network/NetworkResult.hpp" #include "common/QLogging.hpp" +#include "util/CancellationToken.hpp" +#include "util/QMagicEnum.hpp" +#include #include +namespace { + +using namespace chatterino; + +constexpr auto NUM_MODERATORS_TO_FETCH_PER_REQUEST = 100; + +constexpr auto NUM_CHATTERS_TO_FETCH = 1000; + +} // namespace + namespace chatterino { +using namespace literals; + static IHelix *instance = nullptr; +HelixChatters::HelixChatters(const QJsonObject &jsonObject) + : total(jsonObject.value("total").toInt()) + , cursor( + jsonObject.value("pagination").toObject().value("cursor").toString()) +{ + const auto &data = jsonObject.value("data").toArray(); + for (const auto &chatter : data) + { + auto userLogin = chatter.toObject().value("user_login").toString(); + this->chatters.insert(userLogin); + } +} + void Helix::fetchUsers(QStringList userIds, QStringList userLogins, ResultCallback> successCallback, HelixFailureCallback failureCallback) @@ -26,15 +56,15 @@ void Helix::fetchUsers(QStringList userIds, QStringList userLogins, } // TODO: set on success and on error - this->makeRequest("users", urlQuery) - .onSuccess([successCallback, failureCallback](auto result) -> Outcome { + this->makeGet("users", urlQuery) + .onSuccess([successCallback, failureCallback](auto result) { auto root = result.parseJson(); auto data = root.value("data"); if (!data.isArray()) { failureCallback(); - return Failure; + return; } std::vector users; @@ -45,8 +75,6 @@ void Helix::fetchUsers(QStringList userIds, QStringList userLogins, } successCallback(users); - - return Success; }) .onError([failureCallback](auto /*result*/) { // TODO: make better xd @@ -96,52 +124,42 @@ void Helix::getUserById(QString userId, failureCallback); } -void Helix::fetchUsersFollows( - QString fromId, QString toId, - ResultCallback successCallback, - HelixFailureCallback failureCallback) +void Helix::getChannelFollowers( + QString broadcasterID, + ResultCallback successCallback, + std::function failureCallback) { - assert(!fromId.isEmpty() || !toId.isEmpty()); + assert(!broadcasterID.isEmpty()); QUrlQuery urlQuery; - - if (!fromId.isEmpty()) - { - urlQuery.addQueryItem("from_id", fromId); - } - - if (!toId.isEmpty()) - { - urlQuery.addQueryItem("to_id", toId); - } + urlQuery.addQueryItem("broadcaster_id", broadcasterID); // TODO: set on success and on error - this->makeRequest("users/follows", urlQuery) - .onSuccess([successCallback, failureCallback](auto result) -> Outcome { + this->makeGet("channels/followers", urlQuery) + .onSuccess([successCallback, failureCallback](auto result) { auto root = result.parseJson(); if (root.empty()) { - failureCallback(); - return Failure; + failureCallback("Bad JSON response"); + return; } - successCallback(HelixUsersFollowsResponse(root)); - return Success; + successCallback(HelixGetChannelFollowersResponse(root)); }) - .onError([failureCallback](auto /*result*/) { - // TODO: make better xd - failureCallback(); + .onError([failureCallback](auto result) { + auto root = result.parseJson(); + if (root.empty()) + { + failureCallback("Unknown error"); + return; + } + + // Forward "message" from Twitch + HelixError error(root); + failureCallback(error.message); }) .execute(); } -void Helix::getUserFollowers( - QString userId, ResultCallback successCallback, - HelixFailureCallback failureCallback) -{ - this->fetchUsersFollows("", std::move(userId), std::move(successCallback), - std::move(failureCallback)); -} - void Helix::fetchStreams( QStringList userIds, QStringList userLogins, ResultCallback> successCallback, @@ -160,15 +178,15 @@ void Helix::fetchStreams( } // TODO: set on success and on error - this->makeRequest("streams", urlQuery) - .onSuccess([successCallback, failureCallback](auto result) -> Outcome { + this->makeGet("streams", urlQuery) + .onSuccess([successCallback, failureCallback](auto result) { auto root = result.parseJson(); auto data = root.value("data"); if (!data.isArray()) { failureCallback(); - return Failure; + return; } std::vector streams; @@ -179,8 +197,6 @@ void Helix::fetchStreams( } successCallback(streams); - - return Success; }) .onError([failureCallback](auto /*result*/) { // TODO: make better xd @@ -253,15 +269,15 @@ void Helix::fetchGames(QStringList gameIds, QStringList gameNames, } // TODO: set on success and on error - this->makeRequest("games", urlQuery) - .onSuccess([successCallback, failureCallback](auto result) -> Outcome { + this->makeGet("games", urlQuery) + .onSuccess([successCallback, failureCallback](auto result) { auto root = result.parseJson(); auto data = root.value("data"); if (!data.isArray()) { failureCallback(); - return Failure; + return; } std::vector games; @@ -272,8 +288,6 @@ void Helix::fetchGames(QStringList gameIds, QStringList gameNames, } successCallback(games); - - return Success; }) .onError([failureCallback](auto /*result*/) { // TODO: make better xd @@ -289,15 +303,15 @@ void Helix::searchGames(QString gameName, QUrlQuery urlQuery; urlQuery.addQueryItem("query", gameName); - this->makeRequest("search/categories", urlQuery) - .onSuccess([successCallback, failureCallback](auto result) -> Outcome { + this->makeGet("search/categories", urlQuery) + .onSuccess([successCallback, failureCallback](auto result) { auto root = result.parseJson(); auto data = root.value("data"); if (!data.isArray()) { failureCallback(); - return Failure; + return; } std::vector games; @@ -308,8 +322,6 @@ void Helix::searchGames(QString gameName, } successCallback(games); - - return Success; }) .onError([failureCallback](auto /*result*/) { // TODO: make better xd @@ -346,26 +358,24 @@ void Helix::createClip(QString channelId, QUrlQuery urlQuery; urlQuery.addQueryItem("broadcaster_id", channelId); - this->makeRequest("clips", urlQuery) - .type(NetworkRequestType::Post) + this->makePost("clips", urlQuery) .header("Content-Type", "application/json") - .onSuccess([successCallback, failureCallback](auto result) -> Outcome { + .onSuccess([successCallback, failureCallback](auto result) { auto root = result.parseJson(); auto data = root.value("data"); if (!data.isArray()) { failureCallback(HelixClipError::Unknown); - return Failure; + return; } HelixClip clip(data.toArray()[0].toObject()); successCallback(clip); - return Success; }) .onError([failureCallback](auto result) { - switch (result.status()) + switch (result.status().value_or(0)) { case 503: { // Channel has disabled clip-creation, or channel has made cliops only creatable by followers and the user is not a follower (or subscriber) @@ -381,7 +391,7 @@ void Helix::createClip(QString channelId, default: { qCDebug(chatterinoTwitch) - << "Failed to create a clip: " << result.status() + << "Failed to create a clip: " << result.formatError() << result.getData(); failureCallback(HelixClipError::Unknown); } @@ -392,6 +402,44 @@ void Helix::createClip(QString channelId, .execute(); } +void Helix::fetchChannels( + QStringList userIDs, + ResultCallback> successCallback, + HelixFailureCallback failureCallback) +{ + QUrlQuery urlQuery; + + for (const auto &userID : userIDs) + { + urlQuery.addQueryItem("broadcaster_id", userID); + } + + this->makeGet("channels", urlQuery) + .onSuccess([successCallback, failureCallback](auto result) { + auto root = result.parseJson(); + auto data = root.value("data"); + + if (!data.isArray()) + { + failureCallback(); + return; + } + + std::vector channels; + + for (const auto &unparsedChannel : data.toArray()) + { + channels.emplace_back(unparsedChannel.toObject()); + } + + successCallback(channels); + }) + .onError([failureCallback](auto /*result*/) { + failureCallback(); + }) + .execute(); +} + void Helix::getChannel(QString broadcasterId, ResultCallback successCallback, HelixFailureCallback failureCallback) @@ -399,21 +447,20 @@ void Helix::getChannel(QString broadcasterId, QUrlQuery urlQuery; urlQuery.addQueryItem("broadcaster_id", broadcasterId); - this->makeRequest("channels", urlQuery) - .onSuccess([successCallback, failureCallback](auto result) -> Outcome { + this->makeGet("channels", urlQuery) + .onSuccess([successCallback, failureCallback](auto result) { auto root = result.parseJson(); auto data = root.value("data"); if (!data.isArray()) { failureCallback(); - return Failure; + return; } HelixChannel channel(data.toArray()[0].toObject()); successCallback(channel); - return Success; }) .onError([failureCallback](auto /*result*/) { failureCallback(); @@ -434,27 +481,24 @@ void Helix::createStreamMarker( } payload.insert("user_id", QJsonValue(broadcasterId)); - this->makeRequest("streams/markers", QUrlQuery()) - .type(NetworkRequestType::Post) - .header("Content-Type", "application/json") - .payload(QJsonDocument(payload).toJson(QJsonDocument::Compact)) - .onSuccess([successCallback, failureCallback](auto result) -> Outcome { + this->makePost("streams/markers", QUrlQuery()) + .json(payload) + .onSuccess([successCallback, failureCallback](auto result) { auto root = result.parseJson(); auto data = root.value("data"); if (!data.isArray()) { failureCallback(HelixStreamMarkerError::Unknown); - return Failure; + return; } HelixStreamMarker streamMarker(data.toArray()[0].toObject()); successCallback(streamMarker); - return Success; }) .onError([failureCallback](NetworkResult result) { - switch (result.status()) + switch (result.status().value_or(0)) { case 403: { // User isn't a Channel Editor, so he can't create markers @@ -472,7 +516,7 @@ void Helix::createStreamMarker( default: { qCDebug(chatterinoTwitch) << "Failed to create a stream marker: " - << result.status() << result.getData(); + << result.formatError() << result.getData(); failureCallback(HelixStreamMarkerError::Unknown); } break; @@ -482,54 +526,66 @@ void Helix::createStreamMarker( }; void Helix::loadBlocks(QString userId, - ResultCallback> successCallback, - HelixFailureCallback failureCallback) + ResultCallback> pageCallback, + FailureCallback failureCallback, + CancellationToken &&token) { - QUrlQuery urlQuery; - urlQuery.addQueryItem("broadcaster_id", userId); - urlQuery.addQueryItem("first", "100"); + constexpr const size_t blockLimit = 1000; - this->makeRequest("users/blocks", urlQuery) - .onSuccess([successCallback, failureCallback](auto result) -> Outcome { - auto root = result.parseJson(); - auto data = root.value("data"); + // TODO(Qt 5.13): use initializer list + QUrlQuery query; + query.addQueryItem(u"broadcaster_id"_s, userId); + query.addQueryItem(u"first"_s, u"100"_s); - if (!data.isArray()) + size_t receivedItems = 0; + this->paginate( + u"users/blocks"_s, query, + [pageCallback, receivedItems](const QJsonObject &json) mutable { + const auto data = json["data"_L1].toArray(); + + if (data.isEmpty()) { - failureCallback(); - return Failure; + return false; } std::vector ignores; + ignores.reserve(data.count()); - for (const auto &jsonStream : data.toArray()) + for (const auto &ignore : data) { - ignores.emplace_back(jsonStream.toObject()); + ignores.emplace_back(ignore.toObject()); } - successCallback(ignores); + pageCallback(ignores); - return Success; - }) - .onError([failureCallback](auto /*result*/) { - // TODO: make better xd - failureCallback(); - }) - .execute(); + receivedItems += data.count(); + + if (receivedItems >= blockLimit) + { + qCInfo(chatterinoTwitch) << "Reached the limit of" << blockLimit + << "Twitch blocks fetched"; + return false; + } + + return true; + }, + [failureCallback](const NetworkResult &result) { + failureCallback(result.formatError()); + }, + std::move(token)); } -void Helix::blockUser(QString targetUserId, +void Helix::blockUser(QString targetUserId, const QObject *caller, std::function successCallback, HelixFailureCallback failureCallback) { QUrlQuery urlQuery; urlQuery.addQueryItem("target_user_id", targetUserId); - this->makeRequest("users/blocks", urlQuery) - .type(NetworkRequestType::Put) - .onSuccess([successCallback](auto /*result*/) -> Outcome { + this->makePut("users/blocks", urlQuery) + .caller(caller) + .onSuccess([successCallback](auto /*result*/) { successCallback(); - return Success; }) .onError([failureCallback](auto /*result*/) { // TODO: make better xd @@ -538,18 +594,17 @@ void Helix::blockUser(QString targetUserId, .execute(); } -void Helix::unblockUser(QString targetUserId, +void Helix::unblockUser(QString targetUserId, const QObject *caller, std::function successCallback, HelixFailureCallback failureCallback) { QUrlQuery urlQuery; urlQuery.addQueryItem("target_user_id", targetUserId); - this->makeRequest("users/blocks", urlQuery) - .type(NetworkRequestType::Delete) - .onSuccess([successCallback](auto /*result*/) -> Outcome { + this->makeDelete("users/blocks", urlQuery) + .caller(caller) + .onSuccess([successCallback](auto /*result*/) { successCallback(); - return Success; }) .onError([failureCallback](auto /*result*/) { // TODO: make better xd @@ -558,13 +613,14 @@ void Helix::unblockUser(QString targetUserId, .execute(); } -void Helix::updateChannel(QString broadcasterId, QString gameId, - QString language, QString title, - std::function successCallback, - HelixFailureCallback failureCallback) +void Helix::updateChannel( + QString broadcasterId, QString gameId, QString language, QString title, + std::function successCallback, + FailureCallback failureCallback) { + using Error = HelixUpdateChannelError; + QUrlQuery urlQuery; - auto data = QJsonDocument(); auto obj = QJsonObject(); if (!gameId.isEmpty()) { @@ -585,18 +641,68 @@ void Helix::updateChannel(QString broadcasterId, QString gameId, return; } - data.setObject(obj); urlQuery.addQueryItem("broadcaster_id", broadcasterId); - this->makeRequest("channels", urlQuery) - .type(NetworkRequestType::Patch) - .header("Content-Type", "application/json") - .payload(data.toJson()) - .onSuccess([successCallback, failureCallback](auto result) -> Outcome { + this->makePatch("channels", urlQuery) + .json(obj) + .onSuccess([successCallback, failureCallback](auto result) { successCallback(result); - return Success; }) .onError([failureCallback](NetworkResult result) { - failureCallback(); + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + + auto obj = result.parseJson(); + auto message = obj.value("message").toString(); + + switch (*result.status()) + { + case 401: { + if (message.startsWith("Missing scope", + Qt::CaseInsensitive)) + { + failureCallback(Error::UserMissingScope, message); + } + else if (message.compare( + "The ID in broadcaster_id must match the user " + "ID found in the request's OAuth token.", + Qt::CaseInsensitive) == 0) + { + failureCallback(Error::UserNotAuthorized, message); + } + else + { + failureCallback(Error::Forwarded, message); + } + } + break; + + case 400: + case 403: { + failureCallback(Error::Forwarded, message); + } + break; + + case 429: { + failureCallback(Error::Ratelimited, message); + } + break; + + case 500: { + failureCallback(Error::Unknown, message); + } + break; + + default: { + qCDebug(chatterinoTwitch) + << "Helix update channel, unhandled error data:" + << result.formatError() << result.getData() << obj; + failureCallback(Error::Unknown, message); + } + break; + } }) .execute(); } @@ -612,16 +718,13 @@ void Helix::manageAutoModMessages( payload.insert("msg_id", msgID); payload.insert("action", action); - this->makeRequest("moderation/automod/message", QUrlQuery()) - .type(NetworkRequestType::Post) - .header("Content-Type", "application/json") - .payload(QJsonDocument(payload).toJson(QJsonDocument::Compact)) - .onSuccess([successCallback, failureCallback](auto result) -> Outcome { + this->makePost("moderation/automod/message", QUrlQuery()) + .json(payload) + .onSuccess([successCallback, failureCallback](auto result) { successCallback(); - return Success; }) .onError([failureCallback, msgID, action](NetworkResult result) { - switch (result.status()) + switch (result.status().value_or(0)) { case 400: { // Message was already processed @@ -653,7 +756,7 @@ void Helix::manageAutoModMessages( default: { qCDebug(chatterinoTwitch) << "Failed to manage automod message: " << action - << msgID << result.status() << result.getData(); + << msgID << result.formatError() << result.getData(); failureCallback(HelixAutoModMessageError::Unknown); } break; @@ -671,15 +774,15 @@ void Helix::getCheermotes( urlQuery.addQueryItem("broadcaster_id", broadcasterId); - this->makeRequest("bits/cheermotes", urlQuery) - .onSuccess([successCallback, failureCallback](auto result) -> Outcome { + this->makeGet("bits/cheermotes", urlQuery) + .onSuccess([successCallback, failureCallback](auto result) { auto root = result.parseJson(); auto data = root.value("data"); if (!data.isArray()) { failureCallback(); - return Failure; + return; } std::vector cheermoteSets; @@ -690,12 +793,11 @@ void Helix::getCheermotes( } successCallback(cheermoteSets); - return Success; }) .onError([broadcasterId, failureCallback](NetworkResult result) { qCDebug(chatterinoTwitch) << "Failed to get cheermotes(broadcaster_id=" << broadcasterId - << "): " << result.status() << result.getData(); + << "): " << result.formatError() << result.getData(); failureCallback(); }) .execute(); @@ -709,22 +811,20 @@ void Helix::getEmoteSetData(QString emoteSetId, urlQuery.addQueryItem("emote_set_id", emoteSetId); - this->makeRequest("chat/emotes/set", urlQuery) - .onSuccess([successCallback, failureCallback, - emoteSetId](auto result) -> Outcome { + this->makeGet("chat/emotes/set", urlQuery) + .onSuccess([successCallback, failureCallback, emoteSetId](auto result) { QJsonObject root = result.parseJson(); auto data = root.value("data"); if (!data.isArray() || data.toArray().isEmpty()) { failureCallback(); - return Failure; + return; } HelixEmoteSetData emoteSetData(data.toArray()[0].toObject()); successCallback(emoteSetData); - return Success; }) .onError([failureCallback](NetworkResult result) { // TODO: make better xd @@ -741,16 +841,15 @@ void Helix::getChannelEmotes( QUrlQuery urlQuery; urlQuery.addQueryItem("broadcaster_id", broadcasterId); - this->makeRequest("chat/emotes", urlQuery) - .onSuccess([successCallback, - failureCallback](NetworkResult result) -> Outcome { + this->makeGet("chat/emotes", urlQuery) + .onSuccess([successCallback, failureCallback](NetworkResult result) { QJsonObject root = result.parseJson(); auto data = root.value("data"); if (!data.isArray()) { failureCallback(); - return Failure; + return; } std::vector channelEmotes; @@ -761,7 +860,6 @@ void Helix::getChannelEmotes( } successCallback(channelEmotes); - return Success; }) .onError([failureCallback](auto result) { // TODO: make better xd @@ -770,7 +868,2203 @@ void Helix::getChannelEmotes( .execute(); } -NetworkRequest Helix::makeRequest(QString url, QUrlQuery urlQuery) +void Helix::updateUserChatColor( + QString userID, QString color, ResultCallback<> successCallback, + FailureCallback failureCallback) +{ + using Error = HelixUpdateUserChatColorError; + + QJsonObject payload; + + payload.insert("user_id", QJsonValue(userID)); + payload.insert("color", QJsonValue(color)); + + this->makePut("chat/color", QUrlQuery()) + .json(payload) + .onSuccess([successCallback, failureCallback](auto result) { + auto obj = result.parseJson(); + if (result.status() != 204) + { + qCWarning(chatterinoTwitch) + << "Success result for updating chat color was" + << result.formatError() + << "but we only expected it to be 204"; + } + + successCallback(); + }) + .onError([failureCallback](const auto &result) -> void { + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + + auto obj = result.parseJson(); + auto message = obj.value("message").toString(); + + switch (*result.status()) + { + case 400: { + if (message.startsWith("invalid color", + Qt::CaseInsensitive)) + { + // Handle this error specifically since it allows us to list out the available colors + failureCallback(Error::InvalidColor, message); + } + else + { + failureCallback(Error::Forwarded, message); + } + } + break; + + case 401: { + if (message.startsWith("Missing scope", + Qt::CaseInsensitive)) + { + // Handle this error specifically because its API error is especially unfriendly + failureCallback(Error::UserMissingScope, message); + } + else + { + failureCallback(Error::Forwarded, message); + } + } + break; + + default: { + qCDebug(chatterinoTwitch) + << "Unhandled error changing user color:" + << result.formatError() << result.getData() << obj; + failureCallback(Error::Unknown, message); + } + break; + } + }) + .execute(); +}; + +void Helix::deleteChatMessages( + QString broadcasterID, QString moderatorID, QString messageID, + ResultCallback<> successCallback, + FailureCallback failureCallback) +{ + using Error = HelixDeleteChatMessagesError; + + QUrlQuery urlQuery; + + urlQuery.addQueryItem("broadcaster_id", broadcasterID); + urlQuery.addQueryItem("moderator_id", moderatorID); + + if (!messageID.isEmpty()) + { + // If message ID is empty, it's equivalent to /clear + urlQuery.addQueryItem("message_id", messageID); + } + + this->makeDelete("moderation/chat", urlQuery) + .onSuccess([successCallback, failureCallback](auto result) { + if (result.status() != 204) + { + qCWarning(chatterinoTwitch) + << "Success result for deleting chat messages was" + << result.formatError() + << "but we only expected it to be 204"; + } + + successCallback(); + }) + .onError([failureCallback](const auto &result) -> void { + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + + auto obj = result.parseJson(); + auto message = obj.value("message").toString(); + + switch (*result.status()) + { + case 404: { + // A 404 on this endpoint means message id is invalid or unable to be deleted. + // See: https://dev.twitch.tv/docs/api/reference#delete-chat-messages + failureCallback(Error::MessageUnavailable, message); + } + break; + + case 400: { + // These errors are generally well formatted, so we just forward them. + // This is currently undocumented behaviour, see: https://github.com/twitchdev/issues/issues/660 + failureCallback(Error::Forwarded, message); + } + break; + + case 403: { + // 403 endpoint means the user does not have permission to perform this action in that channel + // Most likely to missing moderator permissions + // Missing documentation issue: https://github.com/twitchdev/issues/issues/659 + // `message` value is well-formed so no need for a specific error type + failureCallback(Error::Forwarded, message); + } + break; + + case 401: { + if (message.startsWith("Missing scope", + Qt::CaseInsensitive)) + { + // Handle this error specifically because its API error is especially unfriendly + failureCallback(Error::UserMissingScope, message); + } + else + { + failureCallback(Error::Forwarded, message); + } + } + break; + + default: { + qCDebug(chatterinoTwitch) + << "Unhandled error deleting chat messages:" + << result.formatError() << result.getData() << obj; + failureCallback(Error::Unknown, message); + } + break; + } + }) + .execute(); +} + +void Helix::addChannelModerator( + QString broadcasterID, QString userID, ResultCallback<> successCallback, + FailureCallback failureCallback) +{ + using Error = HelixAddChannelModeratorError; + + QUrlQuery urlQuery; + + urlQuery.addQueryItem("broadcaster_id", broadcasterID); + urlQuery.addQueryItem("user_id", userID); + + this->makePost("moderation/moderators", urlQuery) + .onSuccess([successCallback, failureCallback](auto result) { + if (result.status() != 204) + { + qCWarning(chatterinoTwitch) + << "Success result for adding a moderator was" + << result.formatError() + << "but we only expected it to be 204"; + } + + successCallback(); + }) + .onError([failureCallback](const auto &result) -> void { + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + + auto obj = result.parseJson(); + auto message = obj.value("message").toString(); + + switch (*result.status()) + { + case 401: { + if (message.startsWith("Missing scope", + Qt::CaseInsensitive)) + { + // Handle this error specifically because its API error is especially unfriendly + failureCallback(Error::UserMissingScope, message); + } + else if (message.compare("incorrect user authorization", + Qt::CaseInsensitive) == 0) + { + // This error is pretty ugly, but essentially means they're not authorized to mod people in this channel + failureCallback(Error::UserNotAuthorized, message); + } + else + { + failureCallback(Error::Forwarded, message); + } + } + break; + + case 400: { + if (message.compare("user is already a mod", + Qt::CaseInsensitive) == 0) + { + // This error is particularly ugly, handle it separately + failureCallback(Error::TargetAlreadyModded, message); + } + else + { + // The Twitch API error sufficiently tells the user what went wrong + failureCallback(Error::Forwarded, message); + } + } + break; + + case 422: { + // Target is already a VIP + failureCallback(Error::TargetIsVIP, message); + } + break; + + case 429: { + // Endpoint has a strict ratelimit + failureCallback(Error::Ratelimited, message); + } + break; + + default: { + qCDebug(chatterinoTwitch) + << "Unhandled error adding channel moderator:" + << result.formatError() << result.getData() << obj; + failureCallback(Error::Unknown, message); + } + break; + } + }) + .execute(); +} + +void Helix::removeChannelModerator( + QString broadcasterID, QString userID, ResultCallback<> successCallback, + FailureCallback failureCallback) +{ + using Error = HelixRemoveChannelModeratorError; + + QUrlQuery urlQuery; + + urlQuery.addQueryItem("broadcaster_id", broadcasterID); + urlQuery.addQueryItem("user_id", userID); + + this->makeDelete("moderation/moderators", urlQuery) + .onSuccess([successCallback, failureCallback](auto result) { + if (result.status() != 204) + { + qCWarning(chatterinoTwitch) + << "Success result for unmodding user was" + << result.formatError() + << "but we only expected it to be 204"; + } + + successCallback(); + }) + .onError([failureCallback](const auto &result) -> void { + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + + auto obj = result.parseJson(); + auto message = obj.value("message").toString(); + + switch (*result.status()) + { + case 400: { + if (message.compare("user is not a mod", + Qt::CaseInsensitive) == 0) + { + // This error message is particularly ugly, so we handle it differently + failureCallback(Error::TargetNotModded, message); + } + else + { + failureCallback(Error::Forwarded, message); + } + } + break; + + case 401: { + if (message.startsWith("Missing scope", + Qt::CaseInsensitive)) + { + // Handle this error specifically because its API error is especially unfriendly + failureCallback(Error::UserMissingScope, message); + } + else if (message.compare("incorrect user authorization", + Qt::CaseInsensitive) == 0) + { + failureCallback(Error::UserNotAuthorized, message); + } + else + { + failureCallback(Error::Forwarded, message); + } + } + break; + + case 429: { + failureCallback(Error::Ratelimited, message); + } + break; + + default: { + qCDebug(chatterinoTwitch) + << "Unhandled error unmodding user:" + << result.formatError() << result.getData() << obj; + failureCallback(Error::Unknown, message); + } + break; + } + }) + .execute(); +} + +void Helix::sendChatAnnouncement( + QString broadcasterID, QString moderatorID, QString message, + HelixAnnouncementColor color, ResultCallback<> successCallback, + FailureCallback failureCallback) +{ + using Error = HelixSendChatAnnouncementError; + + QUrlQuery urlQuery; + + urlQuery.addQueryItem("broadcaster_id", broadcasterID); + urlQuery.addQueryItem("moderator_id", moderatorID); + + QJsonObject body; + body.insert("message", message); + body.insert("color", qmagicenum::enumNameString(color).toLower()); + + this->makePost("chat/announcements", urlQuery) + .json(body) + .onSuccess([successCallback, failureCallback](auto result) { + if (result.status() != 204) + { + qCWarning(chatterinoTwitch) + << "Success result for sending an announcement was" + << result.formatError() + << "but we only expected it to be 204"; + } + + successCallback(); + }) + .onError([failureCallback](const auto &result) -> void { + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + + auto obj = result.parseJson(); + auto message = obj.value("message").toString(); + + switch (*result.status()) + { + case 400: { + // These errors are generally well formatted, so we just forward them. + // This is currently undocumented behaviour, see: https://github.com/twitchdev/issues/issues/660 + failureCallback(Error::Forwarded, message); + } + break; + + case 403: { + // 403 endpoint means the user does not have permission to perform this action in that channel + // `message` value is well-formed so no need for a specific error type + failureCallback(Error::Forwarded, message); + } + break; + + case 401: { + if (message.startsWith("Missing scope", + Qt::CaseInsensitive)) + { + // Handle this error specifically because its API error is especially unfriendly + failureCallback(Error::UserMissingScope, message); + } + else + { + failureCallback(Error::Forwarded, message); + } + } + break; + + default: { + qCDebug(chatterinoTwitch) + << "Unhandled error sending an announcement:" + << result.formatError() << result.getData() << obj; + failureCallback(Error::Unknown, message); + } + break; + } + }) + .execute(); +} + +void Helix::addChannelVIP( + QString broadcasterID, QString userID, ResultCallback<> successCallback, + FailureCallback failureCallback) +{ + using Error = HelixAddChannelVIPError; + + QUrlQuery urlQuery; + + urlQuery.addQueryItem("broadcaster_id", broadcasterID); + urlQuery.addQueryItem("user_id", userID); + + this->makePost("channels/vips", urlQuery) + .onSuccess([successCallback, failureCallback](auto result) { + if (result.status() != 204) + { + qCWarning(chatterinoTwitch) + << "Success result for adding channel VIP was" + << result.formatError() + << "but we only expected it to be 204"; + } + + successCallback(); + }) + .onError([failureCallback](const auto &result) -> void { + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + + auto obj = result.parseJson(); + auto message = obj.value("message").toString(); + + switch (*result.status()) + { + case 400: + case 409: + case 422: + case 425: { + // Most of the errors returned by this endpoint are pretty good. We can rely on Twitch's API messages + failureCallback(Error::Forwarded, message); + } + break; + + case 401: { + if (message.startsWith("Missing scope", + Qt::CaseInsensitive)) + { + // Handle this error specifically because its API error is especially unfriendly + failureCallback(Error::UserMissingScope, message); + } + else if (message.compare("incorrect user authorization", + Qt::CaseInsensitive) == 0 || + message.startsWith("the id in broadcaster_id must " + "match the user id", + Qt::CaseInsensitive)) + { + // This error is particularly ugly, but is the equivalent to a user not having permissions + failureCallback(Error::UserNotAuthorized, message); + } + else + { + failureCallback(Error::Forwarded, message); + } + } + break; + + case 429: { + failureCallback(Error::Ratelimited, message); + } + break; + + default: { + qCDebug(chatterinoTwitch) + << "Unhandled error adding channel VIP:" + << result.formatError() << result.getData() << obj; + failureCallback(Error::Unknown, message); + } + break; + } + }) + .execute(); +} + +void Helix::removeChannelVIP( + QString broadcasterID, QString userID, ResultCallback<> successCallback, + FailureCallback failureCallback) +{ + using Error = HelixRemoveChannelVIPError; + + QUrlQuery urlQuery; + + urlQuery.addQueryItem("broadcaster_id", broadcasterID); + urlQuery.addQueryItem("user_id", userID); + + this->makeDelete("channels/vips", urlQuery) + .onSuccess([successCallback, failureCallback](auto result) { + if (result.status() != 204) + { + qCWarning(chatterinoTwitch) + << "Success result for removing channel VIP was" + << result.formatError() + << "but we only expected it to be 204"; + } + + successCallback(); + }) + .onError([failureCallback](const auto &result) -> void { + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + + auto obj = result.parseJson(); + auto message = obj.value("message").toString(); + + switch (*result.status()) + { + case 400: + case 409: + case 422: { + // Most of the errors returned by this endpoint are pretty good. We can rely on Twitch's API messages + failureCallback(Error::Forwarded, message); + } + break; + + case 401: { + if (message.startsWith("Missing scope", + Qt::CaseInsensitive)) + { + // Handle this error specifically because its API error is especially unfriendly + failureCallback(Error::UserMissingScope, message); + } + else if (message.compare("incorrect user authorization", + Qt::CaseInsensitive) == 0 || + message.startsWith("the id in broadcaster_id must " + "match the user id", + Qt::CaseInsensitive)) + { + // This error is particularly ugly, but is the equivalent to a user not having permissions + failureCallback(Error::UserNotAuthorized, message); + } + else + { + failureCallback(Error::Forwarded, message); + } + } + break; + + case 429: { + failureCallback(Error::Ratelimited, message); + } + break; + + default: { + qCDebug(chatterinoTwitch) + << "Unhandled error removing channel VIP:" + << result.formatError() << result.getData() << obj; + failureCallback(Error::Unknown, message); + } + break; + } + }) + .execute(); +} + +void Helix::unbanUser( + QString broadcasterID, QString moderatorID, QString userID, + ResultCallback<> successCallback, + FailureCallback failureCallback) +{ + using Error = HelixUnbanUserError; + + QUrlQuery urlQuery; + + urlQuery.addQueryItem("broadcaster_id", broadcasterID); + urlQuery.addQueryItem("moderator_id", moderatorID); + urlQuery.addQueryItem("user_id", userID); + + this->makeDelete("moderation/bans", urlQuery) + .onSuccess([successCallback, failureCallback](auto result) { + if (result.status() != 204) + { + qCWarning(chatterinoTwitch) + << "Success result for unbanning user was" + << result.formatError() + << "but we only expected it to be 204"; + } + + successCallback(); + }) + .onError([failureCallback](const auto &result) -> void { + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + + auto obj = result.parseJson(); + auto message = obj.value("message").toString(); + + switch (*result.status()) + { + case 400: { + if (message.startsWith("The user in the user_id query " + "parameter is not banned", + Qt::CaseInsensitive)) + { + failureCallback(Error::TargetNotBanned, message); + } + else + { + failureCallback(Error::Forwarded, message); + } + } + break; + + case 409: { + failureCallback(Error::ConflictingOperation, message); + } + break; + + case 401: { + if (message.startsWith("Missing scope", + Qt::CaseInsensitive)) + { + // Handle this error specifically because its API error is especially unfriendly + failureCallback(Error::UserMissingScope, message); + } + else if (message.compare("incorrect user authorization", + Qt::CaseInsensitive) == 0 || + message.startsWith("the id in broadcaster_id must " + "match the user id", + Qt::CaseInsensitive)) + { + // This error is particularly ugly, but is the equivalent to a user not having permissions + failureCallback(Error::UserNotAuthorized, message); + } + else + { + failureCallback(Error::Forwarded, message); + } + } + break; + + case 403: { + failureCallback(Error::UserNotAuthorized, message); + } + break; + + case 429: { + failureCallback(Error::Ratelimited, message); + } + break; + + default: { + qCDebug(chatterinoTwitch) + << "Unhandled error unbanning user:" + << result.formatError() << result.getData() << obj; + failureCallback(Error::Unknown, message); + } + break; + } + }) + .execute(); +} + +void Helix::startRaid( + QString fromBroadcasterID, QString toBroadcasterID, + ResultCallback<> successCallback, + FailureCallback failureCallback) +{ + using Error = HelixStartRaidError; + + QUrlQuery urlQuery; + + urlQuery.addQueryItem("from_broadcaster_id", fromBroadcasterID); + urlQuery.addQueryItem("to_broadcaster_id", toBroadcasterID); + + this->makePost("raids", urlQuery) + .onSuccess([successCallback, failureCallback](auto /*result*/) { + successCallback(); + }) + .onError([failureCallback](const auto &result) -> void { + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + + auto obj = result.parseJson(); + auto message = obj.value("message").toString(); + + switch (*result.status()) + { + case 400: { + if (message.compare("The IDs in from_broadcaster_id and " + "to_broadcaster_id cannot be the same.", + Qt::CaseInsensitive) == 0) + { + failureCallback(Error::CantRaidYourself, message); + } + else + { + failureCallback(Error::Forwarded, message); + } + } + break; + + case 401: { + if (message.startsWith("Missing scope", + Qt::CaseInsensitive)) + { + failureCallback(Error::UserMissingScope, message); + } + else if (message.compare( + "The ID in broadcaster_id must match the user " + "ID " + "found in the request's OAuth token.", + Qt::CaseInsensitive) == 0) + { + // Must be the broadcaster. + failureCallback(Error::UserNotAuthorized, message); + } + else + { + failureCallback(Error::Forwarded, message); + } + } + break; + + case 409: { + failureCallback(Error::Forwarded, message); + } + break; + + case 429: { + failureCallback(Error::Ratelimited, message); + } + break; + + default: { + qCDebug(chatterinoTwitch) + << "Unhandled error while starting a raid:" + << result.formatError() << result.getData() << obj; + failureCallback(Error::Unknown, message); + } + break; + } + }) + .execute(); +} + +void Helix::cancelRaid( + QString broadcasterID, ResultCallback<> successCallback, + FailureCallback failureCallback) +{ + using Error = HelixCancelRaidError; + + QUrlQuery urlQuery; + + urlQuery.addQueryItem("broadcaster_id", broadcasterID); + + this->makeDelete("raids", urlQuery) + .onSuccess([successCallback, failureCallback](auto result) { + if (result.status() != 204) + { + qCWarning(chatterinoTwitch) + << "Success result for canceling the raid was" + << result.formatError() + << "but we only expected it to be 204"; + } + + successCallback(); + }) + .onError([failureCallback](const auto &result) -> void { + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + + auto obj = result.parseJson(); + auto message = obj.value("message").toString(); + + switch (*result.status()) + { + case 401: { + if (message.startsWith("Missing scope", + Qt::CaseInsensitive)) + { + failureCallback(Error::UserMissingScope, message); + } + else if (message.compare( + "The ID in broadcaster_id must match the user " + "ID " + "found in the request's OAuth token.", + Qt::CaseInsensitive) == 0) + { + // Must be the broadcaster. + failureCallback(Error::UserNotAuthorized, message); + } + else + { + failureCallback(Error::Forwarded, message); + } + } + break; + + case 404: { + failureCallback(Error::NoRaidPending, message); + } + break; + + case 429: { + failureCallback(Error::Ratelimited, message); + } + break; + + default: { + qCDebug(chatterinoTwitch) + << "Unhandled error while canceling the raid:" + << result.formatError() << result.getData() << obj; + failureCallback(Error::Unknown, message); + } + break; + } + }) + .execute(); +} // cancelRaid + +void Helix::updateEmoteMode( + QString broadcasterID, QString moderatorID, bool emoteMode, + ResultCallback successCallback, + FailureCallback failureCallback) +{ + QJsonObject json; + json["emote_mode"] = emoteMode; + this->updateChatSettings(broadcasterID, moderatorID, json, successCallback, + failureCallback); +} + +void Helix::updateFollowerMode( + QString broadcasterID, QString moderatorID, + std::optional followerModeDuration, + ResultCallback successCallback, + FailureCallback failureCallback) +{ + QJsonObject json; + json["follower_mode"] = followerModeDuration.has_value(); + if (followerModeDuration) + { + json["follower_mode_duration"] = *followerModeDuration; + } + + this->updateChatSettings(broadcasterID, moderatorID, json, successCallback, + failureCallback); +} + +void Helix::updateNonModeratorChatDelay( + QString broadcasterID, QString moderatorID, + std::optional nonModeratorChatDelayDuration, + ResultCallback successCallback, + FailureCallback failureCallback) +{ + QJsonObject json; + json["non_moderator_chat_delay"] = + nonModeratorChatDelayDuration.has_value(); + if (nonModeratorChatDelayDuration) + { + json["non_moderator_chat_delay_duration"] = + *nonModeratorChatDelayDuration; + } + + this->updateChatSettings(broadcasterID, moderatorID, json, successCallback, + failureCallback); +} + +void Helix::updateSlowMode( + QString broadcasterID, QString moderatorID, + std::optional slowModeWaitTime, + ResultCallback successCallback, + FailureCallback failureCallback) +{ + QJsonObject json; + json["slow_mode"] = slowModeWaitTime.has_value(); + if (slowModeWaitTime) + { + json["slow_mode_wait_time"] = *slowModeWaitTime; + } + + this->updateChatSettings(broadcasterID, moderatorID, json, successCallback, + failureCallback); +} + +void Helix::updateSubscriberMode( + QString broadcasterID, QString moderatorID, bool subscriberMode, + ResultCallback successCallback, + FailureCallback failureCallback) +{ + QJsonObject json; + json["subscriber_mode"] = subscriberMode; + this->updateChatSettings(broadcasterID, moderatorID, json, successCallback, + failureCallback); +} + +void Helix::updateUniqueChatMode( + QString broadcasterID, QString moderatorID, bool uniqueChatMode, + ResultCallback successCallback, + FailureCallback failureCallback) +{ + QJsonObject json; + json["unique_chat_mode"] = uniqueChatMode; + this->updateChatSettings(broadcasterID, moderatorID, json, successCallback, + failureCallback); +} + +void Helix::updateChatSettings( + QString broadcasterID, QString moderatorID, QJsonObject payload, + ResultCallback successCallback, + FailureCallback failureCallback) +{ + using Error = HelixUpdateChatSettingsError; + + QUrlQuery urlQuery; + + urlQuery.addQueryItem("broadcaster_id", broadcasterID); + urlQuery.addQueryItem("moderator_id", moderatorID); + + this->makePatch("chat/settings", urlQuery) + .json(payload) + .onSuccess([successCallback](auto result) { + if (result.status() != 200) + { + qCWarning(chatterinoTwitch) + << "Success result for updating chat settings was" + << result.formatError() << "but we expected it to be 200"; + } + auto response = result.parseJson(); + successCallback(HelixChatSettings( + response.value("data").toArray().first().toObject())); + }) + .onError([failureCallback](const auto &result) -> void { + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + + auto obj = result.parseJson(); + auto message = obj.value("message").toString(); + + switch (*result.status()) + { + case 400: { + if (message.contains("must be in the range")) + { + failureCallback(Error::OutOfRange, message); + } + else + { + failureCallback(Error::Forwarded, message); + } + } + break; + case 409: + case 422: + case 425: { + failureCallback(Error::Forwarded, message); + } + break; + + case 401: { + if (message.startsWith("Missing scope", + Qt::CaseInsensitive)) + { + // Handle this error specifically because its API error is especially unfriendly + failureCallback(Error::UserMissingScope, message); + } + else + { + failureCallback(Error::Forwarded, message); + } + } + break; + + case 403: { + failureCallback(Error::UserNotAuthorized, message); + } + break; + + case 429: { + failureCallback(Error::Ratelimited, message); + } + break; + + default: { + qCDebug(chatterinoTwitch) + << "Unhandled error updating chat settings:" + << result.formatError() << result.getData() << obj; + failureCallback(Error::Unknown, message); + } + break; + } + }) + .execute(); +} + +void Helix::onFetchChattersSuccess( + std::shared_ptr finalChatters, QString broadcasterID, + QString moderatorID, int maxChattersToFetch, + ResultCallback successCallback, + FailureCallback failureCallback, + HelixChatters chatters) +{ + qCDebug(chatterinoTwitch) + << "Fetched" << chatters.chatters.size() << "chatters"; + + finalChatters->chatters.merge(chatters.chatters); + finalChatters->total = chatters.total; + + if (chatters.cursor.isEmpty() || + finalChatters->chatters.size() >= maxChattersToFetch) + { + // Done paginating + successCallback(*finalChatters); + return; + } + + this->fetchChatters( + broadcasterID, moderatorID, NUM_CHATTERS_TO_FETCH, chatters.cursor, + [=, this](auto chatters) { + this->onFetchChattersSuccess( + finalChatters, broadcasterID, moderatorID, maxChattersToFetch, + successCallback, failureCallback, chatters); + }, + failureCallback); +} + +// https://dev.twitch.tv/docs/api/reference#get-chatters +void Helix::fetchChatters( + QString broadcasterID, QString moderatorID, int first, QString after, + ResultCallback successCallback, + FailureCallback failureCallback) +{ + using Error = HelixGetChattersError; + + QUrlQuery urlQuery; + + urlQuery.addQueryItem("broadcaster_id", broadcasterID); + urlQuery.addQueryItem("moderator_id", moderatorID); + urlQuery.addQueryItem("first", QString::number(first)); + + if (!after.isEmpty()) + { + urlQuery.addQueryItem("after", after); + } + + this->makeGet("chat/chatters", urlQuery) + .onSuccess([successCallback](auto result) { + if (result.status() != 200) + { + qCWarning(chatterinoTwitch) + << "Success result for getting chatters was " + << result.formatError() << "but we expected it to be 200"; + } + + auto response = result.parseJson(); + successCallback(HelixChatters(response)); + }) + .onError([failureCallback](const auto &result) -> void { + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + + auto obj = result.parseJson(); + auto message = obj.value("message").toString(); + + switch (*result.status()) + { + case 400: { + failureCallback(Error::Forwarded, message); + } + break; + + case 401: { + if (message.startsWith("Missing scope", + Qt::CaseInsensitive)) + { + failureCallback(Error::UserMissingScope, message); + } + else if (message.contains("OAuth token")) + { + failureCallback(Error::UserNotAuthorized, message); + } + else + { + failureCallback(Error::Forwarded, message); + } + } + break; + + case 403: { + failureCallback(Error::UserNotAuthorized, message); + } + break; + + default: { + qCDebug(chatterinoTwitch) + << "Unhandled error data:" << result.formatError() + << result.getData() << obj; + failureCallback(Error::Unknown, message); + } + break; + } + }) + .execute(); +} + +void Helix::onFetchModeratorsSuccess( + std::shared_ptr> finalModerators, + QString broadcasterID, int maxModeratorsToFetch, + ResultCallback> successCallback, + FailureCallback failureCallback, + HelixModerators moderators) +{ + qCDebug(chatterinoTwitch) + << "Fetched " << moderators.moderators.size() << " moderators"; + + std::for_each(moderators.moderators.begin(), moderators.moderators.end(), + [finalModerators](auto mod) { + finalModerators->push_back(mod); + }); + + if (moderators.cursor.isEmpty() || + finalModerators->size() >= maxModeratorsToFetch) + { + // Done paginating + successCallback(*finalModerators); + return; + } + + this->fetchModerators( + broadcasterID, NUM_MODERATORS_TO_FETCH_PER_REQUEST, moderators.cursor, + [=, this](auto moderators) { + this->onFetchModeratorsSuccess( + finalModerators, broadcasterID, maxModeratorsToFetch, + successCallback, failureCallback, moderators); + }, + failureCallback); +} + +// https://dev.twitch.tv/docs/api/reference#get-moderators +void Helix::fetchModerators( + QString broadcasterID, int first, QString after, + ResultCallback successCallback, + FailureCallback failureCallback) +{ + using Error = HelixGetModeratorsError; + + QUrlQuery urlQuery; + + urlQuery.addQueryItem("broadcaster_id", broadcasterID); + urlQuery.addQueryItem("first", QString::number(first)); + + if (!after.isEmpty()) + { + urlQuery.addQueryItem("after", after); + } + + this->makeGet("moderation/moderators", urlQuery) + .onSuccess([successCallback](auto result) { + if (result.status() != 200) + { + qCWarning(chatterinoTwitch) + << "Success result for getting moderators was " + << result.formatError() << "but we expected it to be 200"; + } + + auto response = result.parseJson(); + successCallback(HelixModerators(response)); + }) + .onError([failureCallback](const auto &result) -> void { + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + + auto obj = result.parseJson(); + auto message = obj.value("message").toString(); + + switch (*result.status()) + { + case 400: { + failureCallback(Error::Forwarded, message); + } + break; + + case 401: { + if (message.startsWith("Missing scope", + Qt::CaseInsensitive)) + { + failureCallback(Error::UserMissingScope, message); + } + else if (message.contains("OAuth token")) + { + failureCallback(Error::UserNotAuthorized, message); + } + else + { + failureCallback(Error::Forwarded, message); + } + } + break; + + case 403: { + failureCallback(Error::UserNotAuthorized, message); + } + break; + + default: { + qCDebug(chatterinoTwitch) + << "Unhandled error data:" << result.formatError() + << result.getData() << obj; + failureCallback(Error::Unknown, message); + } + break; + } + }) + .execute(); +} + +// Ban/timeout a user +// https://dev.twitch.tv/docs/api/reference#ban-user +void Helix::banUser(QString broadcasterID, QString moderatorID, QString userID, + std::optional duration, QString reason, + ResultCallback<> successCallback, + FailureCallback failureCallback) +{ + using Error = HelixBanUserError; + + QUrlQuery urlQuery; + + urlQuery.addQueryItem("broadcaster_id", broadcasterID); + urlQuery.addQueryItem("moderator_id", moderatorID); + + QJsonObject payload; + { + QJsonObject data; + data["reason"] = reason; + data["user_id"] = userID; + if (duration) + { + data["duration"] = *duration; + } + + payload["data"] = data; + } + + this->makePost("moderation/bans", urlQuery) + .json(payload) + .onSuccess([successCallback](auto result) { + if (result.status() != 200) + { + qCWarning(chatterinoTwitch) + << "Success result for banning a user was" + << result.formatError() << "but we expected it to be 200"; + } + // we don't care about the response + successCallback(); + }) + .onError([failureCallback](const auto &result) -> void { + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + + auto obj = result.parseJson(); + auto message = obj.value("message").toString(); + + switch (*result.status()) + { + case 400: { + if (message.startsWith("The user specified in the user_id " + "field is already banned", + Qt::CaseInsensitive)) + { + failureCallback(Error::TargetBanned, message); + } + else if (message.startsWith( + "The user specified in the user_id field may " + "not be banned", + Qt::CaseInsensitive)) + { + failureCallback(Error::CannotBanUser, message); + } + else + { + failureCallback(Error::Forwarded, message); + } + } + break; + + case 409: { + failureCallback(Error::ConflictingOperation, message); + } + break; + + case 401: { + if (message.startsWith("Missing scope", + Qt::CaseInsensitive)) + { + // Handle this error specifically because its API error is especially unfriendly + failureCallback(Error::UserMissingScope, message); + } + else + { + failureCallback(Error::Forwarded, message); + } + } + break; + + case 403: { + failureCallback(Error::UserNotAuthorized, message); + } + break; + + case 429: { + failureCallback(Error::Ratelimited, message); + } + break; + + default: { + qCDebug(chatterinoTwitch) + << "Unhandled error banning user:" + << result.formatError() << result.getData() << obj; + failureCallback(Error::Unknown, message); + } + break; + } + }) + .execute(); +} + +// Warn a user +// https://dev.twitch.tv/docs/api/reference#warn-chat-user +void Helix::warnUser( + QString broadcasterID, QString moderatorID, QString userID, QString reason, + ResultCallback<> successCallback, + FailureCallback failureCallback) +{ + using Error = HelixWarnUserError; + + QUrlQuery urlQuery; + + urlQuery.addQueryItem("broadcaster_id", broadcasterID); + urlQuery.addQueryItem("moderator_id", moderatorID); + + QJsonObject payload; + { + QJsonObject data; + data["reason"] = reason; + data["user_id"] = userID; + + payload["data"] = data; + } + + this->makePost("moderation/warnings", urlQuery) + .json(payload) + .onSuccess([successCallback](auto result) { + if (result.status() != 200) + { + qCWarning(chatterinoTwitch) + << "Success result for warning a user was" + << result.formatError() << "but we expected it to be 200"; + } + // we don't care about the response + successCallback(); + }) + .onError([failureCallback](const auto &result) -> void { + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + + auto obj = result.parseJson(); + auto message = obj.value("message").toString(); + + switch (*result.status()) + { + case 400: { + if (message.startsWith("The user specified in the user_id " + "field may not be warned", + Qt::CaseInsensitive)) + { + failureCallback(Error::CannotWarnUser, message); + } + else + { + failureCallback(Error::Forwarded, message); + } + } + break; + + case 401: { + if (message.startsWith("Missing scope", + Qt::CaseInsensitive)) + { + failureCallback(Error::UserMissingScope, message); + } + else + { + failureCallback(Error::Forwarded, message); + } + } + break; + + case 403: { + failureCallback(Error::UserNotAuthorized, message); + } + break; + + case 409: { + failureCallback(Error::ConflictingOperation, message); + } + break; + + case 429: { + failureCallback(Error::Ratelimited, message); + } + break; + + default: { + qCDebug(chatterinoTwitch) + << "Unhandled error warning user:" + << result.formatError() << result.getData() << obj; + failureCallback(Error::Unknown, message); + } + break; + } + }) + .execute(); +} + +// https://dev.twitch.tv/docs/api/reference#send-whisper +void Helix::sendWhisper( + QString fromUserID, QString toUserID, QString message, + ResultCallback<> successCallback, + FailureCallback failureCallback) +{ + using Error = HelixWhisperError; + + QUrlQuery urlQuery; + + urlQuery.addQueryItem("from_user_id", fromUserID); + urlQuery.addQueryItem("to_user_id", toUserID); + + QJsonObject payload; + payload["message"] = message; + + this->makePost("whispers", urlQuery) + .json(payload) + .onSuccess([successCallback](auto result) { + if (result.status() != 204) + { + qCWarning(chatterinoTwitch) + << "Success result for sending a whisper was" + << result.formatError() << "but we expected it to be 204"; + } + // we don't care about the response + successCallback(); + }) + .onError([failureCallback](const auto &result) -> void { + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + + auto obj = result.parseJson(); + auto message = obj.value("message").toString(); + + switch (*result.status()) + { + case 400: { + if (message.startsWith("A user cannot whisper themself", + Qt::CaseInsensitive)) + { + failureCallback(Error::WhisperSelf, message); + } + else + { + failureCallback(Error::Forwarded, message); + } + } + break; + + case 401: { + if (message.startsWith("Missing scope", + Qt::CaseInsensitive)) + { + // Handle this error specifically because its API error is especially unfriendly + failureCallback(Error::UserMissingScope, message); + } + else if (message.startsWith("the sender does not have a " + "verified phone number", + Qt::CaseInsensitive)) + { + failureCallback(Error::NoVerifiedPhone, message); + } + else + { + failureCallback(Error::Forwarded, message); + } + } + break; + + case 403: { + if (message.startsWith("The recipient's settings prevent " + "this sender from whispering them", + Qt::CaseInsensitive)) + { + failureCallback(Error::RecipientBlockedUser, message); + } + else + { + failureCallback(Error::UserNotAuthorized, message); + } + } + break; + + case 404: { + failureCallback(Error::Forwarded, message); + } + break; + + case 429: { + failureCallback(Error::Ratelimited, message); + } + break; + + default: { + qCDebug(chatterinoTwitch) + << "Unhandled error banning user:" + << result.formatError() << result.getData() << obj; + failureCallback(Error::Unknown, message); + } + break; + } + }) + .execute(); +} + +// https://dev.twitch.tv/docs/api/reference#get-chatters +void Helix::getChatters( + QString broadcasterID, QString moderatorID, int maxChattersToFetch, + ResultCallback successCallback, + FailureCallback failureCallback) +{ + auto finalChatters = std::make_shared(); + + // Initiate the recursive calls + this->fetchChatters( + broadcasterID, moderatorID, NUM_CHATTERS_TO_FETCH, "", + [=, this](auto chatters) { + this->onFetchChattersSuccess( + finalChatters, broadcasterID, moderatorID, maxChattersToFetch, + successCallback, failureCallback, chatters); + }, + failureCallback); +} + +// https://dev.twitch.tv/docs/api/reference#get-moderators +void Helix::getModerators( + QString broadcasterID, int maxModeratorsToFetch, + ResultCallback> successCallback, + FailureCallback failureCallback) +{ + auto finalModerators = std::make_shared>(); + + // Initiate the recursive calls + this->fetchModerators( + broadcasterID, NUM_MODERATORS_TO_FETCH_PER_REQUEST, "", + [=, this](auto moderators) { + this->onFetchModeratorsSuccess( + finalModerators, broadcasterID, maxModeratorsToFetch, + successCallback, failureCallback, moderators); + }, + failureCallback); +} + +// List the VIPs of a channel +// https://dev.twitch.tv/docs/api/reference#get-vips +void Helix::getChannelVIPs( + QString broadcasterID, + ResultCallback> successCallback, + FailureCallback failureCallback) +{ + using Error = HelixListVIPsError; + QUrlQuery urlQuery; + + urlQuery.addQueryItem("broadcaster_id", broadcasterID); + + // No point pagi/pajanating, Twitch's max VIP count doesn't go over 100 + // TODO(jammehcow): probably still implement pagination + // as the mod list can go over 100 (I assume, I see no limit) + urlQuery.addQueryItem("first", "100"); + + this->makeGet("channels/vips", urlQuery) + .header("Content-Type", "application/json") + .onSuccess([successCallback](auto result) { + if (result.status() != 200) + { + qCWarning(chatterinoTwitch) + << "Success result for getting VIPs was" + << result.formatError() << "but we expected it to be 200"; + } + + auto response = result.parseJson(); + + std::vector channelVips; + for (const auto &jsonStream : response.value("data").toArray()) + { + channelVips.emplace_back(jsonStream.toObject()); + } + + successCallback(channelVips); + }) + .onError([failureCallback](const auto &result) -> void { + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + + auto obj = result.parseJson(); + auto message = obj.value("message").toString(); + + switch (*result.status()) + { + case 400: { + failureCallback(Error::Forwarded, message); + } + break; + + case 401: { + if (message.startsWith("Missing scope", + Qt::CaseInsensitive)) + { + failureCallback(Error::UserMissingScope, message); + } + else if (message.compare( + "The ID in broadcaster_id must match the user " + "ID found in the request's OAuth token.", + Qt::CaseInsensitive) == 0) + { + // Must be the broadcaster. + failureCallback(Error::UserNotBroadcaster, message); + } + else + { + failureCallback(Error::Forwarded, message); + } + } + break; + + case 403: { + failureCallback(Error::UserNotAuthorized, message); + } + break; + + case 429: { + failureCallback(Error::Ratelimited, message); + } + break; + + default: { + qCDebug(chatterinoTwitch) + << "Unhandled error listing VIPs:" + << result.formatError() << result.getData() << obj; + failureCallback(Error::Unknown, message); + } + break; + } + }) + .execute(); +} + +void Helix::startCommercial( + QString broadcasterID, int length, + ResultCallback successCallback, + FailureCallback failureCallback) +{ + using Error = HelixStartCommercialError; + + QJsonObject payload; + + payload.insert("broadcaster_id", QJsonValue(broadcasterID)); + payload.insert("length", QJsonValue(length)); + + this->makePost("channels/commercial", QUrlQuery()) + .json(payload) + .onSuccess([successCallback, failureCallback](auto result) { + auto obj = result.parseJson(); + if (obj.isEmpty()) + { + failureCallback( + Error::Unknown, + "Twitch didn't send any information about this error."); + return; + } + + successCallback(HelixStartCommercialResponse(obj)); + }) + .onError([failureCallback](const auto &result) -> void { + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + + auto obj = result.parseJson(); + auto message = obj.value("message").toString(); + + switch (*result.status()) + { + case 400: { + if (message.startsWith("Missing scope", + Qt::CaseInsensitive)) + { + failureCallback(Error::UserMissingScope, message); + } + else if (message.contains( + "To start a commercial, the broadcaster must " + "be streaming live.", + Qt::CaseInsensitive)) + { + failureCallback(Error::BroadcasterNotStreaming, + message); + } + else if (message.startsWith("Missing required parameter", + Qt::CaseInsensitive)) + { + failureCallback(Error::MissingLengthParameter, message); + } + else + { + failureCallback(Error::Forwarded, message); + } + } + break; + + case 401: { + if (message.contains( + "The ID in broadcaster_id must match the user ID " + "found in the request's OAuth token.", + Qt::CaseInsensitive)) + { + failureCallback(Error::TokenMustMatchBroadcaster, + message); + } + else + { + failureCallback(Error::Forwarded, message); + } + } + break; + + case 429: { + // The cooldown period is implied to be included + // in the error's "retry_after" response field but isn't. + // If this becomes available we should append that to the error message. + failureCallback(Error::Ratelimited, message); + } + break; + + default: { + qCDebug(chatterinoTwitch) + << "Unhandled error starting commercial:" + << result.formatError() << result.getData() << obj; + failureCallback(Error::Unknown, message); + } + break; + } + }) + .execute(); +} + +// Twitch global badges +// https://dev.twitch.tv/docs/api/reference/#get-global-chat-badges +void Helix::getGlobalBadges( + ResultCallback successCallback, + FailureCallback failureCallback) +{ + using Error = HelixGetGlobalBadgesError; + + this->makeGet("chat/badges/global", QUrlQuery()) + .onSuccess([successCallback](auto result) { + if (result.status() != 200) + { + qCWarning(chatterinoTwitch) + << "Success result for getting global badges was " + << result.formatError() << "but we expected it to be 200"; + } + + auto response = result.parseJson(); + successCallback(HelixGlobalBadges(response)); + }) + .onError([failureCallback](const auto &result) -> void { + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + + auto obj = result.parseJson(); + auto message = obj.value("message").toString(); + + switch (*result.status()) + { + case 401: { + failureCallback(Error::Forwarded, message); + } + break; + + default: { + qCWarning(chatterinoTwitch) + << "Helix global badges, unhandled error data:" + << result.formatError() << result.getData() << obj; + failureCallback(Error::Unknown, message); + } + break; + } + }) + .execute(); +} + +// Badges for the `broadcasterID` channel +// https://dev.twitch.tv/docs/api/reference/#get-channel-chat-badges +void Helix::getChannelBadges( + QString broadcasterID, ResultCallback successCallback, + FailureCallback failureCallback) +{ + using Error = HelixGetChannelBadgesError; + + QUrlQuery urlQuery; + urlQuery.addQueryItem("broadcaster_id", broadcasterID); + + this->makeGet("chat/badges", urlQuery) + .onSuccess([successCallback](auto result) { + if (result.status() != 200) + { + qCWarning(chatterinoTwitch) + << "Success result for getting badges was " + << result.formatError() << "but we expected it to be 200"; + } + + auto response = result.parseJson(); + successCallback(HelixChannelBadges(response)); + }) + .onError([failureCallback](const auto &result) -> void { + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + + auto obj = result.parseJson(); + auto message = obj.value("message").toString(); + + switch (*result.status()) + { + case 400: + case 401: { + failureCallback(Error::Forwarded, message); + } + break; + + default: { + qCWarning(chatterinoTwitch) + << "Helix channel badges, unhandled error data:" + << result.formatError() << result.getData() << obj; + failureCallback(Error::Unknown, message); + } + break; + } + }) + .execute(); +} + +// https://dev.twitch.tv/docs/api/reference/#update-shield-mode-status +void Helix::updateShieldMode( + QString broadcasterID, QString moderatorID, bool isActive, + ResultCallback successCallback, + FailureCallback failureCallback) +{ + using Error = HelixUpdateShieldModeError; + + QUrlQuery urlQuery; + urlQuery.addQueryItem("broadcaster_id", broadcasterID); + urlQuery.addQueryItem("moderator_id", moderatorID); + + QJsonObject payload; + payload["is_active"] = isActive; + + this->makePut("moderation/shield_mode", urlQuery) + .json(payload) + .onSuccess([successCallback](auto result) { + if (result.status() != 200) + { + qCWarning(chatterinoTwitch) + << "Success result for updating shield mode was " + << result.formatError() << "but we expected it to be 200"; + } + + const auto response = result.parseJson(); + successCallback( + HelixShieldModeStatus(response["data"][0].toObject())); + }) + .onError([failureCallback](const auto &result) -> void { + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + + const auto obj = result.parseJson(); + auto message = obj["message"].toString(); + + switch (*result.status()) + { + case 400: { + if (message.startsWith("Missing scope", + Qt::CaseInsensitive)) + { + failureCallback(Error::UserMissingScope, message); + break; + } + + failureCallback(Error::Forwarded, message); + } + break; + case 401: { + failureCallback(Error::Forwarded, message); + } + break; + case 403: { + if (message.startsWith( + "Requester does not have permissions", + Qt::CaseInsensitive)) + { + failureCallback(Error::MissingPermission, message); + break; + } + } + + default: { + qCWarning(chatterinoTwitch) + << "Helix shield mode, unhandled error data:" + << result.formatError() << result.getData() << obj; + failureCallback(Error::Unknown, message); + } + break; + } + }) + .execute(); +} + +// https://dev.twitch.tv/docs/api/reference/#send-a-shoutout +void Helix::sendShoutout( + QString fromBroadcasterID, QString toBroadcasterID, QString moderatorID, + ResultCallback<> successCallback, + FailureCallback failureCallback) +{ + using Error = HelixSendShoutoutError; + + QUrlQuery urlQuery; + urlQuery.addQueryItem("from_broadcaster_id", fromBroadcasterID); + urlQuery.addQueryItem("to_broadcaster_id", toBroadcasterID); + urlQuery.addQueryItem("moderator_id", moderatorID); + + this->makePost("chat/shoutouts", urlQuery) + .header("Content-Type", "application/json") + .onSuccess([successCallback](NetworkResult result) { + if (result.status() != 204) + { + qCWarning(chatterinoTwitch) + << "Success result for sending shoutout was " + << result.formatError() << "but we expected it to be 204"; + } + + successCallback(); + }) + .onError([failureCallback](const NetworkResult &result) -> void { + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + + const auto obj = result.parseJson(); + auto message = obj["message"].toString(); + + switch (*result.status()) + { + case 400: { + if (message.startsWith("The broadcaster may not give " + "themselves a Shoutout.", + Qt::CaseInsensitive)) + { + failureCallback(Error::UserIsBroadcaster, message); + } + else if (message.startsWith( + "The broadcaster is not streaming live or " + "does not have one or more viewers.", + Qt::CaseInsensitive)) + { + failureCallback(Error::BroadcasterNotLive, message); + } + else + { + failureCallback(Error::UserNotAuthorized, message); + } + } + break; + + case 401: { + if (message.startsWith("Missing scope", + Qt::CaseInsensitive)) + { + failureCallback(Error::UserMissingScope, message); + } + else + { + failureCallback(Error::UserNotAuthorized, message); + } + } + break; + + case 403: { + failureCallback(Error::UserNotAuthorized, message); + } + break; + + case 429: { + failureCallback(Error::Ratelimited, message); + } + break; + + case 500: { + if (message.isEmpty()) + { + failureCallback(Error::Unknown, + "Twitch internal server error"); + } + else + { + failureCallback(Error::Unknown, message); + } + } + break; + + default: { + qCWarning(chatterinoTwitch) + << "Helix send shoutout, unhandled error data:" + << result.formatError() << result.getData() << obj; + failureCallback(Error::Unknown, message); + } + } + }) + .execute(); +} + +// https://dev.twitch.tv/docs/api/reference/#send-chat-message +void Helix::sendChatMessage( + HelixSendMessageArgs args, ResultCallback successCallback, + FailureCallback failureCallback) +{ + using Error = HelixSendMessageError; + + QJsonObject json{{ + {"broadcaster_id", args.broadcasterID}, + {"sender_id", args.senderID}, + {"message", args.message}, + }}; + if (!args.replyParentMessageID.isEmpty()) + { + json["reply_parent_message_id"] = args.replyParentMessageID; + } + + this->makePost("chat/messages", {}) + .json(json) + .onSuccess([successCallback](const NetworkResult &result) { + if (result.status() != 200) + { + qCWarning(chatterinoTwitch) + << "Success result for sending chat message was " + << result.formatError() << "but we expected it to be 200"; + } + auto json = result.parseJson(); + + successCallback(HelixSentMessage( + json.value("data").toArray().at(0).toObject())); + }) + .onError([failureCallback](const NetworkResult &result) -> void { + if (!result.status()) + { + failureCallback(Error::Unknown, result.formatError()); + return; + } + + const auto obj = result.parseJson(); + auto message = + obj["message"].toString(u"Twitch internal server error"_s); + + switch (*result.status()) + { + case 400: { + failureCallback(Error::Unknown, message); + } + break; + + case 401: { + if (message.startsWith("User access token requires the", + Qt::CaseInsensitive)) + { + failureCallback(Error::UserMissingScope, message); + } + else + { + failureCallback(Error::Forwarded, message); + } + } + break; + + case 403: { + failureCallback(Error::Forbidden, message); + } + break; + + case 422: { + failureCallback(Error::MessageTooLarge, message); + } + break; + + case 500: { + failureCallback(Error::Unknown, message); + } + break; + + default: { + qCWarning(chatterinoTwitch) + << "Helix send chat message, unhandled error data:" + << result.formatError() << result.getData() << obj; + failureCallback(Error::Unknown, message); + } + } + }) + .execute(); +} + +NetworkRequest Helix::makeRequest(const QString &url, const QUrlQuery &urlQuery, + NetworkRequestType type) { assert(!url.startsWith("/")); @@ -778,14 +3072,14 @@ NetworkRequest Helix::makeRequest(QString url, QUrlQuery urlQuery) { qCDebug(chatterinoTwitch) << "Helix::makeRequest called without a client ID set BabyRage"; - // return boost::none; + // return std::nullopt; } if (this->oauthToken.isEmpty()) { qCDebug(chatterinoTwitch) << "Helix::makeRequest called without an oauth token set BabyRage"; - // return boost::none; + // return std::nullopt; } const QString baseUrl("https://api.twitch.tv/helix/"); @@ -794,13 +3088,89 @@ NetworkRequest Helix::makeRequest(QString url, QUrlQuery urlQuery) fullUrl.setQuery(urlQuery); - return NetworkRequest(fullUrl) + return NetworkRequest(fullUrl, type) .timeout(5 * 1000) .header("Accept", "application/json") .header("Client-ID", this->clientId) .header("Authorization", "Bearer " + this->oauthToken); } +NetworkRequest Helix::makeGet(const QString &url, const QUrlQuery &urlQuery) +{ + return this->makeRequest(url, urlQuery, NetworkRequestType::Get); +} + +NetworkRequest Helix::makeDelete(const QString &url, const QUrlQuery &urlQuery) +{ + return this->makeRequest(url, urlQuery, NetworkRequestType::Delete); +} + +NetworkRequest Helix::makePost(const QString &url, const QUrlQuery &urlQuery) +{ + return this->makeRequest(url, urlQuery, NetworkRequestType::Post); +} + +NetworkRequest Helix::makePut(const QString &url, const QUrlQuery &urlQuery) +{ + return this->makeRequest(url, urlQuery, NetworkRequestType::Put); +} + +NetworkRequest Helix::makePatch(const QString &url, const QUrlQuery &urlQuery) +{ + return this->makeRequest(url, urlQuery, NetworkRequestType::Patch); +} + +void Helix::paginate(const QString &url, const QUrlQuery &baseQuery, + std::function onPage, + std::function onError, + CancellationToken &&cancellationToken) +{ + auto onSuccess = + std::make_shared>(nullptr); + // This is the actual callback passed to NetworkRequest. + // It wraps the shared-ptr. + auto onSuccessCb = [onSuccess](const auto &res) { + return (*onSuccess)(res); + }; + + *onSuccess = [this, onPage = std::move(onPage), onError, onSuccessCb, + url{url}, baseQuery{baseQuery}, + cancellationToken = + std::move(cancellationToken)](const NetworkResult &res) { + if (cancellationToken.isCancelled()) + { + return; + } + + const auto json = res.parseJson(); + if (!onPage(json)) + { + // The consumer doesn't want any more pages + return; + } + + auto cursor = json["pagination"_L1]["cursor"_L1].toString(); + if (cursor.isEmpty()) + { + return; + } + + auto query = baseQuery; + query.removeAllQueryItems(u"after"_s); + query.addQueryItem(u"after"_s, cursor); + + this->makeGet(url, query) + .onSuccess(onSuccessCb) + .onError(onError) + .execute(); + }; + + this->makeGet(url, baseQuery) + .onSuccess(std::move(onSuccessCb)) + .onError(std::move(onError)) + .execute(); +} + void Helix::update(QString clientId, QString oauthToken) { this->clientId = std::move(clientId); diff --git a/src/providers/twitch/api/Helix.hpp b/src/providers/twitch/api/Helix.hpp index 6e7ef55a6..9fab2648e 100644 --- a/src/providers/twitch/api/Helix.hpp +++ b/src/providers/twitch/api/Helix.hpp @@ -1,17 +1,23 @@ #pragma once #include "common/Aliases.hpp" -#include "common/NetworkRequest.hpp" +#include "common/network/NetworkRequest.hpp" #include "providers/twitch/TwitchEmotes.hpp" +#include "util/Helpers.hpp" +#include "util/QStringHash.hpp" +#include #include +#include #include #include +#include #include #include -#include #include +#include +#include #include namespace chatterino { @@ -20,6 +26,8 @@ using HelixFailureCallback = std::function; template using ResultCallback = std::function; +class CancellationToken; + struct HelixUser { QString id; QString login; @@ -39,44 +47,12 @@ struct HelixUser { } }; -struct HelixUsersFollowsRecord { - QString fromId; - QString fromName; - QString toId; - QString toName; - QString followedAt; // date time object - - HelixUsersFollowsRecord() - : fromId("") - , fromName("") - , toId("") - , toName("") - , followedAt("") - { - } - - explicit HelixUsersFollowsRecord(QJsonObject jsonObject) - : fromId(jsonObject.value("from_id").toString()) - , fromName(jsonObject.value("from_name").toString()) - , toId(jsonObject.value("to_id").toString()) - , toName(jsonObject.value("to_name").toString()) - , followedAt(jsonObject.value("followed_at").toString()) - { - } -}; - -struct HelixUsersFollowsResponse { +struct HelixGetChannelFollowersResponse { int total; - std::vector data; - explicit HelixUsersFollowsResponse(QJsonObject jsonObject) + + explicit HelixGetChannelFollowersResponse(const QJsonObject &jsonObject) : total(jsonObject.value("total").toInt()) { - const auto &jsonData = jsonObject.value("data").toArray(); - std::transform(jsonData.begin(), jsonData.end(), - std::back_inserter(this->data), - [](const QJsonValue &record) { - return HelixUsersFollowsRecord(record.toObject()); - }); } }; @@ -94,6 +70,9 @@ struct HelixStream { QString language; QString thumbnailUrl; + // This is the names, the IDs are now always empty + std::vector tags; + HelixStream() : id("") , userId("") @@ -124,6 +103,11 @@ struct HelixStream { , language(jsonObject.value("language").toString()) , thumbnailUrl(jsonObject.value("thumbnail_url").toString()) { + const auto jsonTags = jsonObject.value("tags").toArray(); + for (const auto &tag : jsonTags) + { + this->tags.push_back(tag.toString()); + } } }; @@ -293,13 +277,185 @@ struct HelixChannelEmote { , name(jsonObject.value("name").toString()) , type(jsonObject.value("emote_type").toString()) , setId(jsonObject.value("emote_set_id").toString()) - , url(QString(TWITCH_EMOTE_TEMPLATE) - .replace("{id}", this->emoteId) - .replace("{scale}", "3.0")) + , url(TWITCH_EMOTE_TEMPLATE.arg(this->emoteId, u"3.0")) { } }; +struct HelixChatSettings { + const QString broadcasterId; + const bool emoteMode; + // std::nullopt if disabled + const std::optional followerModeDuration; // time in minutes + const std::optional nonModeratorChatDelayDuration; // time in seconds + const std::optional slowModeWaitTime; // time in seconds + const bool subscriberMode; + const bool uniqueChatMode; + + explicit HelixChatSettings(QJsonObject jsonObject) + : broadcasterId(jsonObject.value("broadcaster_id").toString()) + , emoteMode(jsonObject.value("emote_mode").toBool()) + , followerModeDuration(makeConditionedOptional( + jsonObject.value("follower_mode").toBool(), + jsonObject.value("follower_mode_duration").toInt())) + , nonModeratorChatDelayDuration(makeConditionedOptional( + jsonObject.value("non_moderator_chat_delay").toBool(), + jsonObject.value("non_moderator_chat_delay_duration").toInt())) + , slowModeWaitTime(makeConditionedOptional( + jsonObject.value("slow_mode").toBool(), + jsonObject.value("slow_mode_wait_time").toInt())) + , subscriberMode(jsonObject.value("subscriber_mode").toBool()) + , uniqueChatMode(jsonObject.value("unique_chat_mode").toBool()) + { + } +}; + +struct HelixVip { + // Twitch ID of the user + QString userId; + + // Display name of the user + QString userName; + + // Login name of the user + QString userLogin; + + explicit HelixVip(const QJsonObject &jsonObject) + : userId(jsonObject.value("user_id").toString()) + , userName(jsonObject.value("user_name").toString()) + , userLogin(jsonObject.value("user_login").toString()) + { + } +}; + +struct HelixChatters { + std::unordered_set chatters; + int total{}; + QString cursor; + + HelixChatters() = default; + + explicit HelixChatters(const QJsonObject &jsonObject); +}; + +using HelixModerator = HelixVip; + +struct HelixModerators { + std::vector moderators; + QString cursor; + + HelixModerators() = default; + + explicit HelixModerators(const QJsonObject &jsonObject) + : cursor(jsonObject.value("pagination") + .toObject() + .value("cursor") + .toString()) + { + const auto &data = jsonObject.value("data").toArray(); + for (const auto &mod : data) + { + HelixModerator moderator(mod.toObject()); + + this->moderators.push_back(moderator); + } + } +}; + +struct HelixBadgeVersion { + QString id; + Url imageURL1x; + Url imageURL2x; + Url imageURL4x; + QString title; + Url clickURL; + + explicit HelixBadgeVersion(const QJsonObject &jsonObject) + : id(jsonObject.value("id").toString()) + , imageURL1x(Url{jsonObject.value("image_url_1x").toString()}) + , imageURL2x(Url{jsonObject.value("image_url_2x").toString()}) + , imageURL4x(Url{jsonObject.value("image_url_4x").toString()}) + , title(jsonObject.value("title").toString()) + , clickURL(Url{jsonObject.value("click_url").toString()}) + { + } +}; + +struct HelixBadgeSet { + QString setID; + std::vector versions; + + explicit HelixBadgeSet(const QJsonObject &json) + : setID(json.value("set_id").toString()) + { + const auto jsonVersions = json.value("versions").toArray(); + for (const auto &version : jsonVersions) + { + versions.emplace_back(version.toObject()); + } + } +}; + +struct HelixGlobalBadges { + std::vector badgeSets; + + explicit HelixGlobalBadges(const QJsonObject &jsonObject) + { + const auto &data = jsonObject.value("data").toArray(); + for (const auto &set : data) + { + this->badgeSets.emplace_back(set.toObject()); + } + } +}; + +using HelixChannelBadges = HelixGlobalBadges; + +struct HelixDropReason { + QString code; + QString message; + + explicit HelixDropReason(const QJsonObject &jsonObject) + : code(jsonObject["code"].toString()) + , message(jsonObject["message"].toString()) + { + } +}; + +struct HelixSentMessage { + QString id; + bool isSent; + std::optional dropReason; + + explicit HelixSentMessage(const QJsonObject &jsonObject) + : id(jsonObject["message_id"].toString()) + , isSent(jsonObject["is_sent"].toBool()) + , dropReason(jsonObject.contains("drop_reason") + ? std::optional(HelixDropReason( + jsonObject["drop_reason"].toObject())) + : std::nullopt) + { + } +}; + +struct HelixSendMessageArgs { + QString broadcasterID; + QString senderID; + QString message; + /// Optional + QString replyParentMessageID; +}; + +enum class HelixAnnouncementColor { + Blue, + Green, + Orange, + Purple, + + // this is the executor's chat color + Primary, +}; + enum class HelixClipError { Unknown, ClipsDisabled, @@ -320,9 +476,322 @@ enum class HelixAutoModMessageError { MessageNotFound, }; +enum class HelixUpdateUserChatColorError { + Unknown, + UserMissingScope, + InvalidColor, + + // The error message is forwarded directly from the Twitch API + Forwarded, +}; + +enum class HelixDeleteChatMessagesError { + Unknown, + UserMissingScope, + UserNotAuthenticated, + UserNotAuthorized, + MessageUnavailable, + + // The error message is forwarded directly from the Twitch API + Forwarded, +}; + +enum class HelixSendChatAnnouncementError { + Unknown, + UserMissingScope, + + // The error message is forwarded directly from the Twitch API + Forwarded, +}; + +enum class HelixAddChannelModeratorError { + Unknown, + UserMissingScope, + UserNotAuthorized, + Ratelimited, + TargetAlreadyModded, + TargetIsVIP, + + // The error message is forwarded directly from the Twitch API + Forwarded, +}; + +enum class HelixRemoveChannelModeratorError { + Unknown, + UserMissingScope, + UserNotAuthorized, + TargetNotModded, + Ratelimited, + + // The error message is forwarded directly from the Twitch API + Forwarded, +}; + +enum class HelixAddChannelVIPError { + Unknown, + UserMissingScope, + UserNotAuthorized, + Ratelimited, + + // The error message is forwarded directly from the Twitch API + Forwarded, +}; + +enum class HelixRemoveChannelVIPError { + Unknown, + UserMissingScope, + UserNotAuthorized, + Ratelimited, + + // The error message is forwarded directly from the Twitch API + Forwarded, +}; + +// These changes are from the helix-command-migration/unban-untimeout branch +enum class HelixUnbanUserError { + Unknown, + UserMissingScope, + UserNotAuthorized, + Ratelimited, + ConflictingOperation, + TargetNotBanned, + + // The error message is forwarded directly from the Twitch API + Forwarded, +}; // These changes are from the helix-command-migration/unban-untimeout branch + +enum class HelixStartRaidError { // /raid + Unknown, + UserMissingScope, + UserNotAuthorized, + CantRaidYourself, + Ratelimited, + + // The error message is forwarded directly from the Twitch API + Forwarded, +}; // /raid + +enum class HelixCancelRaidError { // /unraid + Unknown, + UserMissingScope, + UserNotAuthorized, + NoRaidPending, + Ratelimited, + + // The error message is forwarded directly from the Twitch API + Forwarded, +}; // /unraid + +enum class HelixUpdateChatSettingsError { // update chat settings + Unknown, + UserMissingScope, + UserNotAuthorized, + Ratelimited, + Forbidden, + OutOfRange, + + // The error message is forwarded directly from the Twitch API + Forwarded, +}; // update chat settings + +/// Error type for Helix::updateChannel +/// +/// Used in the /settitle and /setgame commands +enum class HelixUpdateChannelError { + Unknown, + UserMissingScope, + UserNotAuthorized, + Ratelimited, + + // The error message is forwarded directly from the Twitch API + Forwarded, +}; + +enum class HelixBanUserError { // /timeout, /ban + Unknown, + UserMissingScope, + UserNotAuthorized, + Ratelimited, + ConflictingOperation, + TargetBanned, + CannotBanUser, + + // The error message is forwarded directly from the Twitch API + Forwarded, +}; // /timeout, /ban + +enum class HelixWarnUserError { // /warn + Unknown, + UserMissingScope, + UserNotAuthorized, + Ratelimited, + ConflictingOperation, + CannotWarnUser, + + // The error message is forwarded directly from the Twitch API + Forwarded, +}; // /warn + +enum class HelixWhisperError { // /w + Unknown, + UserMissingScope, + UserNotAuthorized, + Ratelimited, + NoVerifiedPhone, + RecipientBlockedUser, + WhisperSelf, + + // The error message is forwarded directly from the Twitch API + Forwarded, +}; // /w + +enum class HelixGetChattersError { + Unknown, + UserMissingScope, + UserNotAuthorized, + + // The error message is forwarded directly from the Twitch API + Forwarded, +}; + +enum class HelixGetModeratorsError { + Unknown, + UserMissingScope, + UserNotAuthorized, + + // The error message is forwarded directly from the Twitch API + Forwarded, +}; + +enum class HelixListVIPsError { // /vips + Unknown, + UserMissingScope, + UserNotAuthorized, + UserNotBroadcaster, + Ratelimited, + + // The error message is forwarded directly from the Twitch API + Forwarded, +}; // /vips + +enum class HelixSendShoutoutError { + Unknown, + // 400 + UserIsBroadcaster, + BroadcasterNotLive, + // 401 + UserNotAuthorized, + UserMissingScope, + + Ratelimited, +}; + +struct HelixStartCommercialResponse { + // Length of the triggered commercial + int length; + // Provides contextual information on why the request failed + QString message; + // Seconds until the next commercial can be served on this channel + int retryAfter; + + explicit HelixStartCommercialResponse(const QJsonObject &jsonObject) + { + auto jsonData = jsonObject.value("data").toArray().at(0).toObject(); + this->length = jsonData.value("length").toInt(); + this->message = jsonData.value("message").toString(); + this->retryAfter = jsonData.value("retry_after").toInt(); + } +}; + +struct HelixShieldModeStatus { + /// A Boolean value that determines whether Shield Mode is active. Is `true` if Shield Mode is active; otherwise, `false`. + bool isActive; + /// An ID that identifies the moderator that last activated Shield Mode. + QString moderatorID; + /// The moderator's login name. + QString moderatorLogin; + /// The moderator's display name. + QString moderatorName; + /// The UTC timestamp of when Shield Mode was last activated. + QDateTime lastActivatedAt; + + explicit HelixShieldModeStatus(const QJsonObject &json) + : isActive(json["is_active"].toBool()) + , moderatorID(json["moderator_id"].toString()) + , moderatorLogin(json["moderator_login"].toString()) + , moderatorName(json["moderator_name"].toString()) + , lastActivatedAt(QDateTime::fromString( + json["last_activated_at"].toString(), Qt::ISODate)) + { + this->lastActivatedAt.setTimeZone(QTimeZone::utc()); + } +}; + +enum class HelixUpdateShieldModeError { + Unknown, + UserMissingScope, + MissingPermission, + + // The error message is forwarded directly from the Twitch API + Forwarded, +}; + +enum class HelixStartCommercialError { + Unknown, + TokenMustMatchBroadcaster, + UserMissingScope, + BroadcasterNotStreaming, + MissingLengthParameter, + Ratelimited, + + // The error message is forwarded directly from the Twitch API + Forwarded, +}; + +enum class HelixGetGlobalBadgesError { + Unknown, + + // The error message is forwarded directly from the Twitch API + Forwarded, +}; + +enum class HelixSendMessageError { + Unknown, + + MissingText, + BadRequest, + Forbidden, + MessageTooLarge, + UserMissingScope, + + // The error message is forwarded directly from the Twitch API + Forwarded, +}; + +struct HelixError { + /// Text version of the HTTP error that happened (e.g. Bad Request) + QString error; + /// Number version of the HTTP error that happened (e.g. 400) + int status; + /// The error message string + QString message; + + explicit HelixError(const QJsonObject &json) + : error(json["error"].toString()) + , status(json["status"].toInt()) + , message(json["message"].toString()) + { + } +}; + +using HelixGetChannelBadgesError = HelixGetGlobalBadgesError; + class IHelix { public: + template + using FailureCallback = std::function; + // https://dev.twitch.tv/docs/api/reference#get-users virtual void fetchUsers( QStringList userIds, QStringList userLogins, @@ -335,16 +804,11 @@ public: ResultCallback successCallback, HelixFailureCallback failureCallback) = 0; - // https://dev.twitch.tv/docs/api/reference#get-users-follows - virtual void fetchUsersFollows( - QString fromId, QString toId, - ResultCallback successCallback, - HelixFailureCallback failureCallback) = 0; - - virtual void getUserFollowers( - QString userId, - ResultCallback successCallback, - HelixFailureCallback failureCallback) = 0; + // https://dev.twitch.tv/docs/api/reference/#get-channel-followers + virtual void getChannelFollowers( + QString broadcasterID, + ResultCallback successCallback, + std::function failureCallback) = 0; // https://dev.twitch.tv/docs/api/reference#get-streams virtual void fetchStreams( @@ -385,6 +849,12 @@ public: std::function failureCallback, std::function finallyCallback) = 0; + // https://dev.twitch.tv/docs/api/reference#get-channel-information + virtual void fetchChannels( + QStringList userIDs, + ResultCallback> successCallback, + HelixFailureCallback failureCallback) = 0; + // https://dev.twitch.tv/docs/api/reference#get-channel-information virtual void getChannel(QString broadcasterId, ResultCallback successCallback, @@ -398,16 +868,17 @@ public: // https://dev.twitch.tv/docs/api/reference#get-user-block-list virtual void loadBlocks( - QString userId, ResultCallback> successCallback, - HelixFailureCallback failureCallback) = 0; + QString userId, ResultCallback> pageCallback, + FailureCallback failureCallback, + CancellationToken &&token) = 0; // https://dev.twitch.tv/docs/api/reference#block-user - virtual void blockUser(QString targetUserId, + virtual void blockUser(QString targetUserId, const QObject *caller, std::function successCallback, HelixFailureCallback failureCallback) = 0; // https://dev.twitch.tv/docs/api/reference#unblock-user - virtual void unblockUser(QString targetUserId, + virtual void unblockUser(QString targetUserId, const QObject *caller, std::function successCallback, HelixFailureCallback failureCallback) = 0; @@ -415,7 +886,7 @@ public: virtual void updateChannel( QString broadcasterId, QString gameId, QString language, QString title, std::function successCallback, - HelixFailureCallback failureCallback) = 0; + FailureCallback failureCallback) = 0; // https://dev.twitch.tv/docs/api/reference#manage-held-automod-messages virtual void manageAutoModMessages( @@ -440,7 +911,216 @@ public: ResultCallback> successCallback, HelixFailureCallback failureCallback) = 0; + // https://dev.twitch.tv/docs/api/reference#update-user-chat-color + virtual void updateUserChatColor( + QString userID, QString color, ResultCallback<> successCallback, + FailureCallback + failureCallback) = 0; + + // https://dev.twitch.tv/docs/api/reference#delete-chat-messages + virtual void deleteChatMessages( + QString broadcasterID, QString moderatorID, QString messageID, + ResultCallback<> successCallback, + FailureCallback + failureCallback) = 0; + + // https://dev.twitch.tv/docs/api/reference#add-channel-moderator + virtual void addChannelModerator( + QString broadcasterID, QString userID, ResultCallback<> successCallback, + FailureCallback + failureCallback) = 0; + + // https://dev.twitch.tv/docs/api/reference#remove-channel-moderator + virtual void removeChannelModerator( + QString broadcasterID, QString userID, ResultCallback<> successCallback, + FailureCallback + failureCallback) = 0; + + // https://dev.twitch.tv/docs/api/reference#send-chat-announcement + virtual void sendChatAnnouncement( + QString broadcasterID, QString moderatorID, QString message, + HelixAnnouncementColor color, ResultCallback<> successCallback, + FailureCallback + failureCallback) = 0; + + // https://dev.twitch.tv/docs/api/reference#add-channel-vip + virtual void addChannelVIP( + QString broadcasterID, QString userID, ResultCallback<> successCallback, + FailureCallback failureCallback) = 0; + + // https://dev.twitch.tv/docs/api/reference#remove-channel-vip + virtual void removeChannelVIP( + QString broadcasterID, QString userID, ResultCallback<> successCallback, + FailureCallback + failureCallback) = 0; + + // These changes are from the helix-command-migration/unban-untimeout branch + // https://dev.twitch.tv/docs/api/reference#unban-user + // These changes are from the helix-command-migration/unban-untimeout branch + virtual void unbanUser( + QString broadcasterID, QString moderatorID, QString userID, + ResultCallback<> successCallback, + FailureCallback failureCallback) = 0; + // These changes are from the helix-command-migration/unban-untimeout branch + + // https://dev.twitch.tv/docs/api/reference#start-a-raid + virtual void startRaid( + QString fromBroadcasterID, QString toBroadcasterID, + ResultCallback<> successCallback, + FailureCallback failureCallback) = 0; + // https://dev.twitch.tv/docs/api/reference#start-a-raid + + // https://dev.twitch.tv/docs/api/reference#cancel-a-raid + virtual void cancelRaid( + QString broadcasterID, ResultCallback<> successCallback, + FailureCallback failureCallback) = 0; + // https://dev.twitch.tv/docs/api/reference#cancel-a-raid + + // Updates the emote mode using + // https://dev.twitch.tv/docs/api/reference#update-chat-settings + virtual void updateEmoteMode( + QString broadcasterID, QString moderatorID, bool emoteMode, + ResultCallback successCallback, + FailureCallback + failureCallback) = 0; + + // Updates the follower mode using + // https://dev.twitch.tv/docs/api/reference#update-chat-settings + virtual void updateFollowerMode( + QString broadcasterID, QString moderatorID, + std::optional followerModeDuration, + ResultCallback successCallback, + FailureCallback + failureCallback) = 0; + + // Updates the non-moderator chat delay using + // https://dev.twitch.tv/docs/api/reference#update-chat-settings + virtual void updateNonModeratorChatDelay( + QString broadcasterID, QString moderatorID, + std::optional nonModeratorChatDelayDuration, + ResultCallback successCallback, + FailureCallback + failureCallback) = 0; + + // Updates the slow mode using + // https://dev.twitch.tv/docs/api/reference#update-chat-settings + virtual void updateSlowMode( + QString broadcasterID, QString moderatorID, + std::optional slowModeWaitTime, + ResultCallback successCallback, + FailureCallback + failureCallback) = 0; + + // Updates the subscriber mode using + // https://dev.twitch.tv/docs/api/reference#update-chat-settings + virtual void updateSubscriberMode( + QString broadcasterID, QString moderatorID, bool subscriberMode, + ResultCallback successCallback, + FailureCallback + failureCallback) = 0; + + // Updates the unique chat mode using + // https://dev.twitch.tv/docs/api/reference#update-chat-settings + virtual void updateUniqueChatMode( + QString broadcasterID, QString moderatorID, bool uniqueChatMode, + ResultCallback successCallback, + FailureCallback + failureCallback) = 0; + + // Ban/timeout a user + // https://dev.twitch.tv/docs/api/reference#ban-user + virtual void banUser( + QString broadcasterID, QString moderatorID, QString userID, + std::optional duration, QString reason, + ResultCallback<> successCallback, + FailureCallback failureCallback) = 0; + + // Warn a user + // https://dev.twitch.tv/docs/api/reference#warn-chat-user + virtual void warnUser( + QString broadcasterID, QString moderatorID, QString userID, + QString reason, ResultCallback<> successCallback, + FailureCallback failureCallback) = 0; + + // Send a whisper + // https://dev.twitch.tv/docs/api/reference#send-whisper + virtual void sendWhisper( + QString fromUserID, QString toUserID, QString message, + ResultCallback<> successCallback, + FailureCallback failureCallback) = 0; + + // Get Chatters from the `broadcasterID` channel + // This will follow the returned cursor and return up to `maxChattersToFetch` chatters + // https://dev.twitch.tv/docs/api/reference#get-chatters + virtual void getChatters( + QString broadcasterID, QString moderatorID, int maxChattersToFetch, + ResultCallback successCallback, + FailureCallback failureCallback) = 0; + + // Get moderators from the `broadcasterID` channel + // This will follow the returned cursor + // https://dev.twitch.tv/docs/api/reference#get-moderators + virtual void getModerators( + QString broadcasterID, int maxModeratorsToFetch, + ResultCallback> successCallback, + FailureCallback failureCallback) = 0; + + // https://dev.twitch.tv/docs/api/reference#get-vips + virtual void getChannelVIPs( + QString broadcasterID, + ResultCallback> successCallback, + FailureCallback failureCallback) = 0; + + // https://dev.twitch.tv/docs/api/reference#start-commercial + virtual void startCommercial( + QString broadcasterID, int length, + ResultCallback successCallback, + FailureCallback + failureCallback) = 0; + + // Get global Twitch badges + // https://dev.twitch.tv/docs/api/reference/#get-global-chat-badges + virtual void getGlobalBadges( + ResultCallback successCallback, + FailureCallback + failureCallback) = 0; + + // Get badges for the `broadcasterID` channel + // https://dev.twitch.tv/docs/api/reference/#get-channel-chat-badges + virtual void getChannelBadges( + QString broadcasterID, + ResultCallback successCallback, + FailureCallback + failureCallback) = 0; + + // https://dev.twitch.tv/docs/api/reference/#update-shield-mode-status + virtual void updateShieldMode( + QString broadcasterID, QString moderatorID, bool isActive, + ResultCallback successCallback, + FailureCallback + failureCallback) = 0; + + // https://dev.twitch.tv/docs/api/reference/#send-a-shoutout + virtual void sendShoutout( + QString fromBroadcasterID, QString toBroadcasterID, QString moderatorID, + ResultCallback<> successCallback, + FailureCallback failureCallback) = 0; + + /// https://dev.twitch.tv/docs/api/reference/#send-chat-message + virtual void sendChatMessage( + HelixSendMessageArgs args, + ResultCallback successCallback, + FailureCallback failureCallback) = 0; + virtual void update(QString clientId, QString oauthToken) = 0; + +protected: + // https://dev.twitch.tv/docs/api/reference#update-chat-settings + virtual void updateChatSettings( + QString broadcasterID, QString moderatorID, QJsonObject json, + ResultCallback successCallback, + FailureCallback + failureCallback) = 0; }; class Helix final : public IHelix @@ -456,16 +1136,11 @@ public: void getUserById(QString userId, ResultCallback successCallback, HelixFailureCallback failureCallback) final; - // https://dev.twitch.tv/docs/api/reference#get-users-follows - void fetchUsersFollows( - QString fromId, QString toId, - ResultCallback successCallback, - HelixFailureCallback failureCallback) final; - - void getUserFollowers( - QString userId, - ResultCallback successCallback, - HelixFailureCallback failureCallback) final; + // https://dev.twitch.tv/docs/api/reference/#get-channel-followers + void getChannelFollowers( + QString broadcasterID, + ResultCallback successCallback, + std::function failureCallback) final; // https://dev.twitch.tv/docs/api/reference#get-streams void fetchStreams(QStringList userIds, QStringList userLogins, @@ -502,6 +1177,12 @@ public: std::function failureCallback, std::function finallyCallback) final; + // https://dev.twitch.tv/docs/api/reference#get-channel-information + void fetchChannels( + QStringList userIDs, + ResultCallback> successCallback, + HelixFailureCallback failureCallback) final; + // https://dev.twitch.tv/docs/api/reference#get-channel-information void getChannel(QString broadcasterId, ResultCallback successCallback, @@ -515,15 +1196,17 @@ public: // https://dev.twitch.tv/docs/api/reference#get-user-block-list void loadBlocks(QString userId, - ResultCallback> successCallback, - HelixFailureCallback failureCallback) final; + ResultCallback> pageCallback, + FailureCallback failureCallback, + CancellationToken &&token) final; // https://dev.twitch.tv/docs/api/reference#block-user - void blockUser(QString targetUserId, std::function successCallback, + void blockUser(QString targetUserId, const QObject *caller, + std::function successCallback, HelixFailureCallback failureCallback) final; // https://dev.twitch.tv/docs/api/reference#unblock-user - void unblockUser(QString targetUserId, + void unblockUser(QString targetUserId, const QObject *caller, std::function successCallback, HelixFailureCallback failureCallback) final; @@ -531,7 +1214,8 @@ public: void updateChannel(QString broadcasterId, QString gameId, QString language, QString title, std::function successCallback, - HelixFailureCallback failureCallback) final; + FailureCallback + failureCallback) final; // https://dev.twitch.tv/docs/api/reference#manage-held-automod-messages void manageAutoModMessages( @@ -556,12 +1240,263 @@ public: ResultCallback> successCallback, HelixFailureCallback failureCallback) final; + // https://dev.twitch.tv/docs/api/reference#update-user-chat-color + void updateUserChatColor( + QString userID, QString color, ResultCallback<> successCallback, + FailureCallback failureCallback) + final; + + // https://dev.twitch.tv/docs/api/reference#delete-chat-messages + void deleteChatMessages( + QString broadcasterID, QString moderatorID, QString messageID, + ResultCallback<> successCallback, + FailureCallback failureCallback) + final; + + // https://dev.twitch.tv/docs/api/reference#add-channel-moderator + void addChannelModerator( + QString broadcasterID, QString userID, ResultCallback<> successCallback, + FailureCallback failureCallback) + final; + + // https://dev.twitch.tv/docs/api/reference#remove-channel-moderator + void removeChannelModerator( + QString broadcasterID, QString userID, ResultCallback<> successCallback, + FailureCallback + failureCallback) final; + + // https://dev.twitch.tv/docs/api/reference#send-chat-announcement + void sendChatAnnouncement( + QString broadcasterID, QString moderatorID, QString message, + HelixAnnouncementColor color, ResultCallback<> successCallback, + FailureCallback + failureCallback) final; + + // https://dev.twitch.tv/docs/api/reference#add-channel-vip + void addChannelVIP(QString broadcasterID, QString userID, + ResultCallback<> successCallback, + FailureCallback + failureCallback) final; + + // https://dev.twitch.tv/docs/api/reference#remove-channel-vip + void removeChannelVIP(QString broadcasterID, QString userID, + ResultCallback<> successCallback, + FailureCallback + failureCallback) final; + + // These changes are from the helix-command-migration/unban-untimeout branch + // https://dev.twitch.tv/docs/api/reference#unban-user + // These changes are from the helix-command-migration/unban-untimeout branch + void unbanUser( + QString broadcasterID, QString moderatorID, QString userID, + ResultCallback<> successCallback, + FailureCallback failureCallback) final; + // These changes are from the helix-command-migration/unban-untimeout branch + + // https://dev.twitch.tv/docs/api/reference#start-a-raid + void startRaid( + QString fromBroadcasterID, QString toBroadcasterID, + ResultCallback<> successCallback, + FailureCallback failureCallback) final; + // https://dev.twitch.tv/docs/api/reference#start-a-raid + + // https://dev.twitch.tv/docs/api/reference#cancel-a-raid + void cancelRaid( + QString broadcasterID, ResultCallback<> successCallback, + FailureCallback failureCallback) final; + // https://dev.twitch.tv/docs/api/reference#cancel-a-raid + + // Updates the emote mode using + // https://dev.twitch.tv/docs/api/reference#update-chat-settings + void updateEmoteMode(QString broadcasterID, QString moderatorID, + bool emoteMode, + ResultCallback successCallback, + FailureCallback + failureCallback) final; + + // Updates the follower mode using + // https://dev.twitch.tv/docs/api/reference#update-chat-settings + void updateFollowerMode( + QString broadcasterID, QString moderatorID, + std::optional followerModeDuration, + ResultCallback successCallback, + FailureCallback failureCallback) + final; + + // Updates the non-moderator chat delay using + // https://dev.twitch.tv/docs/api/reference#update-chat-settings + void updateNonModeratorChatDelay( + QString broadcasterID, QString moderatorID, + std::optional nonModeratorChatDelayDuration, + ResultCallback successCallback, + FailureCallback failureCallback) + final; + + // Updates the slow mode using + // https://dev.twitch.tv/docs/api/reference#update-chat-settings + void updateSlowMode(QString broadcasterID, QString moderatorID, + std::optional slowModeWaitTime, + ResultCallback successCallback, + FailureCallback + failureCallback) final; + + // Updates the subscriber mode using + // https://dev.twitch.tv/docs/api/reference#update-chat-settings + void updateSubscriberMode( + QString broadcasterID, QString moderatorID, bool subscriberMode, + ResultCallback successCallback, + FailureCallback failureCallback) + final; + + // Updates the unique chat mode using + // https://dev.twitch.tv/docs/api/reference#update-chat-settings + void updateUniqueChatMode( + QString broadcasterID, QString moderatorID, bool uniqueChatMode, + ResultCallback successCallback, + FailureCallback failureCallback) + final; + + // Ban/timeout a user + // https://dev.twitch.tv/docs/api/reference#ban-user + void banUser( + QString broadcasterID, QString moderatorID, QString userID, + std::optional duration, QString reason, + ResultCallback<> successCallback, + FailureCallback failureCallback) final; + + // Warn a user + // https://dev.twitch.tv/docs/api/reference#warn-chat-user + void warnUser( + QString broadcasterID, QString moderatorID, QString userID, + QString reason, ResultCallback<> successCallback, + FailureCallback failureCallback) final; + + // Send a whisper + // https://dev.twitch.tv/docs/api/reference#send-whisper + void sendWhisper( + QString fromUserID, QString toUserID, QString message, + ResultCallback<> successCallback, + FailureCallback failureCallback) final; + + // Get Chatters from the `broadcasterID` channel + // This will follow the returned cursor and return up to `maxChattersToFetch` chatters + // https://dev.twitch.tv/docs/api/reference#get-chatters + void getChatters( + QString broadcasterID, QString moderatorID, int maxChattersToFetch, + ResultCallback successCallback, + FailureCallback failureCallback) final; + + // Get moderators from the `broadcasterID` channel + // This will follow the returned cursor + // https://dev.twitch.tv/docs/api/reference#get-moderators + void getModerators( + QString broadcasterID, int maxModeratorsToFetch, + ResultCallback> successCallback, + FailureCallback failureCallback) + final; + + // https://dev.twitch.tv/docs/api/reference#get-vips + void getChannelVIPs( + QString broadcasterID, + ResultCallback> successCallback, + FailureCallback failureCallback) final; + + // https://dev.twitch.tv/docs/api/reference#start-commercial + void startCommercial( + QString broadcasterID, int length, + ResultCallback successCallback, + FailureCallback failureCallback) + final; + + // Get global Twitch badges + // https://dev.twitch.tv/docs/api/reference/#get-global-chat-badges + void getGlobalBadges(ResultCallback successCallback, + FailureCallback + failureCallback) final; + + // Get badges for the `broadcasterID` channel + // https://dev.twitch.tv/docs/api/reference/#get-channel-chat-badges + void getChannelBadges(QString broadcasterID, + ResultCallback successCallback, + FailureCallback + failureCallback) final; + + // https://dev.twitch.tv/docs/api/reference/#update-shield-mode-status + void updateShieldMode(QString broadcasterID, QString moderatorID, + bool isActive, + ResultCallback successCallback, + FailureCallback + failureCallback) final; + + // https://dev.twitch.tv/docs/api/reference/#send-a-shoutout + void sendShoutout( + QString fromBroadcasterID, QString toBroadcasterID, QString moderatorID, + ResultCallback<> successCallback, + FailureCallback failureCallback) final; + + /// https://dev.twitch.tv/docs/api/reference/#send-chat-message + void sendChatMessage( + HelixSendMessageArgs args, + ResultCallback successCallback, + FailureCallback failureCallback) final; + void update(QString clientId, QString oauthToken) final; static void initialize(); +protected: + // https://dev.twitch.tv/docs/api/reference#update-chat-settings + void updateChatSettings( + QString broadcasterID, QString moderatorID, QJsonObject json, + ResultCallback successCallback, + FailureCallback failureCallback) + final; + + // Recursive boy + void onFetchChattersSuccess( + std::shared_ptr finalChatters, QString broadcasterID, + QString moderatorID, int maxChattersToFetch, + ResultCallback successCallback, + FailureCallback failureCallback, + HelixChatters chatters); + + // Get chatters list - This method is what actually runs the API request + // https://dev.twitch.tv/docs/api/reference#get-chatters + void fetchChatters( + QString broadcasterID, QString moderatorID, int first, QString after, + ResultCallback successCallback, + FailureCallback failureCallback); + + // Recursive boy + void onFetchModeratorsSuccess( + std::shared_ptr> finalModerators, + QString broadcasterID, int maxModeratorsToFetch, + ResultCallback> successCallback, + FailureCallback failureCallback, + HelixModerators moderators); + + // Get moderator list - This method is what actually runs the API request + // https://dev.twitch.tv/docs/api/reference#get-moderators + void fetchModerators( + QString broadcasterID, int first, QString after, + ResultCallback successCallback, + FailureCallback failureCallback); + private: - NetworkRequest makeRequest(QString url, QUrlQuery urlQuery); + NetworkRequest makeRequest(const QString &url, const QUrlQuery &urlQuery, + NetworkRequestType type); + NetworkRequest makeGet(const QString &url, const QUrlQuery &urlQuery); + NetworkRequest makeDelete(const QString &url, const QUrlQuery &urlQuery); + NetworkRequest makePost(const QString &url, const QUrlQuery &urlQuery); + NetworkRequest makePut(const QString &url, const QUrlQuery &urlQuery); + NetworkRequest makePatch(const QString &url, const QUrlQuery &urlQuery); + + /// Paginate the `url` endpoint and use `baseQuery` as the starting point for pagination. + /// @param onPage returns true while a new page is expected. Once false is returned, pagination will stop. + void paginate(const QString &url, const QUrlQuery &baseQuery, + std::function onPage, + std::function onError, + CancellationToken &&token); QString clientId; QString oauthToken; diff --git a/src/providers/twitch/api/README.md b/src/providers/twitch/api/README.md index 31a7e7424..18e9d6e4a 100644 --- a/src/providers/twitch/api/README.md +++ b/src/providers/twitch/api/README.md @@ -6,6 +6,18 @@ this folder describes what sort of API requests we do, what permissions are requ Full Helix API reference: https://dev.twitch.tv/docs/api/reference +### Adding support for a new endpoint + +If you're adding support for a new endpoint, these are the things you should know. + +1. Add a virtual function in the `IHelix` class. Naming should reflect the API name as best as possible. +1. Override the virtual function in the `Helix` class. +1. Mock the function in the `mock::Helix` class in the `mocks/include/mocks/Helix.hpp` file. +1. (Optional) Make a new error enum for the failure callback. + +For a simple example, see the `updateUserChatColor` function and its error enum `HelixUpdateUserChatColorError`. +The API is used in the "/color" command in [CommandController.cpp](../../../controllers/commands/CommandController.cpp) + ### Get Users URL: https://dev.twitch.tv/docs/api/reference#get-users @@ -31,7 +43,7 @@ URL: https://dev.twitch.tv/docs/api/reference#get-streams Used in: -- `TwitchChannel` to get live status, game, title, and viewer count of a channel +- `LiveController` to get live status, game, title, and viewer count of a channel - `NotificationController` to provide notifications for channels you might not have open in Chatterino, but are still interested in getting notifications for ### Create Clip @@ -49,7 +61,7 @@ URL: https://dev.twitch.tv/docs/api/reference#get-channel-information Used in: -- `TwitchChannel` to refresh stream title +- `LiveController` to refresh stream title & display name ### Update Channel @@ -124,6 +136,22 @@ Used in: - `providers/twitch/TwitchChannel.cpp` to resolve a chats available cheer emotes. This helps us parse incoming messages like `pajaCheer1000` +### Get Global Badges + +URL: https://dev.twitch.tv/docs/api/reference/#get-global-chat-badges + +Used in: + +- `providers/twitch/TwitchBadges.cpp` to load global badges + +### Get Channel Badges + +URL: https://dev.twitch.tv/docs/api/reference/#get-channel-chat-badges + +Used in: + +- `providers/twitch/TwitchChannel.cpp` to load channel badges + ### Get Emote Sets URL: https://dev.twitch.tv/docs/api/reference#get-emote-sets @@ -136,13 +164,70 @@ URL: https://dev.twitch.tv/docs/api/reference#get-channel-emotes Not used anywhere at the moment. -## TMI - -The TMI api is undocumented. - ### Get Chatters -**Undocumented** +URL: https://dev.twitch.tv/docs/api/reference/#get-chatters -- We use this in `widgets/splits/Split.cpp showViewerList` -- We use this in `providers/twitch/TwitchChannel.cpp refreshChatters` +Used for the chatter list for moderators/broadcasters. + +### Send Shoutout + +URL: https://dev.twitch.tv/docs/api/reference/#send-a-shoutout + +Used in: + +- `controllers/commands/CommandController.cpp` to send Twitch native shoutout using "/shoutout " + +### Warn Chat User + +URL: https://dev.twitch.tv/docs/api/reference/#warn-chat-user + +Used in: + +- `controllers/commands/CommandController.cpp` to warn users via "/warn" command + +## PubSub + +### Whispers + +We listen to the `whispers.` PubSub topic to receive information about incoming whispers to the user + +The EventSub alternative (`user.whisper.message`) is not yet implemented. + +### Chat Moderator Actions + +We listen to the `chat_moderator_actions..` PubSub topic to receive information about incoming moderator events in a channel. + +We listen to this topic in every channel the user is a moderator. + +We have not yet migrated to the EventSub equivalent topics: + +- For showing bans & timeouts => `channel.moderate` +- For showing unbans & untimeouts => `channel.moderate` +- Clear/delete message => `channel.moderate` +- Roomstate (slow(off), followers(off), r9k(off), emoteonly(off), subscribers(off)) => `channel.moderate` +- VIP/Moderator added/removed => `channel.moderate` +- Raid started/cancelled => `channel.moderate` +- Add/delete permitted/blocked term => `channel.moderate` (or `automod.terms.update`) +- Modified automod properties => `automod.settings.update` +- Approve/deny unban request => `channel.moderate` (or `channel.unban_request.resolve`) + +### AutoMod Queue + +We listen to the `automod-queue..` PubSub topic to receive information about incoming automod events in a channel. + +We listen to this topic in every channel the user is a moderator. + +The EventSub alternative (`automod.message.hold` and `automod.message.update`) is not yet implemented. + +### Channel Point Rewards + +We listen to the `community-points-channel-v1.` PubSub topic to receive information about incoming channel points redemptions in a channel. + +The EventSub alternative requires broadcaster auth, which is not a feasible alternative. + +### Low Trust Users + +We want to listen to the `low-trust-users` PubSub topic to receive information about messages from users who are marked as low-trust. + +The EventSub alternative (`channel.suspicious_user.message` and `channel.suspicious_user.update`) is not yet implemented. diff --git a/src/providers/twitch/pubsubmessages/AutoMod.cpp b/src/providers/twitch/pubsubmessages/AutoMod.cpp index 8c0838f6b..697db1e32 100644 --- a/src/providers/twitch/pubsubmessages/AutoMod.cpp +++ b/src/providers/twitch/pubsubmessages/AutoMod.cpp @@ -1,5 +1,7 @@ #include "providers/twitch/pubsubmessages/AutoMod.hpp" +#include "util/QMagicEnum.hpp" + namespace chatterino { PubSubAutoModQueueMessage::PubSubAutoModQueueMessage(const QJsonObject &root) @@ -7,7 +9,7 @@ PubSubAutoModQueueMessage::PubSubAutoModQueueMessage(const QJsonObject &root) , data(root.value("data").toObject()) , status(this->data.value("status").toString()) { - auto oType = magic_enum::enum_cast(this->typeString.toStdString()); + auto oType = qmagicenum::enumCast(this->typeString); if (oType.has_value()) { this->type = oType.value(); diff --git a/src/providers/twitch/pubsubmessages/AutoMod.hpp b/src/providers/twitch/pubsubmessages/AutoMod.hpp index f44bd0082..9f40d39da 100644 --- a/src/providers/twitch/pubsubmessages/AutoMod.hpp +++ b/src/providers/twitch/pubsubmessages/AutoMod.hpp @@ -1,11 +1,10 @@ #pragma once +#include #include #include #include -#include - namespace chatterino { struct PubSubAutoModQueueMessage { @@ -32,7 +31,8 @@ struct PubSubAutoModQueueMessage { QString senderUserDisplayName; QColor senderUserChatColor; - PubSubAutoModQueueMessage(const QJsonObject &root); + PubSubAutoModQueueMessage() = default; + explicit PubSubAutoModQueueMessage(const QJsonObject &root); }; } // namespace chatterino diff --git a/src/providers/twitch/pubsubmessages/Base.cpp b/src/providers/twitch/pubsubmessages/Base.cpp index fd921e765..4b32786e9 100644 --- a/src/providers/twitch/pubsubmessages/Base.cpp +++ b/src/providers/twitch/pubsubmessages/Base.cpp @@ -1,5 +1,7 @@ #include "providers/twitch/pubsubmessages/Base.hpp" +#include "util/QMagicEnum.hpp" + namespace chatterino { PubSubMessage::PubSubMessage(QJsonObject _object) @@ -9,11 +11,23 @@ PubSubMessage::PubSubMessage(QJsonObject _object) , error(this->object.value("error").toString()) , typeString(this->object.value("type").toString()) { - auto oType = magic_enum::enum_cast(this->typeString.toStdString()); + auto oType = qmagicenum::enumCast(this->typeString); if (oType.has_value()) { this->type = oType.value(); } } +std::optional parsePubSubBaseMessage(const QString &blob) +{ + QJsonDocument jsonDoc(QJsonDocument::fromJson(blob.toUtf8())); + + if (jsonDoc.isNull()) + { + return std::nullopt; + } + + return PubSubMessage(jsonDoc.object()); +} + } // namespace chatterino diff --git a/src/providers/twitch/pubsubmessages/Base.hpp b/src/providers/twitch/pubsubmessages/Base.hpp index 3c8f28c01..a1da168ce 100644 --- a/src/providers/twitch/pubsubmessages/Base.hpp +++ b/src/providers/twitch/pubsubmessages/Base.hpp @@ -1,12 +1,11 @@ #pragma once +#include #include #include #include -#include - -#include +#include namespace chatterino { @@ -29,16 +28,16 @@ struct PubSubMessage { PubSubMessage(QJsonObject _object); template - boost::optional toInner(); + std::optional toInner(); }; template -boost::optional PubSubMessage::toInner() +std::optional PubSubMessage::toInner() { auto dataValue = this->object.value("data"); if (!dataValue.isObject()) { - return boost::none; + return std::nullopt; } auto data = dataValue.toObject(); @@ -46,18 +45,7 @@ boost::optional PubSubMessage::toInner() return InnerClass{this->nonce, data}; } -static boost::optional parsePubSubBaseMessage( - const QString &blob) -{ - QJsonDocument jsonDoc(QJsonDocument::fromJson(blob.toUtf8())); - - if (jsonDoc.isNull()) - { - return boost::none; - } - - return PubSubMessage(jsonDoc.object()); -} +std::optional parsePubSubBaseMessage(const QString &blob); } // namespace chatterino diff --git a/src/providers/twitch/pubsubmessages/ChannelPoints.cpp b/src/providers/twitch/pubsubmessages/ChannelPoints.cpp index 8907a2d2e..244e2be98 100644 --- a/src/providers/twitch/pubsubmessages/ChannelPoints.cpp +++ b/src/providers/twitch/pubsubmessages/ChannelPoints.cpp @@ -1,5 +1,7 @@ #include "providers/twitch/pubsubmessages/ChannelPoints.hpp" +#include "util/QMagicEnum.hpp" + namespace chatterino { PubSubCommunityPointsChannelV1Message::PubSubCommunityPointsChannelV1Message( @@ -7,7 +9,7 @@ PubSubCommunityPointsChannelV1Message::PubSubCommunityPointsChannelV1Message( : typeString(root.value("type").toString()) , data(root.value("data").toObject()) { - auto oType = magic_enum::enum_cast(this->typeString.toStdString()); + auto oType = qmagicenum::enumCast(this->typeString); if (oType.has_value()) { this->type = oType.value(); diff --git a/src/providers/twitch/pubsubmessages/ChannelPoints.hpp b/src/providers/twitch/pubsubmessages/ChannelPoints.hpp index 68b9a23f2..fe2254a7a 100644 --- a/src/providers/twitch/pubsubmessages/ChannelPoints.hpp +++ b/src/providers/twitch/pubsubmessages/ChannelPoints.hpp @@ -1,14 +1,14 @@ #pragma once +#include #include #include -#include - namespace chatterino { struct PubSubCommunityPointsChannelV1Message { enum class Type { + AutomaticRewardRedeemed, RewardRedeemed, INVALID, @@ -31,6 +31,9 @@ constexpr magic_enum::customize::customize_t magic_enum::customize::enum_name< { switch (value) { + case chatterino::PubSubCommunityPointsChannelV1Message::Type:: + AutomaticRewardRedeemed: + return "automatic-reward-redeemed"; case chatterino::PubSubCommunityPointsChannelV1Message::Type:: RewardRedeemed: return "reward-redeemed"; diff --git a/src/providers/twitch/pubsubmessages/ChatModeratorAction.cpp b/src/providers/twitch/pubsubmessages/ChatModeratorAction.cpp index 8134178c5..2cc36ca98 100644 --- a/src/providers/twitch/pubsubmessages/ChatModeratorAction.cpp +++ b/src/providers/twitch/pubsubmessages/ChatModeratorAction.cpp @@ -1,5 +1,7 @@ #include "providers/twitch/pubsubmessages/ChatModeratorAction.hpp" +#include "util/QMagicEnum.hpp" + namespace chatterino { PubSubChatModeratorActionMessage::PubSubChatModeratorActionMessage( @@ -7,7 +9,7 @@ PubSubChatModeratorActionMessage::PubSubChatModeratorActionMessage( : typeString(root.value("type").toString()) , data(root.value("data").toObject()) { - auto oType = magic_enum::enum_cast(this->typeString.toStdString()); + auto oType = qmagicenum::enumCast(this->typeString); if (oType.has_value()) { this->type = oType.value(); diff --git a/src/providers/twitch/pubsubmessages/ChatModeratorAction.hpp b/src/providers/twitch/pubsubmessages/ChatModeratorAction.hpp index bd2038e6b..e04019cb7 100644 --- a/src/providers/twitch/pubsubmessages/ChatModeratorAction.hpp +++ b/src/providers/twitch/pubsubmessages/ChatModeratorAction.hpp @@ -1,10 +1,9 @@ #pragma once +#include #include #include -#include - namespace chatterino { struct PubSubChatModeratorActionMessage { diff --git a/src/providers/twitch/pubsubmessages/LowTrustUsers.cpp b/src/providers/twitch/pubsubmessages/LowTrustUsers.cpp new file mode 100644 index 000000000..cac4e02fd --- /dev/null +++ b/src/providers/twitch/pubsubmessages/LowTrustUsers.cpp @@ -0,0 +1,106 @@ +#include "providers/twitch/pubsubmessages/LowTrustUsers.hpp" + +#include "util/QMagicEnum.hpp" + +#include +#include + +namespace chatterino { + +PubSubLowTrustUsersMessage::PubSubLowTrustUsersMessage(const QJsonObject &root) + : typeString(root.value("type").toString()) +{ + if (const auto oType = qmagicenum::enumCast(this->typeString); + oType.has_value()) + { + this->type = oType.value(); + } + + auto data = root.value("data").toObject(); + + if (this->type == Type::UserMessage) + { + this->msgID = data.value("message_id").toString(); + this->sentAt = data.value("sent_at").toString(); + const auto content = data.value("message_content").toObject(); + this->text = content.value("text").toString(); + for (const auto &part : content.value("fragments").toArray()) + { + this->fragments.emplace_back(part.toObject()); + } + + // the rest of the data is within a nested object + data = data.value("low_trust_user").toObject(); + + const auto sender = data.value("sender").toObject(); + this->suspiciousUserID = sender.value("user_id").toString(); + this->suspiciousUserLogin = sender.value("login").toString(); + this->suspiciousUserDisplayName = + sender.value("display_name").toString(); + this->suspiciousUserColor = + QColor(sender.value("chat_color").toString()); + + for (const auto &badge : sender.value("badges").toArray()) + { + const auto badgeObj = badge.toObject(); + const auto badgeID = badgeObj.value("id").toString(); + const auto badgeVersion = badgeObj.value("version").toString(); + this->senderBadges.emplace_back(Badge{badgeID, badgeVersion}); + } + + const auto sharedValue = data.value("shared_ban_channel_ids"); + if (!sharedValue.isNull()) + { + for (const auto &id : sharedValue.toArray()) + { + this->sharedBanChannelIDs.emplace_back(id.toString()); + } + } + } + else + { + this->suspiciousUserID = data.value("target_user_id").toString(); + this->suspiciousUserLogin = data.value("target_user").toString(); + this->suspiciousUserDisplayName = this->suspiciousUserLogin; + } + + this->channelID = data.value("channel_id").toString(); + this->updatedAtString = data.value("updated_at").toString(); + this->updatedAt = QDateTime::fromString(this->updatedAtString, Qt::ISODate) + .toLocalTime() + .toString("MMM d yyyy, h:mm ap"); + + const auto updatedBy = data.value("updated_by").toObject(); + this->updatedByUserID = updatedBy.value("id").toString(); + this->updatedByUserLogin = updatedBy.value("login").toString(); + this->updatedByUserDisplayName = updatedBy.value("display_name").toString(); + + this->treatmentString = data.value("treatment").toString(); + if (const auto oTreatment = + qmagicenum::enumCast(this->treatmentString); + oTreatment.has_value()) + { + this->treatment = oTreatment.value(); + } + + this->evasionEvaluationString = + data.value("ban_evasion_evaluation").toString(); + if (const auto oEvaluation = qmagicenum::enumCast( + this->evasionEvaluationString); + oEvaluation.has_value()) + { + this->evasionEvaluation = oEvaluation.value(); + } + + for (const auto &rType : data.value("types").toArray()) + { + if (const auto oRestriction = + qmagicenum::enumCast(rType.toString()); + oRestriction.has_value()) + { + this->restrictionTypes.set(oRestriction.value()); + } + } +} + +} // namespace chatterino diff --git a/src/providers/twitch/pubsubmessages/LowTrustUsers.hpp b/src/providers/twitch/pubsubmessages/LowTrustUsers.hpp new file mode 100644 index 000000000..e26662813 --- /dev/null +++ b/src/providers/twitch/pubsubmessages/LowTrustUsers.hpp @@ -0,0 +1,266 @@ +#pragma once + +#include "providers/twitch/TwitchBadge.hpp" + +#include +#include +#include +#include +#include + +namespace chatterino { + +struct PubSubLowTrustUsersMessage { + struct Fragment { + QString text; + QString emoteID; + + explicit Fragment(const QJsonObject &obj) + : text(obj.value("text").toString()) + , emoteID(obj.value("emoticon") + .toObject() + .value("emoticonID") + .toString()) + { + } + }; + + /** + * The type of low trust message update + */ + enum class Type { + /** + * An incoming message from someone marked as low trust + */ + UserMessage, + + /** + * An incoming update about a user's low trust status + */ + TreatmentUpdate, + + INVALID, + }; + + /** + * The treatment set for the suspicious user + */ + enum class Treatment { + NoTreatment, + ActiveMonitoring, + Restricted, + + INVALID, + }; + + /** + * A ban evasion likelihood value (if any) that has been applied to the user + * automatically by Twitch + */ + enum class EvasionEvaluation { + UnknownEvader, + UnlikelyEvader, + LikelyEvader, + PossibleEvader, + + INVALID, + }; + + /** + * Restriction type (if any) that apply to the suspicious user + */ + enum class RestrictionType : uint8_t { + UnknownType = 1 << 0, + ManuallyAdded = 1 << 1, + DetectedBanEvader = 1 << 2, + BannedInSharedChannel = 1 << 3, + + INVALID = 1 << 4, + }; + + Type type = Type::INVALID; + + Treatment treatment = Treatment::INVALID; + + EvasionEvaluation evasionEvaluation = EvasionEvaluation::INVALID; + + FlagsEnum restrictionTypes; + + QString channelID; + + QString suspiciousUserID; + QString suspiciousUserLogin; + QString suspiciousUserDisplayName; + + QString updatedByUserID; + QString updatedByUserLogin; + QString updatedByUserDisplayName; + + /** + * Formatted timestamp of when the treatment was last updated for the suspicious user + */ + QString updatedAt; + + /** + * Plain text of the message sent. + * Only used for the UserMessage type. + */ + QString text; + + /** + * Pre-parsed components of the message. + * Only used for the UserMessage type. + */ + std::vector fragments; + + /** + * ID of the message. + * Only used for the UserMessage type. + */ + QString msgID; + + /** + * RFC3339 timestamp of when the message was sent. + * Only used for the UserMessage type. + */ + QString sentAt; + + /** + * Color of the user who sent the message. + * Only used for the UserMessage type. + */ + QColor suspiciousUserColor; + + /** + * A list of channel IDs where the suspicious user is also banned. + * Only used for the UserMessage type. + */ + std::vector sharedBanChannelIDs; + + /** + * A list of badges of the user who sent the message. + * Only used for the UserMessage type. + */ + std::vector senderBadges; + + /** + * Stores the string value of `type` + * Useful in case type shows up as invalid after being parsed + */ + QString typeString; + + /** + * Stores the string value of `treatment` + * Useful in case treatment shows up as invalid after being parsed + */ + QString treatmentString; + + /** + * Stores the string value of `ban_evasion_evaluation` + * Useful in case evasionEvaluation shows up as invalid after being parsed + */ + QString evasionEvaluationString; + + /** + * Stores the string value of `updated_at` + * Useful in case formattedUpdatedAt doesn't parse correctly + */ + QString updatedAtString; + + PubSubLowTrustUsersMessage() = default; + explicit PubSubLowTrustUsersMessage(const QJsonObject &root); +}; + +} // namespace chatterino + +template <> +constexpr magic_enum::customize::customize_t magic_enum::customize::enum_name< + chatterino::PubSubLowTrustUsersMessage::Type>( + chatterino::PubSubLowTrustUsersMessage::Type value) noexcept +{ + switch (value) + { + case chatterino::PubSubLowTrustUsersMessage::Type::UserMessage: + return "low_trust_user_new_message"; + + case chatterino::PubSubLowTrustUsersMessage::Type::TreatmentUpdate: + return "low_trust_user_treatment_update"; + + default: + return default_tag; + } +} + +template <> +constexpr magic_enum::customize::customize_t magic_enum::customize::enum_name< + chatterino::PubSubLowTrustUsersMessage::Treatment>( + chatterino::PubSubLowTrustUsersMessage::Treatment value) noexcept +{ + using Treatment = chatterino::PubSubLowTrustUsersMessage::Treatment; + switch (value) + { + case Treatment::NoTreatment: + return "NO_TREATMENT"; + + case Treatment::ActiveMonitoring: + return "ACTIVE_MONITORING"; + + case Treatment::Restricted: + return "RESTRICTED"; + + default: + return default_tag; + } +} + +template <> +constexpr magic_enum::customize::customize_t magic_enum::customize::enum_name< + chatterino::PubSubLowTrustUsersMessage::EvasionEvaluation>( + chatterino::PubSubLowTrustUsersMessage::EvasionEvaluation value) noexcept +{ + using EvasionEvaluation = + chatterino::PubSubLowTrustUsersMessage::EvasionEvaluation; + switch (value) + { + case EvasionEvaluation::UnknownEvader: + return "UNKNOWN_EVADER"; + + case EvasionEvaluation::UnlikelyEvader: + return "UNLIKELY_EVADER"; + + case EvasionEvaluation::LikelyEvader: + return "LIKELY_EVADER"; + + case EvasionEvaluation::PossibleEvader: + return "POSSIBLE_EVADER"; + + default: + return default_tag; + } +} + +template <> +constexpr magic_enum::customize::customize_t magic_enum::customize::enum_name< + chatterino::PubSubLowTrustUsersMessage::RestrictionType>( + chatterino::PubSubLowTrustUsersMessage::RestrictionType value) noexcept +{ + using RestrictionType = + chatterino::PubSubLowTrustUsersMessage::RestrictionType; + switch (value) + { + case RestrictionType::UnknownType: + return "UNKNOWN_TYPE"; + + case RestrictionType::ManuallyAdded: + return "MANUALLY_ADDED"; + + case RestrictionType::DetectedBanEvader: + return "DETECTED_BAN_EVADER"; + + case RestrictionType::BannedInSharedChannel: + return "BANNED_IN_SHARED_CHANNEL"; + + default: + return default_tag; + } +} diff --git a/src/providers/twitch/pubsubmessages/Message.hpp b/src/providers/twitch/pubsubmessages/Message.hpp index 2ce8a345d..e854929f9 100644 --- a/src/providers/twitch/pubsubmessages/Message.hpp +++ b/src/providers/twitch/pubsubmessages/Message.hpp @@ -6,7 +6,7 @@ #include #include -#include +#include namespace chatterino { @@ -43,15 +43,15 @@ struct PubSubMessageMessage { } template - boost::optional toInner() const; + std::optional toInner() const; }; template -boost::optional PubSubMessageMessage::toInner() const +std::optional PubSubMessageMessage::toInner() const { if (this->messageObject.empty()) { - return boost::none; + return std::nullopt; } return InnerClass{this->messageObject}; diff --git a/src/providers/twitch/pubsubmessages/Whisper.cpp b/src/providers/twitch/pubsubmessages/Whisper.cpp index d0b59d0c6..2001b8ccb 100644 --- a/src/providers/twitch/pubsubmessages/Whisper.cpp +++ b/src/providers/twitch/pubsubmessages/Whisper.cpp @@ -1,11 +1,13 @@ #include "providers/twitch/pubsubmessages/Whisper.hpp" +#include "util/QMagicEnum.hpp" + namespace chatterino { PubSubWhisperMessage::PubSubWhisperMessage(const QJsonObject &root) : typeString(root.value("type").toString()) { - auto oType = magic_enum::enum_cast(this->typeString.toStdString()); + auto oType = qmagicenum::enumCast(this->typeString); if (oType.has_value()) { this->type = oType.value(); diff --git a/src/providers/twitch/pubsubmessages/Whisper.hpp b/src/providers/twitch/pubsubmessages/Whisper.hpp index af29f74a5..979cb6a1e 100644 --- a/src/providers/twitch/pubsubmessages/Whisper.hpp +++ b/src/providers/twitch/pubsubmessages/Whisper.hpp @@ -1,11 +1,10 @@ #pragma once +#include #include #include #include -#include - namespace chatterino { struct PubSubWhisperMessage { @@ -29,7 +28,8 @@ struct PubSubWhisperMessage { QString fromUserDisplayName; QColor fromUserColor; - PubSubWhisperMessage(const QJsonObject &root); + PubSubWhisperMessage() = default; + explicit PubSubWhisperMessage(const QJsonObject &root); }; } // namespace chatterino diff --git a/src/singletons/Badges.cpp b/src/singletons/Badges.cpp deleted file mode 100644 index da38def4f..000000000 --- a/src/singletons/Badges.cpp +++ /dev/null @@ -1,9 +0,0 @@ -#include "Badges.hpp" - -namespace chatterino { - -Badges::Badges() -{ -} - -} // namespace chatterino diff --git a/src/singletons/Badges.hpp b/src/singletons/Badges.hpp deleted file mode 100644 index d049529eb..000000000 --- a/src/singletons/Badges.hpp +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once - -#include -#include "common/Singleton.hpp" -#include "messages/Emote.hpp" - -namespace chatterino { - -class Badges : public Singleton -{ -public: - Badges(); -}; - -} // namespace chatterino diff --git a/src/singletons/CrashHandler.cpp b/src/singletons/CrashHandler.cpp new file mode 100644 index 000000000..ca9ea915c --- /dev/null +++ b/src/singletons/CrashHandler.cpp @@ -0,0 +1,230 @@ +#include "singletons/CrashHandler.hpp" + +#include "common/Args.hpp" +#include "common/Literals.hpp" +#include "common/QLogging.hpp" +#include "singletons/Paths.hpp" + +#include +#include +#include +#include +#include +#include + +#ifdef CHATTERINO_WITH_CRASHPAD +# include + +# include +# include +#endif + +namespace { + +using namespace chatterino; +using namespace literals; + +/// The name of the crashpad handler executable. +/// This varies across platforms +#if defined(Q_OS_UNIX) +const QString CRASHPAD_EXECUTABLE_NAME = QStringLiteral("crashpad-handler"); +#elif defined(Q_OS_WINDOWS) +const QString CRASHPAD_EXECUTABLE_NAME = QStringLiteral("crashpad-handler.exe"); +#else +# error Unsupported platform +#endif + +/// Converts a QString into the platform string representation. +#if defined(Q_OS_UNIX) +std::string nativeString(const QString &s) +{ + return s.toStdString(); +} +#elif defined(Q_OS_WINDOWS) +std::wstring nativeString(const QString &s) +{ + return s.toStdWString(); +} +#else +# error Unsupported platform +#endif + +const QString RECOVERY_FILE = u"chatterino-recovery.json"_s; + +/// The recovery options are saved outside the settings +/// to be able to read them without loading the settings. +/// +/// The flags are saved in the `RECOVERY_FILE` as JSON. +std::optional readRecoverySettings(const Paths &paths) +{ + QFile file(QDir(paths.crashdumpDirectory).filePath(RECOVERY_FILE)); + if (!file.open(QFile::ReadOnly)) + { + return std::nullopt; + } + + QJsonParseError error{}; + auto doc = QJsonDocument::fromJson(file.readAll(), &error); + if (error.error != QJsonParseError::NoError) + { + qCWarning(chatterinoCrashhandler) + << "Failed to parse recovery settings" << error.errorString(); + return std::nullopt; + } + + const auto obj = doc.object(); + auto shouldRecover = obj["shouldRecover"_L1]; + if (!shouldRecover.isBool()) + { + return std::nullopt; + } + + return shouldRecover.toBool(); +} + +bool canRestart(const Paths &paths, const Args &args) +{ +#ifdef NDEBUG + if (args.isFramelessEmbed || args.shouldRunBrowserExtensionHost) + { + return false; + } + + auto settings = readRecoverySettings(paths); + if (!settings) + { + return false; // default, no settings found + } + return *settings; +#else + (void)paths; + return false; +#endif +} + +/// This encodes the arguments into a single string. +/// +/// The command line arguments are joined by '+'. A plus is escaped by an +/// additional plus ('++' -> '+'). +/// +/// The decoding happens in crash-handler/src/CommandLine.cpp +std::string encodeArguments(const Args &appArgs) +{ + std::string args; + for (auto arg : appArgs.currentArguments()) + { + if (!args.empty()) + { + args.push_back('+'); + } + args += arg.replace(u'+', u"++"_s).toStdString(); + } + return args; +} + +} // namespace + +namespace chatterino { + +using namespace std::string_literals; + +CrashHandler::CrashHandler(const Paths &paths_) + : paths(paths_) +{ + auto optSettings = readRecoverySettings(paths); + if (optSettings) + { + this->shouldRecover_ = *optSettings; + } + else + { + // By default, we don't restart after a crash. + this->saveShouldRecover(false); + } +} + +void CrashHandler::saveShouldRecover(bool value) +{ + this->shouldRecover_ = value; + + QFile file(QDir(this->paths.crashdumpDirectory).filePath(RECOVERY_FILE)); + if (!file.open(QFile::WriteOnly | QFile::Truncate)) + { + qCWarning(chatterinoCrashhandler) + << "Failed to open" << file.fileName(); + return; + } + file.write(QJsonDocument(QJsonObject{ + {"shouldRecover"_L1, value}, + }) + .toJson(QJsonDocument::Compact)); +} + +#ifdef CHATTERINO_WITH_CRASHPAD +std::unique_ptr installCrashHandler( + const Args &args, const Paths &paths) +{ + // Currently, the following directory layout is assumed: + // [applicationDirPath] + // ├─chatterino(.exe) + // ╰─[crashpad] + // ╰─crashpad-handler(.exe) + // TODO: The location of the binary might vary across platforms + auto crashpadBinDir = QDir(QApplication::applicationDirPath()); + + if (!crashpadBinDir.cd("crashpad")) + { + qCDebug(chatterinoCrashhandler) << "Cannot find crashpad directory"; + return nullptr; + } + if (!crashpadBinDir.exists(CRASHPAD_EXECUTABLE_NAME)) + { + qCDebug(chatterinoCrashhandler) + << "Cannot find crashpad handler executable"; + return nullptr; + } + + auto handlerPath = base::FilePath(nativeString( + crashpadBinDir.absoluteFilePath(CRASHPAD_EXECUTABLE_NAME))); + + // Argument passed in --database + // > Crash reports are written to this database, and if uploads are enabled, + // uploaded from this database to a crash report collection server. + auto databaseDir = base::FilePath(nativeString(paths.crashdumpDirectory)); + + auto client = std::make_unique(); + + std::map annotations{ + { + "canRestart"s, + canRestart(paths, args) ? "true"s : "false"s, + }, + { + "exePath"s, + QApplication::applicationFilePath().toStdString(), + }, + { + "startedAt"s, + QDateTime::currentDateTimeUtc().toString(Qt::ISODate).toStdString(), + }, + { + "exeArguments"s, + encodeArguments(args), + }, + }; + + // See https://chromium.googlesource.com/crashpad/crashpad/+/HEAD/handler/crashpad_handler.md + // for documentation on available options. + if (!client->StartHandler(handlerPath, databaseDir, {}, {}, {}, annotations, + {}, true, false)) + { + qCDebug(chatterinoCrashhandler) << "Failed to start crashpad handler"; + return nullptr; + } + + qCDebug(chatterinoCrashhandler) << "Started crashpad handler"; + return client; +} +#endif + +} // namespace chatterino diff --git a/src/singletons/CrashHandler.hpp b/src/singletons/CrashHandler.hpp new file mode 100644 index 000000000..3f2a45262 --- /dev/null +++ b/src/singletons/CrashHandler.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include + +#ifdef CHATTERINO_WITH_CRASHPAD +# include + +# include +#endif + +namespace chatterino { + +class Args; +class Paths; + +class CrashHandler +{ + const Paths &paths; + +public: + explicit CrashHandler(const Paths &paths_); + + bool shouldRecover() const + { + return this->shouldRecover_; + } + + /// Sets and saves whether Chatterino should restart on a crash + void saveShouldRecover(bool value); + +private: + bool shouldRecover_ = false; +}; + +#ifdef CHATTERINO_WITH_CRASHPAD +std::unique_ptr installCrashHandler( + const Args &args, const Paths &paths); +#endif + +} // namespace chatterino diff --git a/src/singletons/Emotes.cpp b/src/singletons/Emotes.cpp index c5d93e3bf..a5b169902 100644 --- a/src/singletons/Emotes.cpp +++ b/src/singletons/Emotes.cpp @@ -1,24 +1,12 @@ #include "singletons/Emotes.hpp" -#include "Application.hpp" -#include "controllers/accounts/AccountController.hpp" - namespace chatterino { Emotes::Emotes() -{ -} - -void Emotes::initialize(Settings &settings, Paths &paths) { this->emojis.load(); this->gifTimer.initialize(); } -bool Emotes::isIgnoredEmote(const QString &) -{ - return false; -} - } // namespace chatterino diff --git a/src/singletons/Emotes.hpp b/src/singletons/Emotes.hpp index be7fdc480..9b2520391 100644 --- a/src/singletons/Emotes.hpp +++ b/src/singletons/Emotes.hpp @@ -1,26 +1,40 @@ #pragma once -#include "common/Singleton.hpp" - -#include "providers/bttv/BttvEmotes.hpp" #include "providers/emoji/Emojis.hpp" -#include "providers/ffz/FfzEmotes.hpp" #include "providers/twitch/TwitchEmotes.hpp" #include "singletons/helper/GifTimer.hpp" namespace chatterino { -class Settings; -class Paths; +class IEmotes +{ +public: + virtual ~IEmotes() = default; -class Emotes final : public Singleton + virtual ITwitchEmotes *getTwitchEmotes() = 0; + virtual IEmojis *getEmojis() = 0; + virtual GIFTimer &getGIFTimer() = 0; +}; + +class Emotes final : public IEmotes { public: Emotes(); - virtual void initialize(Settings &settings, Paths &paths) override; + ITwitchEmotes *getTwitchEmotes() final + { + return &this->twitch; + } - bool isIgnoredEmote(const QString &emote); + IEmojis *getEmojis() final + { + return &this->emojis; + } + + GIFTimer &getGIFTimer() final + { + return this->gifTimer; + } TwitchEmotes twitch; Emojis emojis; diff --git a/src/singletons/Fonts.cpp b/src/singletons/Fonts.cpp index 521baefa7..7afe081ec 100644 --- a/src/singletons/Fonts.cpp +++ b/src/singletons/Fonts.cpp @@ -1,94 +1,80 @@ #include "singletons/Fonts.hpp" -#include "BaseSettings.hpp" +#include "Application.hpp" #include "debug/AssertInGuiThread.hpp" +#include "singletons/Settings.hpp" +#include "singletons/WindowManager.hpp" #include #include -#ifdef CHATTERINO -# include "Application.hpp" -# include "singletons/WindowManager.hpp" -#endif - -#ifdef Q_OS_WIN32 -# define DEFAULT_FONT_FAMILY "Segoe UI" -# define DEFAULT_FONT_SIZE 10 -#else -# ifdef Q_OS_MACOS -# define DEFAULT_FONT_FAMILY "Helvetica Neue" -# define DEFAULT_FONT_SIZE 12 -# else -# define DEFAULT_FONT_FAMILY "Arial" -# define DEFAULT_FONT_SIZE 11 -# endif -#endif - -namespace chatterino { namespace { - int getBoldness() + +using namespace chatterino; + +int getBoldness() +{ +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + // From qfont.cpp + // https://github.com/qt/qtbase/blob/589c6d066f84833a7c3dda1638037f4b2e91b7aa/src/gui/text/qfont.cpp#L143-L169 + static constexpr std::array, 9> legacyToOpenTypeMap{{ + {0, QFont::Thin}, + {12, QFont::ExtraLight}, + {25, QFont::Light}, + {50, QFont::Normal}, + {57, QFont::Medium}, + {63, QFont::DemiBold}, + {75, QFont::Bold}, + {81, QFont::ExtraBold}, + {87, QFont::Black}, + }}; + + const int target = getSettings()->boldScale.getValue(); + + int result = QFont::Medium; + int closestDist = INT_MAX; + + // Go through and find the closest mapped value + for (const auto [weightOld, weightNew] : legacyToOpenTypeMap) { -#ifdef CHATTERINO - return getSettings()->boldScale.getValue(); -#else - return QFont::Bold; -#endif + const int dist = qAbs(weightOld - target); + if (dist < closestDist) + { + result = weightNew; + closestDist = dist; + } + else + { + // Break early since following values will be further away + break; + } } + + return result; +#else + return getSettings()->boldScale.getValue(); +#endif +} } // namespace -Fonts *Fonts::instance = nullptr; +namespace chatterino { -Fonts::Fonts() - : chatFontFamily("/appearance/currentFontFamily", DEFAULT_FONT_FAMILY) - , chatFontSize("/appearance/currentFontSize", DEFAULT_FONT_SIZE) +Fonts::Fonts(Settings &settings) { - Fonts::instance = this; - this->fontsByType_.resize(size_t(FontStyle::EndType)); -} -void Fonts::initialize(Settings &, Paths &) -{ - this->chatFontFamily.connect( - [this]() { - assertInGuiThread(); + this->fontChangedListener.setCB([this] { + assertInGuiThread(); - for (auto &map : this->fontsByType_) - { - map.clear(); - } - this->fontChanged.invoke(); - }, - false); - - this->chatFontSize.connect( - [this]() { - assertInGuiThread(); - - for (auto &map : this->fontsByType_) - { - map.clear(); - } - this->fontChanged.invoke(); - }, - false); - -#ifdef CHATTERINO - getSettings()->boldScale.connect( - [this]() { - assertInGuiThread(); - - // REMOVED - getApp()->windows->incGeneration(); - - for (auto &map : this->fontsByType_) - { - map.clear(); - } - this->fontChanged.invoke(); - }, - false); -#endif + for (auto &map : this->fontsByType_) + { + map.clear(); + } + this->fontChanged.invoke(); + }); + this->fontChangedListener.addSetting(settings.chatFontFamily); + this->fontChangedListener.addSetting(settings.chatFontSize); + this->fontChangedListener.addSetting(settings.boldScale); } QFont Fonts::getFont(FontStyle type, float scale) @@ -127,6 +113,8 @@ Fonts::FontData &Fonts::getOrCreateFontData(FontStyle type, float scale) Fonts::FontData Fonts::createFontData(FontStyle type, float scale) { + auto *settings = getSettings(); + // check if it's a chat (scale the setting) if (type >= FontStyle::ChatStart && type <= FontStyle::ChatEnd) { @@ -144,8 +132,8 @@ Fonts::FontData Fonts::createFontData(FontStyle type, float scale) QFont::Weight(getBoldness())}; auto data = sizeScale[type]; return FontData( - QFont(this->chatFontFamily.getValue(), - int(this->chatFontSize.getValue() * data.scale * scale), + QFont(settings->chatFontFamily.getValue(), + int(settings->chatFontSize.getValue() * data.scale * scale), data.weight, data.italic)); } @@ -173,9 +161,4 @@ Fonts::FontData Fonts::createFontData(FontStyle type, float scale) } } -Fonts *getFonts() -{ - return Fonts::instance; -} - } // namespace chatterino diff --git a/src/singletons/Fonts.hpp b/src/singletons/Fonts.hpp index bc700e5e8..e6ea324a0 100644 --- a/src/singletons/Fonts.hpp +++ b/src/singletons/Fonts.hpp @@ -1,16 +1,13 @@ #pragma once -#include "common/ChatterinoSetting.hpp" -#include "common/Singleton.hpp" +#include "pajlada/settings/settinglistener.hpp" -#include -#include -#include -#include #include +#include +#include -#include #include +#include namespace chatterino { @@ -39,23 +36,17 @@ enum class FontStyle : uint8_t { ChatEnd = ChatVeryLarge, }; -class Fonts final : public Singleton +class Fonts final { public: - Fonts(); - - virtual void initialize(Settings &settings, Paths &paths) override; + explicit Fonts(Settings &settings); // font data gets set in createFontData(...) QFont getFont(FontStyle type, float scale); QFontMetrics getFontMetrics(FontStyle type, float scale); - QStringSetting chatFontFamily; - IntSetting chatFontSize; - pajlada::Signals::NoArgSignal fontChanged; - static Fonts *instance; private: struct FontData { @@ -86,8 +77,8 @@ private: FontData createFontData(FontStyle type, float scale); std::vector> fontsByType_; + + pajlada::SettingListener fontChangedListener; }; -Fonts *getFonts(); - } // namespace chatterino diff --git a/src/singletons/ImageUploader.cpp b/src/singletons/ImageUploader.cpp new file mode 100644 index 000000000..ca082a723 --- /dev/null +++ b/src/singletons/ImageUploader.cpp @@ -0,0 +1,390 @@ +#include "singletons/ImageUploader.hpp" + +#include "Application.hpp" +#include "common/Env.hpp" +#include "common/network/NetworkRequest.hpp" +#include "common/network/NetworkResult.hpp" +#include "common/QLogging.hpp" +#include "debug/Benchmark.hpp" +#include "messages/MessageBuilder.hpp" +#include "singletons/Paths.hpp" +#include "singletons/Settings.hpp" +#include "util/CombinePath.hpp" +#include "widgets/helper/ResizingTextEdit.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace { + +// Delay between uploads in milliseconds +constexpr int UPLOAD_DELAY = 2000; + +std::optional convertToPng(const QImage &image) +{ + QByteArray imageData; + QBuffer buf(&imageData); + buf.open(QIODevice::WriteOnly); + bool success = image.save(&buf, "png"); + if (success) + { + return imageData; + } + + return std::nullopt; +} + +} // namespace + +namespace chatterino { + +// logging information on successful uploads to a json file +void ImageUploader::logToFile(const QString &originalFilePath, + const QString &imageLink, + const QString &deletionLink, ChannelPtr channel) +{ + const QString logFileName = + combinePath((getSettings()->logPath.getValue().isEmpty() + ? getApp()->getPaths().messageLogDirectory + : getSettings()->logPath), + "ImageUploader.json"); + + //reading existing logs + QFile logReadFile(logFileName); + bool isLogFileOkay = + logReadFile.open(QIODevice::ReadWrite | QIODevice::Text); + if (!isLogFileOkay) + { + channel->addSystemMessage( + QString("Failed to open log file with links at ") + logFileName); + return; + } + auto logs = logReadFile.readAll(); + if (logs.isEmpty()) + { + logs = QJsonDocument(QJsonArray()).toJson(); + } + logReadFile.close(); + + //writing new data to logs + QJsonObject newLogEntry; + newLogEntry["channelName"] = channel->getName(); + newLogEntry["deletionLink"] = + deletionLink.isEmpty() ? QJsonValue(QJsonValue::Null) : deletionLink; + newLogEntry["imageLink"] = imageLink; + newLogEntry["localPath"] = originalFilePath.isEmpty() + ? QJsonValue(QJsonValue::Null) + : originalFilePath; + newLogEntry["timestamp"] = QDateTime::currentSecsSinceEpoch(); + // channel name + // deletion link (can be empty) + // image link + // local path to an image (can be empty) + // timestamp + QSaveFile logSaveFile(logFileName); + logSaveFile.open(QIODevice::WriteOnly | QIODevice::Text); + QJsonArray entries = QJsonDocument::fromJson(logs).array(); + entries.push_back(newLogEntry); + logSaveFile.write(QJsonDocument(entries).toJson()); + logSaveFile.commit(); +} + +// extracting link to either image or its deletion from response body +QString getJSONValue(QJsonValue responseJson, QString jsonPattern) +{ + for (const QString &key : jsonPattern.split(".")) + { + responseJson = responseJson[key]; + } + return responseJson.toString(); +} + +QString getLinkFromResponse(NetworkResult response, QString pattern) +{ + QRegularExpression regExp("{(.+)}", + QRegularExpression::InvertedGreedinessOption); + auto match = regExp.match(pattern); + + while (match.hasMatch()) + { + pattern.replace(match.captured(0), + getJSONValue(response.parseJson(), match.captured(1))); + match = regExp.match(pattern); + } + return pattern; +} + +void ImageUploader::sendImageUploadRequest(RawImageData imageData, + ChannelPtr channel, + QPointer textEdit) +{ + QUrl url(getSettings()->imageUploaderUrl.getValue().isEmpty() + ? getSettings()->imageUploaderUrl.getDefaultValue() + : getSettings()->imageUploaderUrl); + QString formField( + getSettings()->imageUploaderFormField.getValue().isEmpty() + ? getSettings()->imageUploaderFormField.getDefaultValue() + : getSettings()->imageUploaderFormField); + auto extraHeaders = + parseHeaderList(getSettings()->imageUploaderHeaders.getValue()); + QString originalFilePath = imageData.filePath; + + QHttpMultiPart *payload = new QHttpMultiPart(QHttpMultiPart::FormDataType); + QHttpPart part = QHttpPart(); + part.setBody(imageData.data); + part.setHeader(QNetworkRequest::ContentTypeHeader, + QString("image/%1").arg(imageData.format)); + part.setHeader(QNetworkRequest::ContentLengthHeader, + QVariant(imageData.data.length())); + part.setHeader(QNetworkRequest::ContentDispositionHeader, + QString("form-data; name=\"%1\"; filename=\"control_v.%2\"") + .arg(formField) + .arg(imageData.format)); + payload->append(part); + + NetworkRequest(url, NetworkRequestType::Post) + .headerList(extraHeaders) + .multiPart(payload) + .onSuccess( + [textEdit, channel, originalFilePath, this](NetworkResult result) { + this->handleSuccessfulUpload(result, originalFilePath, channel, + textEdit); + }) + .onError([channel, this](NetworkResult result) -> bool { + this->handleFailedUpload(result, channel); + return true; + }) + .execute(); +} + +void ImageUploader::handleFailedUpload(const NetworkResult &result, + ChannelPtr channel) +{ + auto errorMessage = + QString("An error happened while uploading your image: %1") + .arg(result.formatError()); + + // Try to read more information from the result body + auto obj = result.parseJson(); + if (!obj.isEmpty()) + { + auto apiCode = obj.value("code"); + if (!apiCode.isUndefined()) + { + auto codeString = apiCode.toVariant().toString(); + codeString.truncate(20); + errorMessage += QString(" - code: %1").arg(codeString); + } + + auto apiError = obj.value("error").toString(); + if (!apiError.isEmpty()) + { + apiError.truncate(300); + errorMessage += QString(" - error: %1").arg(apiError.trimmed()); + } + } + + channel->addSystemMessage(errorMessage); + // NOTE: We abort any future uploads on failure. Should this be handled differently? + while (!this->uploadQueue_.empty()) + { + this->uploadQueue_.pop(); + } + this->uploadMutex_.unlock(); +} + +void ImageUploader::handleSuccessfulUpload(const NetworkResult &result, + QString originalFilePath, + ChannelPtr channel, + QPointer textEdit) +{ + if (textEdit == nullptr) + { + // Split was destroyed abort further uploads + + while (!this->uploadQueue_.empty()) + { + this->uploadQueue_.pop(); + } + this->uploadMutex_.unlock(); + return; + } + QString link = + getSettings()->imageUploaderLink.getValue().isEmpty() + ? result.getData() + : getLinkFromResponse(result, getSettings()->imageUploaderLink); + QString deletionLink = + getSettings()->imageUploaderDeletionLink.getValue().isEmpty() + ? "" + : getLinkFromResponse(result, + getSettings()->imageUploaderDeletionLink); + qCDebug(chatterinoImageuploader) << link << deletionLink; + textEdit->insertPlainText(link + " "); + + // 2 seconds for the timer that's there not to spam the remote server + // and 1 second of actual uploading. + auto timeToUpload = this->uploadQueue_.size() * (UPLOAD_DELAY / 1000 + 1); + MessageBuilder builder(imageUploaderResultMessage, link, deletionLink, + this->uploadQueue_.size(), timeToUpload); + channel->addMessage(builder.release(), MessageContext::Original); + if (this->uploadQueue_.empty()) + { + this->uploadMutex_.unlock(); + } + else + { + QTimer::singleShot(UPLOAD_DELAY, [channel, textEdit, this]() { + this->sendImageUploadRequest(this->uploadQueue_.front(), channel, + textEdit); + this->uploadQueue_.pop(); + }); + } + + this->logToFile(originalFilePath, link, deletionLink, channel); +} + +std::pair, QString> ImageUploader::getImages( + const QMimeData *source) const +{ + BenchmarkGuard benchmarkGuard("ImageUploader::getImages"); + + auto tryUploadFromUrls = + [&]() -> std::pair, QString> { + if (!source->hasUrls()) + { + return {{}, {}}; + } + + std::queue images; + + auto mimeDb = QMimeDatabase(); + // This path gets chosen when files are copied from a file manager, like explorer.exe, caja. + // Each entry in source->urls() is a QUrl pointing to a file that was copied. + for (const QUrl &path : source->urls()) + { + QString localPath = path.toLocalFile(); + QMimeType mime = mimeDb.mimeTypeForUrl(path); + if (mime.name().startsWith("image") && !mime.inherits("image/gif")) + { + QImage img = QImage(localPath); + if (img.isNull()) + { + return {{}, "Couldn't load image :("}; + } + + auto imageData = convertToPng(img); + if (!imageData) + { + return { + {}, + QString("Cannot upload file: %1. Couldn't convert " + "image to png.") + .arg(localPath), + }; + } + images.push({*imageData, "png", localPath}); + } + else if (mime.inherits("image/gif")) + { + QFile file(localPath); + bool isOkay = file.open(QIODevice::ReadOnly); + if (!isOkay) + { + return {{}, "Failed to open file :("}; + } + // file.readAll() => might be a bit big but it /should/ work + images.push({file.readAll(), "gif", localPath}); + file.close(); + } + } + + return {images, {}}; + }; + + auto tryUploadDirectly = + [&]() -> std::pair, QString> { + std::queue images; + + if (source->hasFormat("image/png")) + { + // the path to file is not present every time, thus the filePath is empty + images.push({source->data("image/png"), "png", ""}); + return {images, {}}; + } + + if (source->hasFormat("image/jpeg")) + { + images.push({source->data("image/jpeg"), "jpeg", ""}); + return {images, {}}; + } + + if (source->hasFormat("image/gif")) + { + images.push({source->data("image/gif"), "gif", ""}); + return {images, {}}; + } + + // not PNG, try loading it into QImage and save it to a PNG. + auto image = qvariant_cast(source->imageData()); + auto imageData = convertToPng(image); + if (imageData) + { + images.push({*imageData, "png", ""}); + return {images, {}}; + } + + // No direct upload happenned + return {{}, "Cannot upload file, failed to convert to png."}; + }; + + const auto [urlImageData, urlError] = tryUploadFromUrls(); + + if (!urlImageData.empty()) + { + return {urlImageData, {}}; + } + + const auto [directImageData, directError] = tryUploadDirectly(); + if (!directImageData.empty()) + { + return {directImageData, {}}; + } + + return { + {}, + // TODO: verify that this looks ok xd + urlError + directError, + }; +} + +void ImageUploader::upload(std::queue images, ChannelPtr channel, + QPointer outputTextEdit) +{ + BenchmarkGuard benchmarkGuard("upload"); + if (!this->uploadMutex_.tryLock()) + { + channel->addSystemMessage("Please wait until the upload finishes."); + return; + } + + assert(!images.empty()); + assert(this->uploadQueue_.empty()); + + std::swap(this->uploadQueue_, images); + + channel->addSystemMessage("Started upload..."); + + this->sendImageUploadRequest(this->uploadQueue_.front(), std::move(channel), + std::move(outputTextEdit)); + this->uploadQueue_.pop(); +} + +} // namespace chatterino diff --git a/src/singletons/ImageUploader.hpp b/src/singletons/ImageUploader.hpp new file mode 100644 index 000000000..80cd4ef1c --- /dev/null +++ b/src/singletons/ImageUploader.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include +#include +#include + +#include +#include + +namespace chatterino { + +class ResizingTextEdit; +class Channel; +class NetworkResult; +using ChannelPtr = std::shared_ptr; + +struct RawImageData { + QByteArray data; + QString format; + QString filePath; +}; + +class ImageUploader final +{ +public: + /** + * Tries to get the image(s) from the given QMimeData + * + * If no images were found, the second value in the pair will contain an error message + */ + std::pair, QString> getImages( + const QMimeData *source) const; + + void upload(std::queue images, ChannelPtr channel, + QPointer outputTextEdit); + +private: + void sendImageUploadRequest(RawImageData imageData, ChannelPtr channel, + QPointer textEdit); + + // This is called from the onSuccess handler of the NetworkRequest in sendImageUploadRequest + void handleSuccessfulUpload(const NetworkResult &result, + QString originalFilePath, ChannelPtr channel, + QPointer textEdit); + void handleFailedUpload(const NetworkResult &result, ChannelPtr channel); + + void logToFile(const QString &originalFilePath, const QString &imageLink, + const QString &deletionLink, ChannelPtr channel); + + // These variables are only used from the main thread. + QMutex uploadMutex_; + std::queue uploadQueue_; +}; +} // namespace chatterino diff --git a/src/singletons/Logging.cpp b/src/singletons/Logging.cpp index 372c58209..138efd38a 100644 --- a/src/singletons/Logging.cpp +++ b/src/singletons/Logging.cpp @@ -1,36 +1,60 @@ #include "singletons/Logging.hpp" -#include "Application.hpp" +#include "messages/Message.hpp" +#include "singletons/helper/LoggingChannel.hpp" #include "singletons/Paths.hpp" #include "singletons/Settings.hpp" -#include "singletons/helper/LoggingChannel.hpp" #include #include #include -#include #include namespace chatterino { -void Logging::initialize(Settings &settings, Paths &paths) +Logging::Logging(Settings &settings) { + // We can safely ignore this signal connection since settings are only-ever destroyed + // on application exit + // NOTE: SETTINGS_LIFETIME + std::ignore = settings.loggedChannels.delayedItemsChanged.connect( + [this, &settings]() { + this->threadGuard.guard(); + + this->onlyLogListedChannels.clear(); + + for (const auto &loggedChannel : + *settings.loggedChannels.readOnly()) + { + this->onlyLogListedChannels.insert(loggedChannel.channelName()); + } + }); } void Logging::addMessage(const QString &channelName, MessagePtr message, - const QString &platformName) + const QString &platformName, const QString &streamID) { + this->threadGuard.guard(); + if (!getSettings()->enableLogging) { return; } + if (getSettings()->onlyLogListedChannels) + { + if (!this->onlyLogListedChannels.contains(channelName)) + { + return; + } + } + auto platIt = this->loggingChannels_.find(platformName); if (platIt == this->loggingChannels_.end()) { - auto channel = new LoggingChannel(channelName, platformName); - channel->addMessage(message); + auto *channel = new LoggingChannel(channelName, platformName); + channel->addMessage(message, streamID); auto map = std::map>(); this->loggingChannels_[platformName] = std::move(map); auto &ref = this->loggingChannels_.at(platformName); @@ -40,13 +64,13 @@ void Logging::addMessage(const QString &channelName, MessagePtr message, auto chanIt = platIt->second.find(channelName); if (chanIt == platIt->second.end()) { - auto channel = new LoggingChannel(channelName, platformName); - channel->addMessage(message); + auto *channel = new LoggingChannel(channelName, platformName); + channel->addMessage(message, streamID); platIt->second.emplace(channelName, std::move(channel)); } else { - chanIt->second->addMessage(message); + chanIt->second->addMessage(message, streamID); } } diff --git a/src/singletons/Logging.hpp b/src/singletons/Logging.hpp index 5cca957ec..984932ad3 100644 --- a/src/singletons/Logging.hpp +++ b/src/singletons/Logging.hpp @@ -1,27 +1,39 @@ #pragma once -#include "common/Singleton.hpp" +#include "util/QStringHash.hpp" +#include "util/ThreadGuard.hpp" -#include "messages/Message.hpp" -#include "singletons/helper/LoggingChannel.hpp" +#include +#include #include +#include namespace chatterino { -class Paths; +class Settings; +struct Message; +using MessagePtr = std::shared_ptr; +class LoggingChannel; -class Logging : public Singleton +class ILogging { - Paths *pathManager = nullptr; - public: - Logging() = default; + virtual ~ILogging() = default; - virtual void initialize(Settings &settings, Paths &paths) override; + virtual void addMessage(const QString &channelName, MessagePtr message, + const QString &platformName, + const QString &streamID) = 0; +}; + +class Logging : public ILogging +{ +public: + Logging(Settings &settings); void addMessage(const QString &channelName, MessagePtr message, - const QString &platformName); + const QString &platformName, + const QString &streamID) override; private: using PlatformName = QString; @@ -29,6 +41,10 @@ private: std::map>> loggingChannels_; + + // Keeps the value of the `loggedChannels` settings + std::unordered_set onlyLogListedChannels; + ThreadGuard threadGuard; }; } // namespace chatterino diff --git a/src/singletons/NativeMessaging.cpp b/src/singletons/NativeMessaging.cpp index f4dff7c0b..5406fb4d2 100644 --- a/src/singletons/NativeMessaging.cpp +++ b/src/singletons/NativeMessaging.cpp @@ -1,9 +1,13 @@ #include "singletons/NativeMessaging.hpp" #include "Application.hpp" +#include "common/Literals.hpp" +#include "common/Modes.hpp" #include "common/QLogging.hpp" +#include "debug/AssertInGuiThread.hpp" #include "providers/twitch/TwitchIrcServer.hpp" #include "singletons/Paths.hpp" +#include "util/IpcQueue.hpp" #include "util/PostToThread.hpp" #include @@ -12,79 +16,72 @@ #include #include #include - -#include -#include - -namespace ipc = boost::interprocess; +#include #ifdef Q_OS_WIN -# include - -# include -# include "singletons/WindowManager.hpp" # include "widgets/AttachedWindow.hpp" #endif -#include +namespace { -#define EXTENSION_ID "glknmaideaikkmemifbfkhnomoknepka" -#define MESSAGE_SIZE 1024 +using namespace chatterino::literals; + +const QString EXTENSION_ID = u"glknmaideaikkmemifbfkhnomoknepka"_s; +constexpr const size_t MESSAGE_SIZE = 1024; + +} // namespace namespace chatterino { -void registerNmManifest(Paths &paths, const QString &manifestFilename, +using namespace literals; + +void registerNmManifest(const Paths &paths, const QString &manifestFilename, const QString ®istryKeyName, const QJsonDocument &document); -void registerNmHost(Paths &paths) +void registerNmHost(const Paths &paths) { - if (paths.isPortable()) + if (Modes::instance().isPortable) + { return; + } - auto getBaseDocument = [&] { - QJsonObject obj; - obj.insert("name", "com.chatterino.chatterino"); - obj.insert("description", "Browser interaction with chatterino."); - obj.insert("path", QCoreApplication::applicationFilePath()); - obj.insert("type", "stdio"); - - return obj; + auto getBaseDocument = [] { + return QJsonObject{ + {u"name"_s, "com.chatterino.chatterino"_L1}, + {u"description"_s, "Browser interaction with chatterino."_L1}, + {u"path"_s, QCoreApplication::applicationFilePath()}, + {u"type"_s, "stdio"_L1}, + }; }; // chrome { - QJsonDocument document; - auto obj = getBaseDocument(); - QJsonArray allowed_origins_arr = {"chrome-extension://" EXTENSION_ID - "/"}; - obj.insert("allowed_origins", allowed_origins_arr); - document.setObject(obj); + QJsonArray allowedOriginsArr = { + u"chrome-extension://%1/"_s.arg(EXTENSION_ID)}; + obj.insert("allowed_origins", allowedOriginsArr); registerNmManifest(paths, "/native-messaging-manifest-chrome.json", "HKCU\\Software\\Google\\Chrome\\NativeMessagingHost" "s\\com.chatterino.chatterino", - document); + QJsonDocument(obj)); } // firefox { - QJsonDocument document; - auto obj = getBaseDocument(); - QJsonArray allowed_extensions = {"chatterino_native@chatterino.com"}; - obj.insert("allowed_extensions", allowed_extensions); - document.setObject(obj); + QJsonArray allowedExtensions = {"chatterino_native@chatterino.com"}; + obj.insert("allowed_extensions", allowedExtensions); registerNmManifest(paths, "/native-messaging-manifest-firefox.json", "HKCU\\Software\\Mozilla\\NativeMessagingHosts\\com." "chatterino.chatterino", - document); + QJsonDocument(obj)); } } -void registerNmManifest(Paths &paths, const QString &manifestFilename, +void registerNmManifest(const Paths &paths, const QString &manifestFilename, const QString ®istryKeyName, const QJsonDocument &document) { @@ -98,13 +95,12 @@ void registerNmManifest(Paths &paths, const QString &manifestFilename, file.flush(); #ifdef Q_OS_WIN - // clang-format off - QProcess::execute("REG ADD \"" + registryKeyName + "\" /ve /t REG_SZ /d \"" + manifestPath + "\" /f"); -// clang-format on + QSettings registry(registryKeyName, QSettings::NativeFormat); + registry.setValue("Default", manifestPath); #endif } -std::string &getNmQueueName(Paths &paths) +std::string &getNmQueueName(const Paths &paths) { static std::string name = "chatterino_gui" + paths.applicationFilePathHash.toStdString(); @@ -113,184 +109,226 @@ std::string &getNmQueueName(Paths &paths) // CLIENT -void NativeMessagingClient::sendMessage(const QByteArray &array) -{ - try +namespace nm::client { + + void sendMessage(const QByteArray &array) { - ipc::message_queue messageQueue(ipc::open_only, "chatterino_gui"); - - messageQueue.try_send(array.data(), size_t(array.size()), 1); - // messageQueue.timed_send(array.data(), size_t(array.size()), 1, - // boost::posix_time::second_clock::local_time() + - // boost::posix_time::seconds(10)); + ipc::sendMessage("chatterino_gui", array); } - catch (ipc::interprocess_exception &ex) + + void writeToCout(const QByteArray &array) { - qCDebug(chatterinoNativeMessage) << "send to gui process:" << ex.what(); + const auto *data = array.data(); + auto size = uint32_t(array.size()); + + // We're writing the raw bytes to cout. + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + std::cout.write(reinterpret_cast(&size), 4); + std::cout.write(data, size); + std::cout.flush(); } -} -void NativeMessagingClient::writeToCout(const QByteArray &array) -{ - auto *data = array.data(); - auto size = uint32_t(array.size()); - - std::cout.write(reinterpret_cast(&size), 4); - std::cout.write(data, size); - std::cout.flush(); -} +} // namespace nm::client // SERVER +NativeMessagingServer::NativeMessagingServer() + : thread(*this) +{ + this->thread.setObjectName("NativeMessagingReceiver"); +} + +NativeMessagingServer::~NativeMessagingServer() +{ + if (!ipc::IpcQueue::remove("chatterino_gui")) + { + qCWarning(chatterinoNativeMessage) << "Failed to remove message queue"; + } + this->thread.requestInterruption(); + this->thread.quit(); + // Most likely, the receiver thread will still wait for a message + if (!this->thread.wait(250)) + { + this->thread.terminate(); + } +} void NativeMessagingServer::start() { this->thread.start(); } +NativeMessagingServer::ReceiverThread::ReceiverThread( + NativeMessagingServer &parent) + : parent_(parent) +{ +} + void NativeMessagingServer::ReceiverThread::run() { - try - { - ipc::message_queue::remove("chatterino_gui"); - ipc::message_queue messageQueue(ipc::open_or_create, "chatterino_gui", - 100, MESSAGE_SIZE); + auto [messageQueue, error] = + ipc::IpcQueue::tryReplaceOrCreate("chatterino_gui", 100, MESSAGE_SIZE); - while (true) - { - try - { - auto buf = std::make_unique(MESSAGE_SIZE); - auto retSize = ipc::message_queue::size_type(); - auto priority = static_cast(0); - - messageQueue.receive(buf.get(), MESSAGE_SIZE, retSize, - priority); - - auto document = QJsonDocument::fromJson( - QByteArray::fromRawData(buf.get(), retSize)); - - this->handleMessage(document.object()); - } - catch (ipc::interprocess_exception &ex) - { - qCDebug(chatterinoNativeMessage) - << "received from gui process:" << ex.what(); - } - } - } - catch (ipc::interprocess_exception &ex) + if (!error.isEmpty()) { qCDebug(chatterinoNativeMessage) - << "run ipc message queue:" << ex.what(); + << "Failed to create message queue:" << error; - nmIpcError().set(QString::fromLatin1(ex.what())); + nmIpcError().set(error); + return; + } + + while (!this->isInterruptionRequested()) + { + auto buf = messageQueue->receive(); + if (buf.isEmpty()) + { + continue; + } + auto document = QJsonDocument::fromJson(buf); + + this->handleMessage(document.object()); } } void NativeMessagingServer::ReceiverThread::handleMessage( const QJsonObject &root) { - auto app = getApp(); - QString action = root.value("action").toString(); - - if (action.isNull()) - { - qCDebug(chatterinoNativeMessage) << "NM action was null"; - return; - } + QString action = root["action"_L1].toString(); if (action == "select") { - QString _type = root.value("type").toString(); - bool attach = root.value("attach").toBool(); - bool attachFullscreen = root.value("attach_fullscreen").toBool(); - QString name = root.value("name").toString(); - -#ifdef USEWINSDK - AttachedWindow::GetArgs args; - args.winId = root.value("winId").toString(); - args.yOffset = root.value("yOffset").toInt(-1); - - { - const auto sizeObject = root.value("size").toObject(); - args.x = sizeObject.value("x").toDouble(-1.0); - args.pixelRatio = sizeObject.value("pixelRatio").toDouble(-1.0); - args.width = sizeObject.value("width").toInt(-1); - args.height = sizeObject.value("height").toInt(-1); - } - - args.fullscreen = attachFullscreen; - - qCDebug(chatterinoNativeMessage) - << args.x << args.pixelRatio << args.width << args.height - << args.winId; - - if (_type.isNull() || args.winId.isNull()) - { - qCDebug(chatterinoNativeMessage) - << "NM type, name or winId missing"; - attach = false; - attachFullscreen = false; - return; - } -#endif - - if (_type == "twitch") - { - postToThread([=] { - if (!name.isEmpty()) - { - app->twitch->watchingChannel.reset( - app->twitch->getOrAddChannel(name)); - } - - if (attach || attachFullscreen) - { -#ifdef USEWINSDK - // if (args.height != -1) { - auto *window = - AttachedWindow::get(::GetForegroundWindow(), args); - if (!name.isEmpty()) - { - window->setChannel(app->twitch->getOrAddChannel(name)); - } -// } -// window->show(); -#endif - } - }); - } - else - { - qCDebug(chatterinoNativeMessage) << "NM unknown channel type"; - } + this->handleSelect(root); + return; } - else if (action == "detach") + if (action == "detach") { - QString winId = root.value("winId").toString(); - - if (winId.isNull()) - { - qCDebug(chatterinoNativeMessage) << "NM winId missing"; - return; - } - -#ifdef USEWINSDK - postToThread([winId] { - qCDebug(chatterinoNativeMessage) << "NW detach"; - AttachedWindow::detach(winId); - }); -#endif + this->handleDetach(root); + return; } - else + if (action == "sync") { - qCDebug(chatterinoNativeMessage) << "NM unknown action " + action; + this->handleSync(root); + return; } + + qCDebug(chatterinoNativeMessage) << "NM unknown action" << action; } -Atomic> &nmIpcError() +// NOLINTBEGIN(readability-convert-member-functions-to-static) +void NativeMessagingServer::ReceiverThread::handleSelect( + const QJsonObject &root) { - static Atomic> x; + QString type = root["type"_L1].toString(); + bool attach = root["attach"_L1].toBool(); + bool attachFullscreen = root["attach_fullscreen"_L1].toBool(); + QString name = root["name"_L1].toString(); + +#ifdef USEWINSDK + const auto sizeObject = root["size"_L1].toObject(); + AttachedWindow::GetArgs args = { + .winId = root["winId"_L1].toString(), + .yOffset = root["yOffset"_L1].toInt(-1), + .x = sizeObject["x"_L1].toDouble(-1.0), + .pixelRatio = sizeObject["pixelRatio"_L1].toDouble(-1.0), + .width = sizeObject["width"_L1].toInt(-1), + .height = sizeObject["height"_L1].toInt(-1), + .fullscreen = attachFullscreen, + }; + + qCDebug(chatterinoNativeMessage) + << args.x << args.pixelRatio << args.width << args.height << args.winId; + + if (args.winId.isNull()) + { + qCDebug(chatterinoNativeMessage) << "winId in select is missing"; + return; + } +#endif + + if (type != u"twitch"_s) + { + qCDebug(chatterinoNativeMessage) << "NM unknown channel type"; + return; + } + + postToThread([=] { + if (!name.isEmpty()) + { + auto channel = getApp()->getTwitch()->getOrAddChannel(name); + if (getApp()->getTwitch()->getWatchingChannel().get() != channel) + { + getApp()->getTwitch()->setWatchingChannel(channel); + } + } + + if (attach || attachFullscreen) + { +#ifdef USEWINSDK + auto *window = AttachedWindow::getForeground(args); + if (!name.isEmpty()) + { + window->setChannel( + getApp()->getTwitch()->getOrAddChannel(name)); + } +#endif + } + }); +} + +void NativeMessagingServer::ReceiverThread::handleDetach( + const QJsonObject &root) +{ + QString winId = root["winId"_L1].toString(); + + if (winId.isNull()) + { + qCDebug(chatterinoNativeMessage) << "NM winId missing"; + return; + } + +#ifdef USEWINSDK + postToThread([winId] { + qCDebug(chatterinoNativeMessage) << "NW detach"; + AttachedWindow::detach(winId); + }); +#endif +} +// NOLINTEND(readability-convert-member-functions-to-static) + +void NativeMessagingServer::ReceiverThread::handleSync(const QJsonObject &root) +{ + // Structure: + // { action: 'sync', twitchChannels?: string[] } + postToThread([&parent = this->parent_, + twitch = root["twitchChannels"_L1].toArray()] { + parent.syncChannels(twitch); + }); +} + +void NativeMessagingServer::syncChannels(const QJsonArray &twitchChannels) +{ + assertInGuiThread(); + + std::vector updated; + updated.reserve(twitchChannels.size()); + for (const auto &value : twitchChannels) + { + auto name = value.toString(); + if (name.isEmpty()) + { + continue; + } + // the deduping is done on the extension side + updated.emplace_back(getApp()->getTwitch()->getOrAddChannel(name)); + } + + // This will destroy channels that aren't used anymore. + this->channelWarmer_ = std::move(updated); +} + +Atomic> &nmIpcError() +{ + static Atomic> x; return x; } diff --git a/src/singletons/NativeMessaging.hpp b/src/singletons/NativeMessaging.hpp index 2bb0107b2..0d3ba454f 100644 --- a/src/singletons/NativeMessaging.hpp +++ b/src/singletons/NativeMessaging.hpp @@ -1,43 +1,71 @@ #pragma once +#include "common/Atomic.hpp" + #include #include -#include -#include + +#include +#include namespace chatterino { class Application; class Paths; +class Channel; -void registerNmHost(Paths &paths); -std::string &getNmQueueName(Paths &paths); +using ChannelPtr = std::shared_ptr; -Atomic> &nmIpcError(); +void registerNmHost(const Paths &paths); +std::string &getNmQueueName(const Paths &paths); + +Atomic> &nmIpcError(); + +namespace nm::client { -class NativeMessagingClient final -{ -public: void sendMessage(const QByteArray &array); void writeToCout(const QByteArray &array); -}; + +} // namespace nm::client class NativeMessagingServer final { public: + NativeMessagingServer(); + NativeMessagingServer(const NativeMessagingServer &) = delete; + NativeMessagingServer(NativeMessagingServer &&) = delete; + NativeMessagingServer &operator=(const NativeMessagingServer &) = delete; + NativeMessagingServer &operator=(NativeMessagingServer &&) = delete; + ~NativeMessagingServer(); + void start(); private: class ReceiverThread : public QThread { public: + ReceiverThread(NativeMessagingServer &parent); + void run() override; private: void handleMessage(const QJsonObject &root); + void handleSelect(const QJsonObject &root); + void handleDetach(const QJsonObject &root); + void handleSync(const QJsonObject &root); + + NativeMessagingServer &parent_; }; + void syncChannels(const QJsonArray &twitchChannels); + ReceiverThread thread; + + /// This vector contains all channels that are open the user's browser. + /// These channels are joined to be able to switch channels more quickly. + std::vector channelWarmer_; + + friend ReceiverThread; }; } // namespace chatterino diff --git a/src/singletons/Paths.cpp b/src/singletons/Paths.cpp index 6844af029..b315f16da 100644 --- a/src/singletons/Paths.cpp +++ b/src/singletons/Paths.cpp @@ -1,26 +1,22 @@ #include "singletons/Paths.hpp" +#include "common/Modes.hpp" #include "singletons/Settings.hpp" +#include "util/CombinePath.hpp" #include #include #include #include -#include -#include "common/Modes.hpp" -#include "util/CombinePath.hpp" +#include using namespace std::literals; namespace chatterino { -Paths *Paths::instance = nullptr; - Paths::Paths() { - this->instance = this; - this->initAppFilePathHash(); this->initCheckPortable(); @@ -33,12 +29,12 @@ bool Paths::createFolder(const QString &folderPath) return QDir().mkpath(folderPath); } -bool Paths::isPortable() +bool Paths::isPortable() const { return Modes::instance().isPortable; } -QString Paths::cacheDirectory() +QString Paths::cacheDirectory() const { static const auto pathSetting = [] { QStringSetting cachePathSetting("/cache/path"); @@ -83,14 +79,14 @@ void Paths::initCheckPortable() void Paths::initRootDirectory() { - assert(this->portable_.is_initialized()); + assert(this->portable_.has_value()); // Root path = %APPDATA%/Chatterino or the folder that the executable // resides in this->rootAppDataDirectory = [&]() -> QString { // portable - if (this->isPortable()) + if (Modes::instance().isPortable) { return QCoreApplication::applicationDirPath(); } @@ -122,9 +118,8 @@ void Paths::initSubDirectories() // create settings subdirectories and validate that they are created // properly - auto makePath = [&](const std::string &name) -> QString { - auto path = combinePath(this->rootAppDataDirectory, - QString::fromStdString(name)); + auto makePath = [&](const QString &name) -> QString { + auto path = combinePath(this->rootAppDataDirectory, name); if (!QDir().mkpath(path)) { @@ -140,13 +135,18 @@ void Paths::initSubDirectories() this->cacheDirectory_ = makePath("Cache"); this->messageLogDirectory = makePath("Logs"); this->miscDirectory = makePath("Misc"); - this->twitchProfileAvatars = makePath("ProfileAvatars"); - //QDir().mkdir(this->twitchProfileAvatars + "/twitch"); -} - -Paths *getPaths() -{ - return Paths::instance; + this->twitchProfileAvatars = + makePath(combinePath("ProfileAvatars", "twitch")); + this->pluginsDirectory = makePath("Plugins"); + this->themesDirectory = makePath("Themes"); + this->crashdumpDirectory = makePath("Crashes"); +#ifdef Q_OS_WIN + this->ipcDirectory = makePath("IPC"); +#else + // NOTE: We do *NOT* use IPC on non-Windows platforms. + // If we start, we should re-consider this directory. + this->ipcDirectory = "/tmp"; +#endif } } // namespace chatterino diff --git a/src/singletons/Paths.hpp b/src/singletons/Paths.hpp index 62b1f2c2a..eabd06933 100644 --- a/src/singletons/Paths.hpp +++ b/src/singletons/Paths.hpp @@ -1,15 +1,14 @@ #pragma once #include -#include + +#include namespace chatterino { class Paths { public: - static Paths *instance; - Paths(); // Root directory for the configuration files. %APPDATA%/chatterino or @@ -25,16 +24,31 @@ public: // Directory for miscellaneous files. Same as /Misc QString miscDirectory; + // Directory for crashdumps. Same as /Crashes + QString crashdumpDirectory; + // Hash of QCoreApplication::applicationFilePath() QString applicationFilePathHash; - // Profile avatars for Twitch /cache/twitch + // Profile avatars for Twitch /ProfileAvatars/twitch QString twitchProfileAvatars; - bool createFolder(const QString &folderPath); - bool isPortable(); + // Plugin files live here. /Plugins + QString pluginsDirectory; - QString cacheDirectory(); + // Custom themes live here. /Themes + QString themesDirectory; + + // Directory for shared memory files. + // /IPC on Windows + // /tmp elsewhere + QString ipcDirectory; + + bool createFolder(const QString &folderPath); + [[deprecated("use Modes::instance().portable instead")]] bool isPortable() + const; + + QString cacheDirectory() const; private: void initAppFilePathHash(); @@ -42,12 +56,10 @@ private: void initRootDirectory(); void initSubDirectories(); - boost::optional portable_; + std::optional portable_; // Directory for cache files. Same as /Misc QString cacheDirectory_; }; -Paths *getPaths(); - } // namespace chatterino diff --git a/src/singletons/Resources.cpp b/src/singletons/Resources.cpp index 18995dc31..7bafdd4fb 100644 --- a/src/singletons/Resources.cpp +++ b/src/singletons/Resources.cpp @@ -2,10 +2,15 @@ #include "debug/AssertInGuiThread.hpp" -namespace chatterino { namespace { - static Resources2 *resources = nullptr; -} + +using namespace chatterino; + +static Resources2 *resources = nullptr; + +} // namespace + +namespace chatterino { Resources2 &getResources() { diff --git a/src/singletons/Resources.hpp b/src/singletons/Resources.hpp index f4217ca8f..791039351 100644 --- a/src/singletons/Resources.hpp +++ b/src/singletons/Resources.hpp @@ -1,6 +1,6 @@ #pragma once -#include "autogenerated/ResourcesAutogen.hpp" +#include "ResourcesAutogen.hpp" namespace chatterino { diff --git a/src/singletons/Settings.cpp b/src/singletons/Settings.cpp index f81620914..a62f46585 100644 --- a/src/singletons/Settings.cpp +++ b/src/singletons/Settings.cpp @@ -1,70 +1,84 @@ #include "singletons/Settings.hpp" #include "Application.hpp" +#include "common/Args.hpp" +#include "controllers/filters/FilterRecord.hpp" +#include "controllers/highlights/HighlightBadge.hpp" #include "controllers/highlights/HighlightBlacklistUser.hpp" #include "controllers/highlights/HighlightPhrase.hpp" #include "controllers/ignores/IgnorePhrase.hpp" -#include "singletons/Paths.hpp" -#include "singletons/Resources.hpp" -#include "singletons/WindowManager.hpp" -#include "util/PersistSignalVector.hpp" +#include "controllers/moderationactions/ModerationAction.hpp" +#include "controllers/nicknames/Nickname.hpp" +#include "debug/Benchmark.hpp" +#include "pajlada/settings/signalargs.hpp" #include "util/WindowsHelper.hpp" +#include + +namespace { + +using namespace chatterino; + +template +void initializeSignalVector(pajlada::Signals::SignalHolder &signalHolder, + ChatterinoSetting> &setting, + SignalVector &vec) +{ + // Fill the SignalVector up with initial values + for (auto &&item : setting.getValue()) + { + vec.append(item); + } + + // Set up a signal to + signalHolder.managedConnect(vec.delayedItemsChanged, [&] { + setting.setValue(vec.raw()); + }); +} + +} // namespace + namespace chatterino { -ConcurrentSettings *concurrentInstance_{}; +std::vector> _settings; -ConcurrentSettings::ConcurrentSettings() - // NOTE: these do not get deleted - : highlightedMessages(*new SignalVector()) - , highlightedUsers(*new SignalVector()) - , highlightedBadges(*new SignalVector()) - , blacklistedUsers(*new SignalVector()) - , ignoredMessages(*new SignalVector()) - , mutedChannels(*new SignalVector()) - , filterRecords(*new SignalVector()) - , nicknames(*new SignalVector()) - , moderationActions(*new SignalVector) +void _actuallyRegisterSetting( + std::weak_ptr setting) { - persist(this->highlightedMessages, "/highlighting/highlights"); - persist(this->blacklistedUsers, "/highlighting/blacklist"); - persist(this->highlightedBadges, "/highlighting/badges"); - persist(this->highlightedUsers, "/highlighting/users"); - persist(this->ignoredMessages, "/ignore/phrases"); - persist(this->mutedChannels, "/pings/muted"); - persist(this->filterRecords, "/filtering/filters"); - persist(this->nicknames, "/nicknames"); - // tagged users? - persist(this->moderationActions, "/moderation/actions"); + _settings.push_back(std::move(setting)); } -bool ConcurrentSettings::isHighlightedUser(const QString &username) +bool Settings::isHighlightedUser(const QString &username) { auto items = this->highlightedUsers.readOnly(); for (const auto &highlightedUser : *items) { if (highlightedUser.isMatch(username)) + { return true; + } } return false; } -bool ConcurrentSettings::isBlacklistedUser(const QString &username) +bool Settings::isBlacklistedUser(const QString &username) { auto items = this->blacklistedUsers.readOnly(); for (const auto &blacklistedUser : *items) { if (blacklistedUser.isMatch(username)) + { return true; + } } return false; } -bool ConcurrentSettings::isMutedChannel(const QString &channelName) +bool Settings::isMutedChannel(const QString &channelName) { auto items = this->mutedChannels.readOnly(); @@ -78,12 +92,27 @@ bool ConcurrentSettings::isMutedChannel(const QString &channelName) return false; } -void ConcurrentSettings::mute(const QString &channelName) +std::optional Settings::matchNickname(const QString &usernameText) +{ + auto nicknames = this->nicknames.readOnly(); + + for (const auto &nickname : *nicknames) + { + if (auto nicknameText = nickname.match(usernameText)) + { + return nicknameText; + } + } + + return std::nullopt; +} + +void Settings::mute(const QString &channelName) { mutedChannels.append(channelName); } -void ConcurrentSettings::unmute(const QString &channelName) +void Settings::unmute(const QString &channelName) { for (std::vector::size_type i = 0; i != mutedChannels.raw().size(); i++) @@ -96,7 +125,7 @@ void ConcurrentSettings::unmute(const QString &channelName) } } -bool ConcurrentSettings::toggleMutedChannel(const QString &channelName) +bool Settings::toggleMutedChannel(const QString &channelName) { if (this->isMutedChannel(channelName)) { @@ -110,21 +139,46 @@ bool ConcurrentSettings::toggleMutedChannel(const QString &channelName) } } -ConcurrentSettings &getCSettings() -{ - // `concurrentInstance_` gets assigned in Settings ctor. - assert(concurrentInstance_); - - return *concurrentInstance_; -} - Settings *Settings::instance_ = nullptr; -Settings::Settings(const QString &settingsDirectory) - : ABSettings(settingsDirectory) +Settings::Settings(const Args &args, const QString &settingsDirectory) + : prevInstance_(Settings::instance_) + , disableSaving(args.dontSaveSettings) { + QString settingsPath = settingsDirectory + "/settings.json"; + + // get global instance of the settings library + auto settingsInstance = pajlada::Settings::SettingManager::getInstance(); + + settingsInstance->load(qPrintable(settingsPath)); + + settingsInstance->setBackupEnabled(true); + settingsInstance->setBackupSlots(9); + settingsInstance->saveMethod = + pajlada::Settings::SettingManager::SaveMethod::SaveManually; + + initializeSignalVector(this->signalHolder, this->highlightedMessagesSetting, + this->highlightedMessages); + initializeSignalVector(this->signalHolder, this->highlightedUsersSetting, + this->highlightedUsers); + initializeSignalVector(this->signalHolder, this->highlightedBadgesSetting, + this->highlightedBadges); + initializeSignalVector(this->signalHolder, this->blacklistedUsersSetting, + this->blacklistedUsers); + initializeSignalVector(this->signalHolder, this->ignoredMessagesSetting, + this->ignoredMessages); + initializeSignalVector(this->signalHolder, this->mutedChannelsSetting, + this->mutedChannels); + initializeSignalVector(this->signalHolder, this->filterRecordsSetting, + this->filterRecords); + initializeSignalVector(this->signalHolder, this->nicknamesSetting, + this->nicknames); + initializeSignalVector(this->signalHolder, this->moderationActionsSetting, + this->moderationActions); + initializeSignalVector(this->signalHolder, this->loggedChannelsSetting, + this->loggedChannels); + instance_ = this; - concurrentInstance_ = this; #ifdef USEWINSDK this->autorun = isRegisteredForStartup(); @@ -136,8 +190,105 @@ Settings::Settings(const QString &settingsDirectory) #endif } +Settings::~Settings() +{ + Settings::instance_ = this->prevInstance_; +} + +void Settings::requestSave() const +{ + if (this->disableSaving) + { + return; + } + + pajlada::Settings::SettingManager::gSave(); +} + +void Settings::saveSnapshot() +{ + BenchmarkGuard benchmark("Settings::saveSnapshot"); + + rapidjson::Document *d = new rapidjson::Document(rapidjson::kObjectType); + rapidjson::Document::AllocatorType &a = d->GetAllocator(); + + for (const auto &weakSetting : _settings) + { + auto setting = weakSetting.lock(); + if (!setting) + { + continue; + } + + rapidjson::Value key(setting->getPath().c_str(), a); + auto *curVal = setting->unmarshalJSON(); + if (curVal == nullptr) + { + continue; + } + + rapidjson::Value val; + val.CopyFrom(*curVal, a); + d->AddMember(key.Move(), val.Move(), a); + } + + // log("Snapshot state: {}", rj::stringify(*d)); + + this->snapshot_.reset(d); +} + +void Settings::restoreSnapshot() +{ + if (!this->snapshot_) + { + return; + } + + BenchmarkGuard benchmark("Settings::restoreSnapshot"); + + const auto &snapshot = *(this->snapshot_.get()); + + if (!snapshot.IsObject()) + { + return; + } + + for (const auto &weakSetting : _settings) + { + auto setting = weakSetting.lock(); + if (!setting) + { + continue; + } + + const char *path = setting->getPath().c_str(); + + if (!snapshot.HasMember(path)) + { + continue; + } + + pajlada::Settings::SignalArgs args; + args.compareBeforeSet = true; + + setting->marshalJSON(snapshot[path], std::move(args)); + } +} + +float Settings::getClampedUiScale() const +{ + return std::clamp(this->uiScale.getValue(), 0.2F, 10.F); +} + +void Settings::setClampedUiScale(float value) +{ + this->uiScale.setValue(std::clamp(value, 0.2F, 10.F)); +} + Settings &Settings::instance() { + assert(instance_ != nullptr); + return *instance_; } diff --git a/src/singletons/Settings.hpp b/src/singletons/Settings.hpp index 40b0ef638..39769ad9b 100644 --- a/src/singletons/Settings.hpp +++ b/src/singletons/Settings.hpp @@ -1,74 +1,109 @@ #pragma once -#include -#include - -#include "BaseSettings.hpp" #include "common/Channel.hpp" +#include "common/ChatterinoSetting.hpp" +#include "common/enums/MessageOverflow.hpp" #include "common/SignalVector.hpp" #include "controllers/filters/FilterRecord.hpp" #include "controllers/highlights/HighlightBadge.hpp" +#include "controllers/highlights/HighlightBlacklistUser.hpp" #include "controllers/highlights/HighlightPhrase.hpp" +#include "controllers/ignores/IgnorePhrase.hpp" +#include "controllers/logging/ChannelLog.hpp" #include "controllers/moderationactions/ModerationAction.hpp" #include "controllers/nicknames/Nickname.hpp" +#include "controllers/sound/ISoundController.hpp" #include "singletons/Toasts.hpp" -#include "util/StreamerMode.hpp" +#include "util/RapidJsonSerializeQString.hpp" #include "widgets/Notebook.hpp" +#include +#include +#include + using TimeoutButton = std::pair; namespace chatterino { -class HighlightPhrase; -class HighlightBlacklistUser; -class IgnorePhrase; -class FilterRecord; -class Nickname; +class Args; -/// Settings which are available for reading on all threads. -class ConcurrentSettings -{ -public: - ConcurrentSettings(); +#ifdef Q_OS_WIN32 +# define DEFAULT_FONT_FAMILY "Segoe UI" +# define DEFAULT_FONT_SIZE 10 +#else +# ifdef Q_OS_MACOS +# define DEFAULT_FONT_FAMILY "Helvetica Neue" +# define DEFAULT_FONT_SIZE 12 +# else +# define DEFAULT_FONT_FAMILY "Arial" +# define DEFAULT_FONT_SIZE 11 +# endif +#endif - SignalVector &highlightedMessages; - SignalVector &highlightedUsers; - SignalVector &highlightedBadges; - SignalVector &blacklistedUsers; - SignalVector &ignoredMessages; - SignalVector &mutedChannels; - SignalVector &filterRecords; - SignalVector &nicknames; - SignalVector &moderationActions; - - bool isHighlightedUser(const QString &username); - bool isBlacklistedUser(const QString &username); - bool isMutedChannel(const QString &channelName); - bool toggleMutedChannel(const QString &channelName); - -private: - void mute(const QString &channelName); - void unmute(const QString &channelName); -}; - -ConcurrentSettings &getCSettings(); +void _actuallyRegisterSetting( + std::weak_ptr setting); enum UsernameDisplayMode : int { Username = 1, // Username LocalizedName = 2, // Localized name UsernameAndLocalizedName = 3, // Username (Localized name) }; + +enum ThumbnailPreviewMode : int { + DontShow = 0, + + AlwaysShow = 1, + + ShowOnShift = 2, +}; + +enum UsernameRightClickBehavior : int { + Reply = 0, + Mention = 1, + Ignore = 2, +}; + +enum class ChatSendProtocol : int { + Default = 0, + IRC = 1, + Helix = 2, +}; + +enum StreamerModeSetting { + Disabled = 0, + Enabled = 1, + DetectStreamingSoftware = 2, +}; + /// Settings which are availlable for reading and writing on the gui thread. // These settings are still accessed concurrently in the code but it is bad practice. -class Settings : public ABSettings, public ConcurrentSettings +class Settings { static Settings *instance_; + Settings *prevInstance_ = nullptr; + + const bool disableSaving; public: - Settings(const QString &settingsDirectory); + Settings(const Args &args, const QString &settingsDirectory); + ~Settings(); static Settings &instance(); + /// Request the settings to be saved to file + /// + /// Depending on the launch options, a save might end up not happening + void requestSave() const; + + void saveSnapshot(); + void restoreSnapshot(); + + FloatSetting uiScale = {"/appearance/uiScale2", 1}; + BoolSetting windowTopMost = {"/appearance/windowAlwaysOnTop", false}; + + float getClampedUiScale() const; + void setClampedUiScale(float value); + /// Appearance BoolSetting showTimestamps = {"/appearance/messages/showTimestamps", true}; BoolSetting animationsWhenFocused = { @@ -84,9 +119,10 @@ public: BoolSetting showEmptyInput = {"/appearance/showEmptyInputBox", true}; BoolSetting showMessageLength = {"/appearance/messages/showMessageLength", false}; + EnumSetting messageOverflow = { + "/appearance/messages/messageOverflow", MessageOverflow::Highlight}; BoolSetting separateMessages = {"/appearance/messages/separateMessages", false}; - BoolSetting compactEmotes = {"/appearance/messages/compactEmotes", true}; BoolSetting hideModerated = {"/appearance/messages/hideModerated", false}; BoolSetting hideModerationActions = { "/appearance/messages/hideModerationActions", false}; @@ -100,10 +136,24 @@ public: EnumSetting tabDirection = {"/appearance/tabDirection", NotebookTabLocation::Top}; + EnumSetting tabVisibility = { + "/appearance/tabVisibility", + NotebookTabVisibility::AllTabs, + }; // BoolSetting collapseLongMessages = // {"/appearance/messages/collapseLongMessages", false}; + QStringSetting chatFontFamily{ + "/appearance/currentFontFamily", + DEFAULT_FONT_FAMILY, + }; + IntSetting chatFontSize{ + "/appearance/currentFontSize", + DEFAULT_FONT_SIZE, + }; + BoolSetting hideReplyContext = {"/appearance/hideReplyContext", false}; BoolSetting showReplyButton = {"/appearance/showReplyButton", false}; + BoolSetting stripReplyMention = {"/appearance/stripReplyMention", true}; IntSetting collpseMessagesMinLines = { "/appearance/messages/collapseMessagesMinLines", 0}; BoolSetting alternateMessages = { @@ -149,6 +199,7 @@ public: "/appearance/badges/useCustomFfzModeratorBadges", true}; BoolSetting useCustomFfzVipBadges = { "/appearance/badges/useCustomFfzVipBadges", true}; + BoolSetting showBadgesSevenTV = {"/appearance/badges/seventv", true}; /// Behaviour BoolSetting allowDuplicateMessages = {"/behaviour/allowDuplicateMessages", @@ -161,6 +212,24 @@ public: BoolSetting autoCloseUserPopup = {"/behaviour/autoCloseUserPopup", true}; BoolSetting autoCloseThreadPopup = {"/behaviour/autoCloseThreadPopup", false}; + + EnumSetting usernameRightClickBehavior = { + "/behaviour/usernameRightClickBehavior", + UsernameRightClickBehavior::Mention, + }; + EnumSetting usernameRightClickModifierBehavior = + { + "/behaviour/usernameRightClickBehaviorWithModifier", + UsernameRightClickBehavior::Reply, + }; + EnumSetting usernameRightClickModifier = { + "/behaviour/usernameRightClickModifier", + Qt::KeyboardModifier::ShiftModifier}; + + BoolSetting autoSubToParticipatedThreads = { + "/behaviour/autoSubToParticipatedThreads", + true, + }; // BoolSetting twitchSeperateWriteConnection = // {"/behaviour/twitchSeperateWriteConnection", false}; @@ -177,6 +246,14 @@ public: "/behaviour/autocompletion/emoteCompletionWithColon", true}; BoolSetting showUsernameCompletionMenu = { "/behaviour/autocompletion/showUsernameCompletionMenu", true}; + BoolSetting alwaysIncludeBroadcasterInUserCompletions = { + "/behaviour/autocompletion/alwaysIncludeBroadcasterInUserCompletions", + true, + }; + BoolSetting useSmartEmoteCompletion = { + "/experiments/useSmartEmoteCompletion", + false, + }; FloatSetting pauseOnHoverDuration = {"/behaviour/pauseOnHoverDuration", 0}; EnumSetting pauseChatModifier = { @@ -193,8 +270,10 @@ public: false}; BoolSetting enableEmoteImages = {"/emotes/enableEmoteImages", true}; BoolSetting animateEmotes = {"/emotes/enableGifAnimations", true}; + BoolSetting enableZeroWidthEmotes = {"/emotes/enableZeroWidthEmotes", true}; FloatSetting emoteScale = {"/emotes/scale", 1.f}; - + BoolSetting showUnlistedSevenTVEmotes = { + "/emotes/showUnlistedSevenTVEmotes", false}; QStringSetting emojiSet = {"/emotes/emojiSet", "Twitter"}; BoolSetting stackBits = {"/emotes/stackBits", false}; @@ -203,8 +282,13 @@ public: BoolSetting enableBTTVGlobalEmotes = {"/emotes/bttv/global", true}; BoolSetting enableBTTVChannelEmotes = {"/emotes/bttv/channel", true}; + BoolSetting enableBTTVLiveUpdates = {"/emotes/bttv/liveupdates", true}; BoolSetting enableFFZGlobalEmotes = {"/emotes/ffz/global", true}; BoolSetting enableFFZChannelEmotes = {"/emotes/ffz/channel", true}; + BoolSetting enableSevenTVGlobalEmotes = {"/emotes/seventv/global", true}; + BoolSetting enableSevenTVChannelEmotes = {"/emotes/seventv/channel", true}; + BoolSetting enableSevenTVEventAPI = {"/emotes/seventv/eventapi", true}; + BoolSetting sendSevenTVActivity = {"/emotes/seventv/sendActivity", true}; /// Links BoolSetting linksDoubleClickOnly = {"/links/doubleClickToOpen", false}; @@ -223,9 +307,13 @@ public: "/streamerMode/hideLinkThumbnails", true}; BoolSetting streamerModeHideViewerCountAndDuration = { "/streamerMode/hideViewerCountAndDuration", false}; + BoolSetting streamerModeHideModActions = {"/streamerMode/hideModActions", + true}; BoolSetting streamerModeMuteMentions = {"/streamerMode/muteMentions", true}; BoolSetting streamerModeSuppressLiveNotifications = { "/streamerMode/supressLiveNotifications", false}; + BoolSetting streamerModeSuppressInlineWhispers = { + "/streamerMode/suppressInlineWhispers", true}; /// Ignored Phrases QStringSetting ignoredPhraseReplace = {"/ignore/ignoredPhraseReplace", @@ -244,7 +332,6 @@ public: /// Highlighting // BoolSetting enableHighlights = {"/highlighting/enabled", true}; - BoolSetting customHighlightSound = {"/highlighting/useCustomSound", false}; BoolSetting enableSelfHighlight = { "/highlighting/selfHighlight/nameIsHighlightKeyword", true}; @@ -259,6 +346,13 @@ public: QStringSetting selfHighlightColor = {"/highlighting/selfHighlightColor", ""}; + BoolSetting enableSelfMessageHighlight = { + "/highlighting/selfMessageHighlight/enabled", false}; + BoolSetting showSelfMessageHighlightInMentions = { + "/highlighting/selfMessageHighlight/showInMentions", false}; + QStringSetting selfMessageHighlightColor = { + "/highlighting/selfMessageHighlight/color", ""}; + BoolSetting enableWhisperHighlight = { "/highlighting/whisperHighlight/whispersHighlighted", true}; BoolSetting enableWhisperHighlightSound = { @@ -276,8 +370,8 @@ public: // "/highlighting/redeemedHighlight/enableSound", false}; // BoolSetting enableRedeemedHighlightTaskbar = { // "/highlighting/redeemedHighlight/enableTaskbarFlashing", false}; - QStringSetting redeemedHighlightSoundUrl = { - "/highlighting/redeemedHighlightSoundUrl", ""}; + // QStringSetting redeemedHighlightSoundUrl = { + // "/highlighting/redeemedHighlightSoundUrl", ""}; QStringSetting redeemedHighlightColor = { "/highlighting/redeemedHighlightColor", ""}; @@ -287,11 +381,22 @@ public: // "/highlighting/firstMessageHighlight/enableSound", false}; // BoolSetting enableFirstMessageHighlightTaskbar = { // "/highlighting/firstMessageHighlight/enableTaskbarFlashing", false}; - QStringSetting firstMessageHighlightSoundUrl = { - "/highlighting/firstMessageHighlightSoundUrl", ""}; + // QStringSetting firstMessageHighlightSoundUrl = { + // "/highlighting/firstMessageHighlightSoundUrl", ""}; QStringSetting firstMessageHighlightColor = { "/highlighting/firstMessageHighlightColor", ""}; + BoolSetting enableElevatedMessageHighlight = { + "/highlighting/elevatedMessageHighlight/highlighted", true}; + // BoolSetting enableElevatedMessageHighlightSound = { + // "/highlighting/elevatedMessageHighlight/enableSound", false}; + // BoolSetting enableElevatedMessageHighlightTaskbar = { + // "/highlighting/elevatedMessageHighlight/enableTaskbarFlashing", false}; + // QStringSetting elevatedMessageHighlightSoundUrl = { + // "/highlighting/elevatedMessageHighlight/soundUrl", ""}; + QStringSetting elevatedMessageHighlightColor = { + "/highlighting/elevatedMessageHighlight/color", ""}; + BoolSetting enableSubHighlight = { "/highlighting/subHighlight/subsHighlighted", true}; BoolSetting enableSubHighlightSound = { @@ -302,6 +407,41 @@ public: ""}; QStringSetting subHighlightColor = {"/highlighting/subHighlightColor", ""}; + BoolSetting enableAutomodHighlight = { + "/highlighting/automod/enabled", + true, + }; + BoolSetting showAutomodInMentions = { + "/highlighting/automod/showInMentions", + false, + }; + BoolSetting enableAutomodHighlightSound = { + "/highlighting/automod/enableSound", + false, + }; + BoolSetting enableAutomodHighlightTaskbar = { + "/highlighting/automod/enableTaskbarFlashing", + false, + }; + QStringSetting automodHighlightSoundUrl = { + "/highlighting/automod/soundUrl", + "", + }; + QStringSetting automodHighlightColor = {"/highlighting/automod/color", ""}; + + BoolSetting enableThreadHighlight = { + "/highlighting/thread/nameIsHighlightKeyword", true}; + BoolSetting showThreadHighlightInMentions = { + "/highlighting/thread/showSelfHighlightInMentions", true}; + BoolSetting enableThreadHighlightSound = { + "/highlighting/thread/enableSound", true}; + BoolSetting enableThreadHighlightTaskbar = { + "/highlighting/thread/enableTaskbarFlashing", true}; + QStringSetting threadHighlightSoundUrl = { + "/highlighting/threadHighlightSoundUrl", ""}; + QStringSetting threadHighlightColor = {"/highlighting/threadHighlightColor", + ""}; + QStringSetting highlightColor = {"/highlighting/color", ""}; BoolSetting longAlerts = {"/highlighting/alerts", false}; @@ -314,6 +454,12 @@ public: /// Logging BoolSetting enableLogging = {"/logging/enabled", false}; + BoolSetting onlyLogListedChannels = {"/logging/onlyLogListedChannels", + false}; + BoolSetting separatelyStoreStreamLogs = { + "/logging/separatelyStoreStreamLogs", + false, + }; QStringSetting logPath = {"/logging/path", ""}; @@ -338,6 +484,8 @@ public: "qrc:/sounds/ping3.wav"}; BoolSetting notificationOnAnyChannel = {"/notifications/onAnyChannel", false}; + BoolSetting suppressInitialLiveNotification = { + "/notifications/suppressInitialLive", false}; BoolSetting notificationToast = {"/notifications/enableToast", false}; IntSetting openFromToast = {"/notifications/openFromToast", @@ -372,7 +520,6 @@ public: #ifdef Q_OS_LINUX BoolSetting useKeyring = {"/misc/useKeyring", true}; #endif - BoolSetting enableExperimentalIrc = {"/misc/experimentalIrc", false}; IntSetting startUpNotification = {"/misc/startUpNotification", 0}; QStringSetting currentVersion = {"/misc/currentVersion", ""}; @@ -383,12 +530,25 @@ public: "/misc/twitch/messageHistoryLimit", 800, }; + IntSetting scrollbackSplitLimit = { + "/misc/scrollback/splitLimit", + 1000, + }; + IntSetting scrollbackUsercardLimit = { + "/misc/scrollback/usercardLimit", + 1000, + }; + + EnumStringSetting chatSendProtocol = { + "/misc/chatSendProtocol", ChatSendProtocol::Default}; - IntSetting emotesTooltipPreview = {"/misc/emotesTooltipPreview", 1}; BoolSetting openLinksIncognito = {"/misc/openLinksIncognito", 0}; + EnumSetting emotesTooltipPreview = { + "/misc/emotesTooltipPreview", + ThumbnailPreviewMode::AlwaysShow, + }; QStringSetting cachePath = {"/cache/path", ""}; - BoolSetting restartOnCrash = {"/misc/restartOnCrash", false}; BoolSetting attachExtensionToAnyProcess = { "/misc/attachExtensionToAnyProcess", false}; BoolSetting askOnImageUpload = {"/misc/askOnImageUpload", true}; @@ -396,14 +556,9 @@ public: true}; BoolSetting lockNotebookLayout = {"/misc/lockNotebookLayout", false}; - /// Debug - BoolSetting showUnhandledIrcMessages = {"/debug/showUnhandledIrcMessages", - false}; - /// UI - // Purely QOL settings are here (like last item in a list). - IntSetting lastSelectChannelTab = {"/ui/lastSelectChannelTab", 0}; - IntSetting lastSelectIrcConn = {"/ui/lastSelectIrcConn", 0}; + + BoolSetting showSendButton = {"/ui/showSendButton", false}; // Similarity BoolSetting similarityEnabled = {"/similarity/similarityEnabled", false}; @@ -434,12 +589,66 @@ public: {"d", 1}, {"w", 1}}}; + BoolSetting pluginsEnabled = {"/plugins/supportEnabled", false}; + ChatterinoSetting> enabledPlugins = { + "/plugins/enabledPlugins", {}}; + + // Advanced + EnumStringSetting soundBackend = { + "/sound/backend", + SoundBackend::Miniaudio, + }; + private: + ChatterinoSetting> highlightedMessagesSetting = + {"/highlighting/highlights"}; + ChatterinoSetting> highlightedUsersSetting = { + "/highlighting/users"}; + ChatterinoSetting> highlightedBadgesSetting = { + "/highlighting/badges"}; + ChatterinoSetting> + blacklistedUsersSetting = {"/highlighting/blacklist"}; + ChatterinoSetting> ignoredMessagesSetting = { + "/ignore/phrases"}; + ChatterinoSetting> mutedChannelsSetting = { + "/pings/muted"}; + ChatterinoSetting> filterRecordsSetting = { + "/filtering/filters"}; + ChatterinoSetting> nicknamesSetting = {"/nicknames"}; + ChatterinoSetting> moderationActionsSetting = + {"/moderation/actions"}; + ChatterinoSetting> loggedChannelsSetting = { + "/logging/channels"}; + +public: + SignalVector highlightedMessages; + SignalVector highlightedUsers; + SignalVector highlightedBadges; + SignalVector blacklistedUsers; + SignalVector ignoredMessages; + SignalVector mutedChannels; + SignalVector filterRecords; + SignalVector nicknames; + SignalVector moderationActions; + SignalVector loggedChannels; + + bool isHighlightedUser(const QString &username); + bool isBlacklistedUser(const QString &username); + bool isMutedChannel(const QString &channelName); + bool toggleMutedChannel(const QString &channelName); + std::optional matchNickname(const QString &username); + +private: + void mute(const QString &channelName); + void unmute(const QString &channelName); + void updateModerationActions(); + + std::unique_ptr snapshot_; + + pajlada::Signals::SignalHolder signalHolder; }; -} // namespace chatterino +Settings *getSettings(); -#ifdef CHATTERINO -# include "singletons/Settings.hpp" -#endif +} // namespace chatterino diff --git a/src/singletons/StreamerMode.cpp b/src/singletons/StreamerMode.cpp new file mode 100644 index 000000000..67ca2d348 --- /dev/null +++ b/src/singletons/StreamerMode.cpp @@ -0,0 +1,315 @@ +#include "singletons/StreamerMode.hpp" + +#include "Application.hpp" +#include "common/Literals.hpp" +#include "common/QLogging.hpp" +#include "providers/twitch/TwitchIrcServer.hpp" +#include "singletons/Settings.hpp" +#include "util/PostToThread.hpp" + +#include +#include +#include +#include +#include + +#ifdef Q_OS_WIN +// clang-format off +# include +# include +# include +// clang-format on +#endif + +#include + +namespace { + +using namespace chatterino; +using namespace literals; + +/// Number of timeouts to skip if nothing called `isEnabled` in the meantime. +constexpr uint8_t SKIPPED_TIMEOUTS = 5; + +const QStringList &broadcastingBinaries() +{ +#ifdef USEWINSDK + static QStringList bins = { + u"obs.exe"_s, u"obs64.exe"_s, u"PRISMLiveStudio.exe"_s, + u"XSplit.Core.exe"_s, u"TwitchStudio.exe"_s, u"vMix64.exe"_s, + }; +#else + static QStringList bins = { + u"obs"_s, + u"Twitch Studio"_s, + u"Streamlabs Desktop"_s, + }; +#endif + return bins; +} + +bool isBroadcasterSoftwareActive() +{ +#if defined(Q_OS_LINUX) || defined(Q_OS_MACOS) + static bool shouldShowTimeoutWarning = true; + static bool shouldShowWarning = true; + + QProcess p; + p.start("pgrep", {"-xi", broadcastingBinaries().join("|")}, + QIODevice::NotOpen); + + if (p.waitForFinished(1000) && p.exitStatus() == QProcess::NormalExit) + { + return (p.exitCode() == 0); + } + + // Fallback to false and showing a warning + + switch (p.error()) + { + case QProcess::Timedout: { + qCWarning(chatterinoStreamerMode) << "pgrep execution timed out!"; + if (shouldShowTimeoutWarning) + { + shouldShowTimeoutWarning = false; + + postToThread([] { + getApp()->getTwitch()->addGlobalSystemMessage( + "Streamer Mode is set to Automatic, but pgrep timed " + "out. This can happen if your system lagged at the " + "wrong moment. If Streamer Mode continues to not work, " + "you can manually set it to Enabled or Disabled in the " + "Settings."); + }); + } + } + break; + + default: { + qCWarning(chatterinoStreamerMode) + << "pgrep execution failed:" << p.error(); + + if (shouldShowWarning) + { + shouldShowWarning = false; + + postToThread([] { + getApp()->getTwitch()->addGlobalSystemMessage( + "Streamer Mode is set to Automatic, but pgrep is " + "missing. " + "Install it to fix the issue or set Streamer Mode to " + "Enabled or Disabled in the Settings."); + }); + } + } + break; + } + + if (!p.waitForFinished(1000)) + { + qCWarning(chatterinoStreamerMode) << "Force-killing pgrep"; + p.kill(); + } + + return false; +#elif defined(Q_OS_WIN) + if (!IsWindowsVistaOrGreater()) + { + return false; + } + + WTS_PROCESS_INFO *pProcessInfo = nullptr; + DWORD dwProcCount = 0; + + if (WTSEnumerateProcesses(WTS_CURRENT_SERVER_HANDLE, 0, 1, &pProcessInfo, + &dwProcCount)) + { + //Go through all processes retrieved + for (DWORD i = 0; i < dwProcCount; i++) + { + QStringView processName(pProcessInfo[i].pProcessName); + + if (broadcastingBinaries().contains(processName)) + { + WTSFreeMemory(pProcessInfo); + return true; + } + } + } + + if (pProcessInfo) + { + WTSFreeMemory(pProcessInfo); + } + +#else +# warning Unsupported OS: Broadcasting software can't be detected +#endif + return false; +} + +} // namespace + +namespace chatterino { + +using namespace std::chrono_literals; + +class StreamerModePrivate +{ +public: + StreamerModePrivate(StreamerMode *parent_); + ~StreamerModePrivate(); + StreamerModePrivate(const StreamerModePrivate &) = delete; + StreamerModePrivate(StreamerModePrivate &&) = delete; + StreamerModePrivate &operator=(const StreamerModePrivate &) = delete; + StreamerModePrivate &operator=(StreamerModePrivate &&) = delete; + + [[nodiscard]] bool isEnabled() const; + + void start(); + +private: + void settingChanged(StreamerModeSetting value); + void setEnabled(bool enabled); + + void check(); + + StreamerMode *parent_; + pajlada::Signals::SignalHolder settingConnections_; + + QThread thread_; + QTimer *timer_; + + std::atomic enabled_ = false; + mutable std::atomic timeouts_ = 0; + StreamerModeSetting currentSetting_ = StreamerModeSetting::Disabled; +}; + +StreamerMode::StreamerMode() + : private_(new StreamerModePrivate(this)) +{ +} + +StreamerMode::~StreamerMode() = default; + +void StreamerMode::updated(bool enabled) +{ + this->changed(enabled); +} + +bool StreamerMode::isEnabled() const +{ + return this->private_->isEnabled(); +} + +void StreamerMode::start() +{ + this->private_->start(); +} + +StreamerModePrivate::StreamerModePrivate(StreamerMode *parent) + : parent_(parent) + , timer_(new QTimer(&this->thread_)) +{ + this->thread_.setObjectName("StreamerMode"); + this->timer_->moveToThread(&this->thread_); + QObject::connect(this->timer_, &QTimer::timeout, [this] { + auto timeouts = + this->timeouts_.fetch_add(1, std::memory_order::relaxed); + if (timeouts < SKIPPED_TIMEOUTS) + { + return; + } + this->timeouts_.store(0, std::memory_order::relaxed); + this->check(); + }); + + getSettings()->enableStreamerMode.connect( + [this](auto value) { + QMetaObject::invokeMethod(this->thread_.eventDispatcher(), [this, + value] { + this->settingChanged(static_cast(value)); + }); + }, + this->settingConnections_); + + QObject::connect(&this->thread_, &QThread::started, [this] { + this->settingChanged(getSettings()->enableStreamerMode.getEnum()); + }); +} + +void StreamerModePrivate::start() +{ + this->thread_.start(); +} + +StreamerModePrivate::~StreamerModePrivate() +{ + this->timer_->deleteLater(); + this->timer_ = nullptr; + this->thread_.quit(); + if (!this->thread_.wait(500)) + { + qCWarning(chatterinoStreamerMode) + << "Failed waiting for thread, terminating it"; + this->thread_.terminate(); + } +} + +bool StreamerModePrivate::isEnabled() const +{ + this->timeouts_.store(SKIPPED_TIMEOUTS, std::memory_order::relaxed); + return this->enabled_.load(std::memory_order::relaxed); +} + +void StreamerModePrivate::setEnabled(bool enabled) +{ + if (enabled == this->enabled_.load(std::memory_order::relaxed)) + { + return; + } + + this->enabled_.store(enabled, std::memory_order::relaxed); + this->parent_->updated(enabled); +} + +void StreamerModePrivate::settingChanged(StreamerModeSetting value) +{ + if (value == this->currentSetting_) + { + return; + } + this->currentSetting_ = value; + + switch (this->currentSetting_) + { + case StreamerModeSetting::Disabled: { + this->setEnabled(false); + this->timer_->stop(); + } + break; + case StreamerModeSetting::Enabled: { + this->setEnabled(true); + this->timer_->stop(); + } + break; + case StreamerModeSetting::DetectStreamingSoftware: { + if (!this->timer_->isActive()) + { + this->timer_->start(20s); + this->check(); + } + } + break; + default: + assert(false && "Unexpected setting"); + break; + } +} + +void StreamerModePrivate::check() +{ + this->setEnabled(isBroadcasterSoftwareActive()); +} + +} // namespace chatterino diff --git a/src/singletons/StreamerMode.hpp b/src/singletons/StreamerMode.hpp new file mode 100644 index 000000000..58ce43be6 --- /dev/null +++ b/src/singletons/StreamerMode.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include + +#include + +namespace chatterino { + +class IStreamerMode : public QObject +{ + Q_OBJECT + +public: + IStreamerMode() = default; + ~IStreamerMode() override = default; + IStreamerMode(const IStreamerMode &) = delete; + IStreamerMode(IStreamerMode &&) = delete; + IStreamerMode &operator=(const IStreamerMode &) = delete; + IStreamerMode &operator=(IStreamerMode &&) = delete; + + [[nodiscard]] virtual bool isEnabled() const = 0; + + virtual void start() = 0; + +signals: + void changed(bool enabled); +}; + +class StreamerModePrivate; +class StreamerMode : public IStreamerMode +{ +public: + StreamerMode(); + ~StreamerMode() override; + StreamerMode(const StreamerMode &) = delete; + StreamerMode(StreamerMode &&) = delete; + StreamerMode &operator=(const StreamerMode &) = delete; + StreamerMode &operator=(StreamerMode &&) = delete; + + bool isEnabled() const override; + + void start() override; + +private: + void updated(bool enabled); + + std::unique_ptr private_; + + friend class StreamerModePrivate; +}; + +} // namespace chatterino diff --git a/src/singletons/Theme.cpp b/src/singletons/Theme.cpp index bc506f9a6..3680e9d94 100644 --- a/src/singletons/Theme.cpp +++ b/src/singletons/Theme.cpp @@ -1,104 +1,565 @@ - #include "singletons/Theme.hpp" #include "Application.hpp" +#include "common/Literals.hpp" +#include "common/QLogging.hpp" +#include "singletons/Paths.hpp" #include "singletons/Resources.hpp" +#include "singletons/WindowManager.hpp" #include +#include +#include +#include +#include +#include +#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) +# include +#endif +#include #include -#define LOOKUP_COLOR_COUNT 360 +namespace { + +using namespace chatterino; +using namespace literals; + +void parseInto(const QJsonObject &obj, const QJsonObject &fallbackObj, + QLatin1String key, QColor &color) +{ + auto parseColorFrom = [](const auto &obj, + QLatin1String key) -> std::optional { + auto jsonValue = obj[key]; + if (!jsonValue.isString()) [[unlikely]] + { + return std::nullopt; + } + QColor parsed = {jsonValue.toString()}; + if (!parsed.isValid()) [[unlikely]] + { + qCWarning(chatterinoTheme).nospace() + << "While parsing " << key << ": '" << jsonValue.toString() + << "' isn't a valid color."; + return std::nullopt; + } + return parsed; + }; + + auto firstColor = parseColorFrom(obj, key); + if (firstColor.has_value()) + { + color = firstColor.value(); + return; + } + + if (!fallbackObj.isEmpty()) + { + auto fallbackColor = parseColorFrom(fallbackObj, key); + if (fallbackColor.has_value()) + { + color = fallbackColor.value(); + return; + } + } + + qCWarning(chatterinoTheme) << key + << "was expected but not found in the " + "current theme, and no fallback value found."; +} + +// NOLINTBEGIN(cppcoreguidelines-macro-usage) +#define _c2StringLit(s, ty) s##ty +#define parseColor(to, from, key) \ + parseInto(from, from##Fallback, _c2StringLit(#key, _L1), (to).from.key) +// NOLINTEND(cppcoreguidelines-macro-usage) + +void parseWindow(const QJsonObject &window, const QJsonObject &windowFallback, + chatterino::Theme &theme) +{ + parseColor(theme, window, background); + parseColor(theme, window, text); +} + +void parseTabs(const QJsonObject &tabs, const QJsonObject &tabsFallback, + chatterino::Theme &theme) +{ + const auto parseTabColors = [](const auto &json, const auto &jsonFallback, + auto &tab) { + parseInto(json, jsonFallback, "text"_L1, tab.text); + { + const auto backgrounds = json["backgrounds"_L1].toObject(); + const auto backgroundsFallback = + jsonFallback["backgrounds"_L1].toObject(); + parseColor(tab, backgrounds, regular); + parseColor(tab, backgrounds, hover); + parseColor(tab, backgrounds, unfocused); + } + { + const auto line = json["line"_L1].toObject(); + const auto lineFallback = jsonFallback["line"_L1].toObject(); + parseColor(tab, line, regular); + parseColor(tab, line, hover); + parseColor(tab, line, unfocused); + } + }; + parseColor(theme, tabs, dividerLine); + parseColor(theme, tabs, liveIndicator); + parseColor(theme, tabs, rerunIndicator); + parseTabColors(tabs["regular"_L1].toObject(), + tabsFallback["regular"_L1].toObject(), theme.tabs.regular); + parseTabColors(tabs["newMessage"_L1].toObject(), + tabsFallback["newMessage"_L1].toObject(), + theme.tabs.newMessage); + parseTabColors(tabs["highlighted"_L1].toObject(), + tabsFallback["highlighted"_L1].toObject(), + theme.tabs.highlighted); + parseTabColors(tabs["selected"_L1].toObject(), + tabsFallback["selected"_L1].toObject(), theme.tabs.selected); +} + +void parseMessages(const QJsonObject &messages, + const QJsonObject &messagesFallback, + chatterino::Theme &theme) +{ + { + const auto textColors = messages["textColors"_L1].toObject(); + const auto textColorsFallback = + messagesFallback["textColors"_L1].toObject(); + parseColor(theme.messages, textColors, regular); + parseColor(theme.messages, textColors, caret); + parseColor(theme.messages, textColors, link); + parseColor(theme.messages, textColors, system); + parseColor(theme.messages, textColors, chatPlaceholder); + } + { + const auto backgrounds = messages["backgrounds"_L1].toObject(); + const auto backgroundsFallback = + messagesFallback["backgrounds"_L1].toObject(); + parseColor(theme.messages, backgrounds, regular); + parseColor(theme.messages, backgrounds, alternate); + } + parseColor(theme, messages, disabled); + parseColor(theme, messages, selection); + parseColor(theme, messages, highlightAnimationStart); + parseColor(theme, messages, highlightAnimationEnd); +} + +void parseScrollbars(const QJsonObject &scrollbars, + const QJsonObject &scrollbarsFallback, + chatterino::Theme &theme) +{ + parseColor(theme, scrollbars, background); + parseColor(theme, scrollbars, thumb); + parseColor(theme, scrollbars, thumbSelected); +} + +void parseSplits(const QJsonObject &splits, const QJsonObject &splitsFallback, + chatterino::Theme &theme) +{ + parseColor(theme, splits, messageSeperator); + parseColor(theme, splits, background); + parseColor(theme, splits, dropPreview); + parseColor(theme, splits, dropPreviewBorder); + parseColor(theme, splits, dropTargetRect); + parseColor(theme, splits, dropTargetRectBorder); + parseColor(theme, splits, resizeHandle); + parseColor(theme, splits, resizeHandleBackground); + + { + const auto header = splits["header"_L1].toObject(); + const auto headerFallback = splitsFallback["header"_L1].toObject(); + parseColor(theme.splits, header, border); + parseColor(theme.splits, header, focusedBorder); + parseColor(theme.splits, header, background); + parseColor(theme.splits, header, focusedBackground); + parseColor(theme.splits, header, text); + parseColor(theme.splits, header, focusedText); + } + { + const auto input = splits["input"_L1].toObject(); + const auto inputFallback = splitsFallback["input"_L1].toObject(); + parseColor(theme.splits, input, background); + parseColor(theme.splits, input, text); + } +} + +void parseColors(const QJsonObject &root, const QJsonObject &fallbackTheme, + chatterino::Theme &theme) +{ + const auto colors = root["colors"_L1].toObject(); + const auto fallbackColors = fallbackTheme["colors"_L1].toObject(); + + parseInto(colors, fallbackColors, "accent"_L1, theme.accent); + + parseWindow(colors["window"_L1].toObject(), + fallbackColors["window"_L1].toObject(), theme); + parseTabs(colors["tabs"_L1].toObject(), + fallbackColors["tabs"_L1].toObject(), theme); + parseMessages(colors["messages"_L1].toObject(), + fallbackColors["messages"_L1].toObject(), theme); + parseScrollbars(colors["scrollbars"_L1].toObject(), + fallbackColors["scrollbars"_L1].toObject(), theme); + parseSplits(colors["splits"_L1].toObject(), + fallbackColors["splits"_L1].toObject(), theme); +} +#undef parseColor +#undef _c2StringLit + +std::optional loadThemeFromPath(const QString &path) +{ + QFile file(path); + if (!file.open(QFile::ReadOnly)) + { + qCWarning(chatterinoTheme) + << "Failed to open" << file.fileName() << "at" << path; + return std::nullopt; + } + + QJsonParseError error{}; + auto json = QJsonDocument::fromJson(file.readAll(), &error); + if (!json.isObject()) + { + qCWarning(chatterinoTheme) << "Failed to parse" << file.fileName() + << "error:" << error.errorString(); + return std::nullopt; + } + + // TODO: Validate JSON schema? + + return json.object(); +} + +/** + * Load the given theme descriptor from its path + * + * Returns a JSON object containing theme data if the theme is valid, otherwise nullopt + * + * NOTE: No theme validation is done by this function + **/ +std::optional loadTheme(const ThemeDescriptor &theme) +{ + return loadThemeFromPath(theme.path); +} + +} // namespace namespace chatterino { -Theme::Theme() -{ - this->update(); +const std::vector Theme::builtInThemes{ + { + .key = "White", + .path = ":/themes/White.json", + .name = "White", + }, + { + .key = "Light", + .path = ":/themes/Light.json", + .name = "Light", + }, + { + .key = "Dark", + .path = ":/themes/Dark.json", + .name = "Dark", + }, + { + .key = "Black", + .path = ":/themes/Black.json", + .name = "Black", + }, +}; - this->themeName.connectSimple( - [this](auto) { - this->update(); - }, - false); - this->themeHue.connectSimple( - [this](auto) { - this->update(); - }, - false); +// Dark is our default & fallback theme +const ThemeDescriptor Theme::fallbackTheme = Theme::builtInThemes.at(2); + +bool Theme::isLightTheme() const +{ + return this->isLight_; } -// hue: theme color (0 - 1) -// multiplier: 1 = white, 0.8 = light, -0.8 dark, -1 black -void Theme::actuallyUpdate(double hue, double multiplier) +bool Theme::isSystemTheme() const { - BaseTheme::actuallyUpdate(hue, multiplier); + return this->themeName == u"System"_s; +} - auto getColor = [multiplier](double h, double s, double l, double a = 1.0) { - return QColor::fromHslF(h, s, ((l - 0.5) * multiplier) + 0.5, a); +Theme::Theme(const Paths &paths) +{ + this->themeName.connect( + [this](auto themeName) { + qCInfo(chatterinoTheme) << "Theme updated to" << themeName; + this->update(); + }, + false); + auto updateIfSystem = [this](const auto &) { + if (this->isSystemTheme()) + { + this->update(); + } + }; + this->darkSystemThemeName.connect(updateIfSystem, false); + this->lightSystemThemeName.connect(updateIfSystem, false); + + this->loadAvailableThemes(paths); + +#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) + QObject::connect(QApplication::styleHints(), + &QStyleHints::colorSchemeChanged, &this->lifetime_, + [this] { + if (this->isSystemTheme()) + { + this->update(); + getApp()->getWindows()->forceLayoutChannelViews(); + } + }); +#endif + + this->update(); +} + +void Theme::update() +{ + auto currentTheme = [&]() -> QString { +#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) + if (this->isSystemTheme()) + { + switch (QApplication::styleHints()->colorScheme()) + { + case Qt::ColorScheme::Light: + return this->lightSystemThemeName; + case Qt::ColorScheme::Unknown: + case Qt::ColorScheme::Dark: + return this->darkSystemThemeName; + } + } +#endif + return this->themeName; }; - const auto sat = qreal(0); - const auto isLight = this->isLightTheme(); - const auto flat = isLight; + auto oTheme = this->findThemeByKey(currentTheme()); - if (this->isLightTheme()) + constexpr const double nsToMs = 1.0 / 1000000.0; + QElapsedTimer timer; + timer.start(); + + std::optional themeJSON; + QString themePath; + bool isCustomTheme = false; + if (!oTheme) { - this->splits.dropTargetRect = QColor(255, 255, 255, 0x00); - this->splits.dropTargetRectBorder = QColor(0, 148, 255, 0x00); + qCWarning(chatterinoTheme) + << "Theme" << this->themeName + << "not found, falling back to the fallback theme"; - this->splits.resizeHandle = QColor(0, 148, 255, 0xff); - this->splits.resizeHandleBackground = QColor(0, 148, 255, 0x50); + themeJSON = loadTheme(fallbackTheme); + themePath = fallbackTheme.path; } else { - this->splits.dropTargetRect = QColor(0, 148, 255, 0x00); - this->splits.dropTargetRectBorder = QColor(0, 148, 255, 0x00); + const auto &theme = *oTheme; - this->splits.resizeHandle = QColor(0, 148, 255, 0x70); - this->splits.resizeHandleBackground = QColor(0, 148, 255, 0x20); + themeJSON = loadTheme(theme); + themePath = theme.path; + + if (!themeJSON) + { + qCWarning(chatterinoTheme) + << "Theme" << this->themeName + << "not valid, falling back to the fallback theme"; + + // Parsing the theme failed, fall back + themeJSON = loadTheme(fallbackTheme); + themePath = fallbackTheme.path; + } + else + { + isCustomTheme = theme.custom; + } + } + auto loadTs = double(timer.nsecsElapsed()) * nsToMs; + + if (!themeJSON) + { + qCWarning(chatterinoTheme) + << "Failed to load" << this->themeName << "or the fallback theme"; + return; } - this->splits.header.background = getColor(0, sat, flat ? 1 : 0.9); - this->splits.header.border = getColor(0, sat, flat ? 1 : 0.85); - this->splits.header.text = this->messages.textColors.regular; - this->splits.header.focusedBackground = - getColor(0, sat, isLight ? 0.95 : 0.79); - this->splits.header.focusedBorder = getColor(0, sat, isLight ? 0.90 : 0.78); - this->splits.header.focusedText = QColor::fromHsvF( - 0.58388, isLight ? 1.0 : 0.482, isLight ? 0.6375 : 1.0); + if (this->isAutoReloading() && this->currentThemeJson_ == *themeJSON) + { + return; + } - this->splits.input.background = getColor(0, sat, flat ? 0.95 : 0.95); - this->splits.input.border = getColor(0, sat, flat ? 1 : 1); - this->splits.input.text = this->messages.textColors.regular; - this->splits.input.styleSheet = - "background:" + this->splits.input.background.name() + ";" + - "border:" + this->tabs.selected.backgrounds.regular.color().name() + - ";" + "color:" + this->messages.textColors.regular.name() + ";" + - "selection-background-color:" + - (isLight ? "#68B1FF" - : this->tabs.selected.backgrounds.regular.color().name()); + this->parseFrom(*themeJSON, isCustomTheme); + this->currentThemePath_ = themePath; - this->splits.input.focusedLine = this->tabs.highlighted.line.regular; + auto parseTs = double(timer.nsecsElapsed()) * nsToMs; - this->splits.messageSeperator = - isLight ? QColor(127, 127, 127) : QColor(60, 60, 60); - this->splits.background = getColor(0, sat, 1); - this->splits.dropPreview = QColor(0, 148, 255, 0x30); - this->splits.dropPreviewBorder = QColor(0, 148, 255, 0xff); + this->updated.invoke(); + auto updateTs = double(timer.nsecsElapsed()) * nsToMs; + qCDebug(chatterinoTheme).nospace().noquote() + << "Updated theme in " << QString::number(updateTs, 'f', 2) + << "ms (load: " << QString::number(loadTs, 'f', 2) + << "ms, parse: " << QString::number(parseTs - loadTs, 'f', 2) + << "ms, update: " << QString::number(updateTs - parseTs, 'f', 2) + << "ms)"; - // Copy button + if (this->isAutoReloading()) + { + this->currentThemeJson_ = *themeJSON; + } +} + +std::vector> Theme::availableThemes() const +{ + std::vector> packagedThemes; + + for (const auto &theme : this->availableThemes_) + { + if (theme.custom) + { + auto p = std::make_pair( + QStringLiteral("Custom: %1").arg(theme.name), theme.key); + + packagedThemes.emplace_back(p); + } + else + { + auto p = std::make_pair(theme.name, theme.key); + + packagedThemes.emplace_back(p); + } + } + + return packagedThemes; +} + +void Theme::loadAvailableThemes(const Paths &paths) +{ + this->availableThemes_ = Theme::builtInThemes; + + auto dir = QDir(paths.themesDirectory); + for (const auto &info : + dir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot, QDir::Name)) + { + if (!info.isFile()) + { + continue; + } + + if (!info.fileName().endsWith(".json")) + { + continue; + } + + auto themeName = info.baseName(); + + auto themeDescriptor = ThemeDescriptor{ + info.fileName(), info.absoluteFilePath(), themeName, true}; + + auto theme = loadTheme(themeDescriptor); + if (!theme) + { + qCWarning(chatterinoTheme) << "Failed to parse theme at" << info; + continue; + } + + this->availableThemes_.emplace_back(std::move(themeDescriptor)); + } +} + +std::optional Theme::findThemeByKey(const QString &key) +{ + for (const auto &theme : this->availableThemes_) + { + if (theme.key == key) + { + return theme; + } + } + + return std::nullopt; +} + +void Theme::parseFrom(const QJsonObject &root, bool isCustomTheme) +{ + this->isLight_ = + root["metadata"_L1]["iconTheme"_L1].toString() == u"dark"_s; + + std::optional fallbackTheme; + if (isCustomTheme) + { + // Only attempt to load a fallback theme if the theme we're loading is a custom theme + auto fallbackThemeName = + root["metadata"_L1]["fallbackTheme"_L1].toString( + this->isLightTheme() ? "Light" : "Dark"); + for (const auto &theme : Theme::builtInThemes) + { + if (fallbackThemeName.compare(theme.key, Qt::CaseInsensitive) == 0) + { + fallbackTheme = loadTheme(theme); + break; + } + } + } + + parseColors(root, fallbackTheme.value_or(QJsonObject()), *this); + + this->splits.input.styleSheet = uR"( + background: %1; + border: %2; + color: %3; + selection-background-color: %4; + )"_s.arg( + this->splits.input.background.name(QColor::HexArgb), + this->tabs.selected.backgrounds.regular.name(QColor::HexArgb), + this->messages.textColors.regular.name(QColor::HexArgb), + this->isLightTheme() + ? u"#68B1FF"_s + : this->tabs.selected.backgrounds.regular.name(QColor::HexArgb)); + + // Usercard buttons if (this->isLightTheme()) { this->buttons.copy = getResources().buttons.copyDark; + this->buttons.pin = getResources().buttons.pinDisabledDark; } else { this->buttons.copy = getResources().buttons.copyLight; + this->buttons.pin = getResources().buttons.pinDisabledLight; } } -void Theme::normalizeColor(QColor &color) +bool Theme::isAutoReloading() const +{ + return this->themeReloadTimer_ != nullptr; +} + +void Theme::setAutoReload(bool autoReload) +{ + if (autoReload == this->isAutoReloading()) + { + return; + } + + if (!autoReload) + { + this->themeReloadTimer_.reset(); + this->currentThemeJson_ = {}; + return; + } + + this->themeReloadTimer_ = std::make_unique(); + QObject::connect(this->themeReloadTimer_.get(), &QTimer::timeout, [this]() { + this->update(); + }); + this->themeReloadTimer_->setInterval(Theme::AUTO_RELOAD_INTERVAL_MS); + this->themeReloadTimer_->start(); + + qCDebug(chatterinoTheme) << "Enabled theme watcher"; +} + +void Theme::normalizeColor(QColor &color) const { if (this->isLightTheme()) { @@ -137,7 +598,7 @@ void Theme::normalizeColor(QColor &color) Theme *getTheme() { - return getApp()->themes; + return getApp()->getThemes(); } } // namespace chatterino diff --git a/src/singletons/Theme.hpp b/src/singletons/Theme.hpp index 9af05f543..cb811e2db 100644 --- a/src/singletons/Theme.hpp +++ b/src/singletons/Theme.hpp @@ -1,22 +1,115 @@ #pragma once -#include "BaseTheme.hpp" -#include "common/Singleton.hpp" +#include "common/ChatterinoSetting.hpp" +#include "singletons/Paths.hpp" #include "util/RapidJsonSerializeQString.hpp" -#include -#include #include -#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include namespace chatterino { class WindowManager; -class Theme final : public Singleton, public BaseTheme +struct ThemeDescriptor { + QString key; + + // Path to the theme on disk + // Can be a Qt resource path + QString path; + + // Name of the theme + QString name; + + bool custom{}; +}; + +class Theme final { public: - Theme(); + static const std::vector builtInThemes; + + // The built in theme that will be used if some theme parsing fails + static const ThemeDescriptor fallbackTheme; + + static const int AUTO_RELOAD_INTERVAL_MS = 500; + + Theme(const Paths &paths); + + bool isLightTheme() const; + bool isSystemTheme() const; + + struct TabColors { + QColor text; + struct { + QColor regular; + QColor hover; + QColor unfocused; + } backgrounds; + struct { + QColor regular; + QColor hover; + QColor unfocused; + } line; + }; + + QColor accent{"#00aeef"}; + + /// WINDOW + struct { + QColor background; + QColor text; + } window; + + /// TABS + struct { + TabColors regular; + TabColors newMessage; + TabColors highlighted; + TabColors selected; + QColor dividerLine; + + QColor liveIndicator; + QColor rerunIndicator; + } tabs; + + /// MESSAGES + struct { + struct { + QColor regular; + QColor caret; + QColor link; + QColor system; + QColor chatPlaceholder; + } textColors; + + struct { + QColor regular; + QColor alternate; + } backgrounds; + + QColor disabled; + QColor selection; + + QColor highlightAnimationStart; + QColor highlightAnimationEnd; + } messages; + + /// SCROLLBAR + struct { + QColor background; + QColor thumb; + QColor thumbSelected; + } scrollbars; /// SPLITS struct { @@ -36,32 +129,65 @@ public: QColor focusedBackground; QColor text; QColor focusedText; - // int margin; } header; struct { - QColor border; QColor background; - QColor selection; - QColor focusedLine; QColor text; QString styleSheet; - // int margin; } input; } splits; struct { QPixmap copy; + QPixmap pin; } buttons; - void normalizeColor(QColor &color); + void normalizeColor(QColor &color) const; + void update(); + + bool isAutoReloading() const; + void setAutoReload(bool autoReload); + + /** + * Return a list of available themes + **/ + std::vector> availableThemes() const; + + pajlada::Signals::NoArgSignal updated; + + QStringSetting themeName{"/appearance/theme/name", "Dark"}; + QStringSetting lightSystemThemeName{"/appearance/theme/lightSystem", + "Light"}; + QStringSetting darkSystemThemeName{"/appearance/theme/darkSystem", "Dark"}; private: - void actuallyUpdate(double hue, double multiplier) override; + bool isLight_ = false; + + std::vector availableThemes_; + + QString currentThemePath_; + std::unique_ptr themeReloadTimer_; + // This will only be populated when auto-reloading themes + QJsonObject currentThemeJson_; + + QObject lifetime_; + + /** + * Figure out which themes are available in the Themes directory + * + * NOTE: This is currently not built to be reloadable + **/ + void loadAvailableThemes(const Paths &paths); + + std::optional findThemeByKey(const QString &key); + + void parseFrom(const QJsonObject &root, bool isCustomTheme); pajlada::Signals::NoArgSignal repaintVisibleChatWidgets_; friend class WindowManager; }; +Theme *getTheme(); } // namespace chatterino diff --git a/src/singletons/Toasts.cpp b/src/singletons/Toasts.cpp index 1cf99bea2..7e21dc2c3 100644 --- a/src/singletons/Toasts.cpp +++ b/src/singletons/Toasts.cpp @@ -1,45 +1,80 @@ -#include "Toasts.hpp" +#include "singletons/Toasts.hpp" #include "Application.hpp" -#include "common/DownloadManager.hpp" -#include "common/NetworkRequest.hpp" +#include "common/Common.hpp" +#include "common/Literals.hpp" +#include "common/QLogging.hpp" +#include "common/Version.hpp" #include "controllers/notifications/NotificationController.hpp" -#include "providers/twitch/TwitchChannel.hpp" -#include "providers/twitch/TwitchCommon.hpp" -#include "providers/twitch/TwitchIrcServer.hpp" #include "providers/twitch/api/Helix.hpp" +#include "providers/twitch/TwitchIrcServer.hpp" #include "singletons/Paths.hpp" +#include "singletons/Settings.hpp" +#include "singletons/StreamerMode.hpp" #include "util/StreamLink.hpp" #include "widgets/helper/CommonTexts.hpp" #ifdef Q_OS_WIN - # include - #endif #include #include #include #include +#include #include -#include +#include + +namespace { + +using namespace chatterino; +using namespace literals; + +QString avatarFilePath(const QString &channelName) +{ + // TODO: cleanup channel (to be used as a file) and use combinePath + return getApp()->getPaths().twitchProfileAvatars % '/' % channelName % + u".png"; +} + +bool hasAvatarForChannel(const QString &channelName) +{ + QFileInfo avatarFile(avatarFilePath(channelName)); + return avatarFile.exists() && avatarFile.isFile(); +} + +/// A job that downlaods a twitch avatar and saves it to a file +class AvatarDownloader : public QObject +{ + Q_OBJECT +public: + AvatarDownloader(const QString &avatarURL, const QString &channelName); + +private: + QNetworkAccessManager manager_; + QFile file_; + QNetworkReply *reply_{}; + +signals: + void downloadComplete(); +}; + +} // namespace namespace chatterino { -std::map Toasts::reactionToString = { - {ToastReaction::OpenInBrowser, OPEN_IN_BROWSER}, - {ToastReaction::OpenInPlayer, OPEN_PLAYER_IN_BROWSER}, - {ToastReaction::OpenInStreamlink, OPEN_IN_STREAMLINK}, - {ToastReaction::DontOpen, DONT_OPEN}}; +#ifdef Q_OS_WIN +using WinToastLib::WinToast; +using WinToastLib::WinToastTemplate; +#endif bool Toasts::isEnabled() { #ifdef Q_OS_WIN - return WinToastLib::WinToast::isCompatible() && - getSettings()->notificationToast && - !(isInStreamerMode() && + return WinToast::isCompatible() && getSettings()->notificationToast && + !(getApp()->getStreamerMode()->isEnabled() && getSettings()->streamerModeSuppressLiveNotifications); #else return false; @@ -48,41 +83,46 @@ bool Toasts::isEnabled() QString Toasts::findStringFromReaction(const ToastReaction &reaction) { - auto iterator = Toasts::reactionToString.find(reaction); - if (iterator != Toasts::reactionToString.end()) + switch (reaction) { - return iterator->second; - } - else - { - return DONT_OPEN; + case ToastReaction::OpenInBrowser: + return OPEN_IN_BROWSER; + case ToastReaction::OpenInPlayer: + return OPEN_PLAYER_IN_BROWSER; + case ToastReaction::OpenInStreamlink: + return OPEN_IN_STREAMLINK; + case ToastReaction::DontOpen: + default: + return DONT_OPEN; } } QString Toasts::findStringFromReaction( - const pajlada::Settings::Setting &value) + const pajlada::Settings::Setting &reaction) { - int i = static_cast(value); - return Toasts::findStringFromReaction(static_cast(i)); + static_assert(std::is_same_v, int>); + int value = reaction; + return Toasts::findStringFromReaction(static_cast(value)); } -void Toasts::sendChannelNotification(const QString &channelName, Platform p) +// NOLINTNEXTLINE(readability-convert-member-functions-to-static) +void Toasts::sendChannelNotification(const QString &channelName, + const QString &channelTitle, Platform p) { #ifdef Q_OS_WIN - auto sendChannelNotification = [this, channelName, p] { - this->sendWindowsNotification(channelName, p); + auto sendChannelNotification = [this, channelName, channelTitle, p] { + this->sendWindowsNotification(channelName, channelTitle, p); }; #else + (void)channelTitle; auto sendChannelNotification = [] { - // Unimplemented for OSX and Linux + // Unimplemented for macOS and Linux }; #endif // Fetch user profile avatar if (p == Platform::Twitch) { - QFileInfo check_file(getPaths()->twitchProfileAvatars + "/twitch/" + - channelName + ".png"); - if (check_file.exists() && check_file.isFile()) + if (hasAvatarForChannel(channelName)) { sendChannelNotification(); } @@ -91,10 +131,11 @@ void Toasts::sendChannelNotification(const QString &channelName, Platform p) getHelix()->getUserByName( channelName, [channelName, sendChannelNotification](const auto &user) { - DownloadManager *manager = new DownloadManager(); - manager->setFile(user.profileImageUrl, channelName); - manager->connect(manager, - &DownloadManager::downloadComplete, + // gets deleted when finished + auto *downloader = + new AvatarDownloader(user.profileImageUrl, channelName); + QObject::connect(downloader, + &AvatarDownloader::downloadComplete, sendChannelNotification); }, [] { @@ -114,13 +155,12 @@ private: public: CustomHandler(QString channelName, Platform p) - : channelName_(channelName) + : channelName_(std::move(channelName)) , platform_(p) { } - void toastActivated() const + void toastActivated() const override { - QString link; auto toastReaction = static_cast(getSettings()->openFromToast.getValue()); @@ -129,50 +169,73 @@ public: case ToastReaction::OpenInBrowser: if (platform_ == Platform::Twitch) { - link = "http://www.twitch.tv/" + channelName_; + QDesktopServices::openUrl( + QUrl(u"https://www.twitch.tv/" % channelName_)); } - QDesktopServices::openUrl(QUrl(link)); break; case ToastReaction::OpenInPlayer: if (platform_ == Platform::Twitch) { - link = - "https://player.twitch.tv/?parent=twitch.tv&channel=" + - channelName_; + QDesktopServices::openUrl( + QUrl(TWITCH_PLAYER_URL.arg(channelName_))); } - QDesktopServices::openUrl(QUrl(link)); break; case ToastReaction::OpenInStreamlink: { openStreamlinkForChannel(channelName_); break; } - // the fourth and last option is "don't open" - // in this case obviously nothing should happen + case ToastReaction::DontOpen: + // nothing should happen + break; } } - void toastActivated(int actionIndex) const + void toastActivated(int actionIndex) const override { } - void toastFailed() const + void toastFailed() const override { } - void toastDismissed(WinToastDismissalReason state) const + void toastDismissed(WinToastDismissalReason state) const override { } }; -void Toasts::sendWindowsNotification(const QString &channelName, Platform p) +void Toasts::ensureInitialized() { - WinToastLib::WinToastTemplate templ = WinToastLib::WinToastTemplate( - WinToastLib::WinToastTemplate::ImageAndText03); - QString str = channelName + " is live!"; - std::string utf8_text = str.toUtf8().constData(); - std::wstring widestr = std::wstring(utf8_text.begin(), utf8_text.end()); + if (this->initialized_) + { + return; + } + this->initialized_ = true; - templ.setTextField(widestr, WinToastLib::WinToastTemplate::FirstLine); + auto *instance = WinToast::instance(); + instance->setAppName(L"Chatterino2"); + instance->setAppUserModelId( + WinToast::configureAUMI(L"", L"Chatterino 2", L"", + Version::instance().version().toStdWString())); + instance->setShortcutPolicy(WinToast::SHORTCUT_POLICY_IGNORE); + WinToast::WinToastError error{}; + instance->initialize(&error); + + if (error != WinToast::NoError) + { + qCDebug(chatterinoNotification) + << "Failed to initialize WinToast - error:" << error; + } +} + +void Toasts::sendWindowsNotification(const QString &channelName, + const QString &channelTitle, Platform p) +{ + this->ensureInitialized(); + + WinToastTemplate templ(WinToastTemplate::ImageAndText03); + QString str = channelName % u" is live!"; + + templ.setTextField(str.toStdWString(), WinToastTemplate::FirstLine); if (static_cast(getSettings()->openFromToast.getValue()) != ToastReaction::DontOpen) { @@ -180,40 +243,68 @@ void Toasts::sendWindowsNotification(const QString &channelName, Platform p) Toasts::findStringFromReaction(getSettings()->openFromToast); mode = mode.toLower(); - templ.setTextField(L"Click here to " + mode.toStdWString(), - WinToastLib::WinToastTemplate::SecondLine); + templ.setTextField( + u"%1 \nClick to %2"_s.arg(channelTitle).arg(mode).toStdWString(), + WinToastTemplate::SecondLine); } - QString Path; + QString avatarPath; if (p == Platform::Twitch) { - Path = getPaths()->twitchProfileAvatars + "/twitch/" + channelName + - ".png"; + avatarPath = avatarFilePath(channelName); } - std::string temp_Utf8 = Path.toUtf8().constData(); - std::wstring imagePath = std::wstring(temp_Utf8.begin(), temp_Utf8.end()); - templ.setImagePath(imagePath); + templ.setImagePath(avatarPath.toStdWString()); if (getSettings()->notificationPlaySound) { - templ.setAudioOption( - WinToastLib::WinToastTemplate::AudioOption::Silent); + templ.setAudioOption(WinToastTemplate::AudioOption::Silent); + } + + WinToast::WinToastError error = WinToast::NoError; + WinToast::instance()->showToast(templ, new CustomHandler(channelName, p), + &error); + if (error != WinToast::NoError) + { + qCWarning(chatterinoNotification) << "Failed to show toast:" << error; } - WinToastLib::WinToast::instance()->setAppName(L"Chatterino2"); - int mbstowcs(wchar_t * aumi_version, const char *CHATTERINO_VERSION, - size_t size); - std::string(CHATTERINO_VERSION); - std::wstring aumi_version = - std::wstring(CHATTERINO_VERSION.begin(), CHATTERINO_VERSION.end()); - WinToastLib::WinToast::instance()->setAppUserModelId( - WinToastLib::WinToast::configureAUMI(L"", L"Chatterino 2", L"", - aumi_version)); - WinToastLib::WinToast::instance()->setShortcutPolicy( - WinToastLib::WinToast::SHORTCUT_POLICY_IGNORE); - WinToastLib::WinToast::instance()->initialize(); - WinToastLib::WinToast::instance()->showToast( - templ, new CustomHandler(channelName, p)); } #endif } // namespace chatterino + +namespace { + +AvatarDownloader::AvatarDownloader(const QString &avatarURL, + const QString &channelName) + : file_(avatarFilePath(channelName)) +{ + if (!this->file_.open(QFile::WriteOnly | QFile::Truncate)) + { + qCWarning(chatterinoNotification) + << "Failed to open avatar file" << this->file_.errorString(); + } + + this->reply_ = this->manager_.get(QNetworkRequest(avatarURL)); + + connect(this->reply_, &QNetworkReply::readyRead, this, [this] { + this->file_.write(this->reply_->readAll()); + }); + connect(this->reply_, &QNetworkReply::finished, this, [this] { + if (this->reply_->error() != QNetworkReply::NoError) + { + qCWarning(chatterinoNotification) + << "Failed to download avatar" << this->reply_->errorString(); + } + + if (this->file_.isOpen()) + { + this->file_.close(); + } + emit downloadComplete(); + this->deleteLater(); + }); +} + +#include "Toasts.moc" + +} // namespace diff --git a/src/singletons/Toasts.hpp b/src/singletons/Toasts.hpp index f32c0c4c4..e4c2ad234 100644 --- a/src/singletons/Toasts.hpp +++ b/src/singletons/Toasts.hpp @@ -1,9 +1,7 @@ #pragma once -#include "Application.hpp" -#include "common/Singleton.hpp" - #include +#include namespace chatterino { @@ -16,20 +14,24 @@ enum class ToastReaction { DontOpen = 3 }; -class Toasts final : public Singleton +class Toasts final { public: - void sendChannelNotification(const QString &channelName, Platform p); + void sendChannelNotification(const QString &channelName, + const QString &channelTitle, Platform p); static QString findStringFromReaction(const ToastReaction &reaction); static QString findStringFromReaction( const pajlada::Settings::Setting &reaction); - static std::map reactionToString; static bool isEnabled(); private: #ifdef Q_OS_WIN - void sendWindowsNotification(const QString &channelName, Platform p); + void ensureInitialized(); + void sendWindowsNotification(const QString &channelName, + const QString &channelTitle, Platform p); + + bool initialized_ = false; #endif }; } // namespace chatterino diff --git a/src/singletons/TooltipPreviewImage.cpp b/src/singletons/TooltipPreviewImage.cpp deleted file mode 100644 index 7b33039e8..000000000 --- a/src/singletons/TooltipPreviewImage.cpp +++ /dev/null @@ -1,81 +0,0 @@ -#include "TooltipPreviewImage.hpp" - -#include "Application.hpp" -#include "singletons/WindowManager.hpp" -#include "widgets/TooltipWidget.hpp" - -namespace chatterino { - -TooltipPreviewImage &TooltipPreviewImage::instance() -{ - static TooltipPreviewImage *instance = new TooltipPreviewImage(); - return *instance; -} - -TooltipPreviewImage::TooltipPreviewImage() -{ - auto windows = getApp()->windows; - - this->connections_.managedConnect(windows->gifRepaintRequested, [&] { - if (this->image_ && this->image_->animated()) - { - this->refreshTooltipWidgetPixmap(); - } - }); - - this->connections_.managedConnect(windows->miscUpdate, [&] { - if (this->attemptRefresh) - { - this->refreshTooltipWidgetPixmap(); - } - }); -} - -void TooltipPreviewImage::setImage(ImagePtr image) -{ - this->image_ = std::move(image); - - this->refreshTooltipWidgetPixmap(); -} - -void TooltipPreviewImage::setImageScale(int w, int h) -{ - this->imageWidth_ = w; - this->imageHeight_ = h; - - this->refreshTooltipWidgetPixmap(); -} - -void TooltipPreviewImage::refreshTooltipWidgetPixmap() -{ - auto tooltipWidget = TooltipWidget::instance(); - - if (this->image_ && !tooltipWidget->isHidden()) - { - if (auto pixmap = this->image_->pixmapOrLoad()) - { - if (this->imageWidth_ != 0 && this->imageHeight_) - { - tooltipWidget->setImage(pixmap->scaled(this->imageWidth_, - this->imageHeight_, - Qt::KeepAspectRatio)); - } - else - { - tooltipWidget->setImage(*pixmap); - } - - this->attemptRefresh = false; - } - else - { - this->attemptRefresh = true; - } - } - else - { - tooltipWidget->clearImage(); - } -} - -} // namespace chatterino diff --git a/src/singletons/TooltipPreviewImage.hpp b/src/singletons/TooltipPreviewImage.hpp deleted file mode 100644 index 309d1ef15..000000000 --- a/src/singletons/TooltipPreviewImage.hpp +++ /dev/null @@ -1,36 +0,0 @@ -#pragma once - -#include "messages/Image.hpp" - -#include - -namespace chatterino { - -class TooltipPreviewImage -{ -public: - static TooltipPreviewImage &instance(); - void setImage(ImagePtr image); - void setImageScale(int w, int h); - - TooltipPreviewImage(const TooltipPreviewImage &) = delete; - -private: - TooltipPreviewImage(); - -private: - ImagePtr image_ = nullptr; - int imageWidth_ = 0; - int imageHeight_ = 0; - - pajlada::Signals::SignalHolder connections_; - - // attemptRefresh is set to true in case we want to preview an image that has not loaded yet (if pixmapOrLoad fails) - bool attemptRefresh{false}; - - // Refresh the pixmap used in the Tooltip Widget - // Called from setImage and from the "gif repaint" signal if the image is animated - void refreshTooltipWidgetPixmap(); -}; - -} // namespace chatterino diff --git a/src/singletons/Updates.cpp b/src/singletons/Updates.cpp index cf32b30ff..0aad6c220 100644 --- a/src/singletons/Updates.cpp +++ b/src/singletons/Updates.cpp @@ -1,73 +1,105 @@ -#include "Updates.hpp" +#include "singletons/Updates.hpp" -#include "Settings.hpp" +#include "common/Literals.hpp" #include "common/Modes.hpp" -#include "common/NetworkRequest.hpp" -#include "common/Outcome.hpp" +#include "common/network/NetworkRequest.hpp" +#include "common/network/NetworkResult.hpp" +#include "common/QLogging.hpp" #include "common/Version.hpp" #include "singletons/Paths.hpp" +#include "singletons/Settings.hpp" #include "util/CombinePath.hpp" #include "util/PostToThread.hpp" +#include #include #include #include #include -#include "common/QLogging.hpp" +#include +#include +#include -namespace chatterino { namespace { - QString currentBranch() - { - return getSettings()->betaUpdates ? "beta" : "stable"; - } - /// Checks if the online version is newer or older than the current version. - bool isDowngradeOf(const QString &online, const QString ¤t) - { - static auto matchVersion = - QRegularExpression(R"((\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.(\d+))?)"); +using namespace chatterino; +using namespace literals; - // Versions are just strings, they don't need to follow a specific - // format so we can only assume if one version is newer than another - // one. +QString currentBranch() +{ + return getSettings()->betaUpdates ? "beta" : "stable"; +} - // We match x.x.x.x with each version level being optional. +#if defined(Q_OS_WIN) +const QString CHATTERINO_OS = u"win"_s; +#elif defined(Q_OS_MACOS) +const QString CHATTERINO_OS = u"macos"_s; +#elif defined(Q_OS_LINUX) +const QString CHATTERINO_OS = u"linux"_s; +#elif defined(Q_OS_FREEBSD) +const QString CHATTERINO_OS = u"freebsd"_s; +#else +const QString CHATTERINO_OS = u"unknown"_s; +#endif - auto onlineMatch = matchVersion.match(online); - auto currentMatch = matchVersion.match(current); - - for (int i = 1; i <= 4; i++) - { - if (onlineMatch.captured(i).toInt() < - currentMatch.captured(i).toInt()) - { - return true; - } - if (onlineMatch.captured(i).toInt() > - currentMatch.captured(i).toInt()) - { - break; - } - } - - return false; - } } // namespace -Updates::Updates() - : currentVersion_(CHATTERINO_VERSION) +namespace chatterino { + +Updates::Updates(const Paths &paths_, Settings &settings) + : paths(paths_) + , currentVersion_(CHATTERINO_VERSION) , updateGuideLink_("https://chatterino.com") { qCDebug(chatterinoUpdate) << "init UpdateManager"; + + settings.betaUpdates.connect( + [this] { + this->checkForUpdates(); + }, + this->managedConnections, false); } -Updates &Updates::instance() +/// Checks if the online version is newer or older than the current version. +bool Updates::isDowngradeOf(const QString &online, const QString ¤t) { - // fourtf: don't add this class to the application class - static Updates instance; + semver::version onlineVersion; + if (!onlineVersion.from_string_noexcept(online.toStdString())) + { + qCWarning(chatterinoUpdate) << "Unable to parse online version" + << online << "into a proper semver string"; + return false; + } - return instance; + semver::version currentVersion; + if (!currentVersion.from_string_noexcept(current.toStdString())) + { + qCWarning(chatterinoUpdate) << "Unable to parse current version" + << current << "into a proper semver string"; + return false; + } + + return onlineVersion < currentVersion; +} + +void Updates::deleteOldFiles() +{ + std::ignore = QtConcurrent::run([dir{this->paths.miscDirectory}] { + { + auto path = combinePath(dir, "Update.exe"); + if (QFile::exists(path)) + { + QFile::remove(path); + } + } + { + auto path = combinePath(dir, "update.zip"); + if (QFile::exists(path)) + { + QFile::remove(path); + } + } + }); } const QString &Updates::getCurrentVersion() const @@ -104,7 +136,7 @@ void Updates::installUpdates() box->exec(); QDesktopServices::openUrl(this->updateGuideLink_); #elif defined Q_OS_WIN - if (getPaths()->isPortable()) + if (Modes::instance().isPortable) { QMessageBox *box = new QMessageBox(QMessageBox::Information, "Chatterino Update", @@ -128,10 +160,22 @@ void Updates::installUpdates() box->raise(); }); }) - .onSuccess([this](auto result) -> Outcome { + .onSuccess([this](auto result) { + if (result.status() != 200) + { + auto *box = new QMessageBox( + QMessageBox::Information, "Chatterino Update", + QStringLiteral("The update couldn't be downloaded " + "(Error: %1).") + .arg(result.formatError())); + box->setAttribute(Qt::WA_DeleteOnClose); + box->exec(); + return; + } + QByteArray object = result.getData(); auto filename = - combinePath(getPaths()->miscDirectory, "update.zip"); + combinePath(this->paths.miscDirectory, "update.zip"); QFile file(filename); file.open(QIODevice::Truncate | QIODevice::WriteOnly); @@ -139,7 +183,7 @@ void Updates::installUpdates() if (file.write(object) == -1) { this->setStatus_(WriteFileFailed); - return Failure; + return; } file.flush(); file.close(); @@ -150,7 +194,6 @@ void Updates::installUpdates() {filename, "restart"}); QApplication::exit(0); - return Success; }) .execute(); this->setStatus_(Downloading); @@ -177,12 +220,24 @@ void Updates::installUpdates() box->setAttribute(Qt::WA_DeleteOnClose); box->exec(); }) - .onSuccess([this](auto result) -> Outcome { - QByteArray object = result.getData(); - auto filename = - combinePath(getPaths()->miscDirectory, "Update.exe"); + .onSuccess([this](auto result) { + if (result.status() != 200) + { + auto *box = new QMessageBox( + QMessageBox::Information, "Chatterino Update", + QStringLiteral("The update couldn't be downloaded " + "(Error: %1).") + .arg(result.formatError())); + box->setAttribute(Qt::WA_DeleteOnClose); + box->exec(); + return; + } - QFile file(filename); + QByteArray object = result.getData(); + auto filePath = + combinePath(this->paths.miscDirectory, "Update.exe"); + + QFile file(filePath); file.open(QIODevice::Truncate | QIODevice::WriteOnly); if (file.write(object) == -1) @@ -198,12 +253,12 @@ void Updates::installUpdates() box->exec(); QDesktopServices::openUrl(this->updateExe_); - return Failure; + return; } file.flush(); file.close(); - if (QProcess::startDetached(filename)) + if (QProcess::startDetached(filePath, {})) { QApplication::exit(0); } @@ -221,8 +276,6 @@ void Updates::installUpdates() QDesktopServices::openUrl(this->updateExe_); } - - return Success; }) .execute(); this->setStatus_(Downloading); @@ -232,6 +285,7 @@ void Updates::installUpdates() void Updates::checkForUpdates() { +#ifndef CHATTERINO_DISABLE_UPDATER auto version = Version::instance(); if (!version.isSupportedOS()) @@ -254,75 +308,78 @@ void Updates::checkForUpdates() return; } - QString url = - "https://notitia.chatterino.com/version/chatterino/" CHATTERINO_OS "/" + - currentBranch(); + QString url = "https://notitia.chatterino.com/version/chatterino/" % + CHATTERINO_OS % "/" % currentBranch(); NetworkRequest(url) .timeout(60000) - .onSuccess([this](auto result) -> Outcome { - auto object = result.parseJson(); + .onSuccess([this](auto result) { + const auto object = result.parseJson(); /// Version available on every platform - QJsonValue version_val = object.value("version"); + auto version = object["version"]; - if (!version_val.isString()) + if (!version.isString()) { this->setStatus_(SearchFailed); - qCDebug(chatterinoUpdate) << "error updating"; - return Failure; + qCDebug(chatterinoUpdate) + << "error checking version - missing 'version'" << object; + return; } -#if defined Q_OS_WIN || defined Q_OS_MACOS +# if defined Q_OS_WIN || defined Q_OS_MACOS /// Downloads an installer for the new version - QJsonValue updateExe_val = object.value("updateexe"); - if (!updateExe_val.isString()) + auto updateExeUrl = object["updateexe"]; + if (!updateExeUrl.isString()) { this->setStatus_(SearchFailed); - qCDebug(chatterinoUpdate) << "error updating"; - return Failure; + qCDebug(chatterinoUpdate) + << "error checking version - missing 'updateexe'" << object; + return; } - this->updateExe_ = updateExe_val.toString(); + this->updateExe_ = updateExeUrl.toString(); -# ifdef Q_OS_WIN +# ifdef Q_OS_WIN /// Windows portable - QJsonValue portable_val = object.value("portable_download"); - if (!portable_val.isString()) + auto portableUrl = object["portable_download"]; + if (!portableUrl.isString()) { this->setStatus_(SearchFailed); - qCDebug(chatterinoUpdate) << "error updating"; - return Failure; + qCDebug(chatterinoUpdate) + << "error checking version - missing 'portable_download'" + << object; + return; } - this->updatePortable_ = portable_val.toString(); + this->updatePortable_ = portableUrl.toString(); +# endif + +# elif defined Q_OS_LINUX + QJsonValue updateGuide = object.value("updateguide"); + if (updateGuide.isString()) + { + this->updateGuideLink_ = updateGuide.toString(); + } +# else + return; # endif -#elif defined Q_OS_LINUX - QJsonValue updateGuide_val = object.value("updateguide"); - if (updateGuide_val.isString()) - { - this->updateGuideLink_ = updateGuide_val.toString(); - } -#else - return Failure; -#endif - /// Current version - this->onlineVersion_ = version_val.toString(); + this->onlineVersion_ = version.toString(); /// Update available :) if (this->currentVersion_ != this->onlineVersion_) { this->setStatus_(UpdateAvailable); - this->isDowngrade_ = - isDowngradeOf(this->onlineVersion_, this->currentVersion_); + this->isDowngrade_ = Updates::isDowngradeOf( + this->onlineVersion_, this->currentVersion_); } else { this->setStatus_(NoUpdateAvailable); } - return Failure; }) .execute(); this->setStatus_(Searching); +#endif } Updates::Status Updates::getStatus() const diff --git a/src/singletons/Updates.hpp b/src/singletons/Updates.hpp index 9ad33b283..93160b9f9 100644 --- a/src/singletons/Updates.hpp +++ b/src/singletons/Updates.hpp @@ -1,15 +1,28 @@ #pragma once -#include +#include #include +#include + +#include +#include namespace chatterino { +class Paths; +class Settings; + +/** + * To check for updates, use the `checkForUpdates` method. + * The class by itself does not start any automatic updates. + */ class Updates { - Updates(); + const Paths &paths; public: + Updates(const Paths &paths_, Settings &settings); + enum Status { None, Searching, @@ -21,8 +34,12 @@ public: WriteFileFailed, }; - // fourtf: don't add this class to the application class - static Updates &instance(); + static bool isDowngradeOf(const QString &online, const QString ¤t); + + /** + * @brief Delete old files that belong to the update process + */ + void deleteOldFiles(); void checkForUpdates(); const QString &getCurrentVersion() const; @@ -47,6 +64,9 @@ private: QString updateGuideLink_; void setStatus_(Status status); + + std::vector> + managedConnections; }; } // namespace chatterino diff --git a/src/singletons/WindowManager.cpp b/src/singletons/WindowManager.cpp index f7f555d66..bf0192e73 100644 --- a/src/singletons/WindowManager.cpp +++ b/src/singletons/WindowManager.cpp @@ -1,47 +1,42 @@ #include "singletons/WindowManager.hpp" +#include "Application.hpp" +#include "common/Args.hpp" +#include "common/QLogging.hpp" +#include "debug/AssertInGuiThread.hpp" +#include "messages/MessageElement.hpp" +#include "providers/twitch/TwitchIrcServer.hpp" +#include "singletons/Paths.hpp" +#include "singletons/Settings.hpp" +#include "singletons/Theme.hpp" +#include "util/CombinePath.hpp" +#include "util/SignalListener.hpp" +#include "widgets/AccountSwitchPopup.hpp" +#include "widgets/dialogs/SettingsDialog.hpp" +#include "widgets/FramelessEmbedWindow.hpp" +#include "widgets/helper/NotebookTab.hpp" +#include "widgets/Notebook.hpp" +#include "widgets/splits/Split.hpp" +#include "widgets/splits/SplitContainer.hpp" +#include "widgets/Window.hpp" + #include -#include #include #include #include #include #include #include -#include -#include -#include -#include "Application.hpp" -#include "common/Args.hpp" -#include "common/QLogging.hpp" -#include "debug/AssertInGuiThread.hpp" -#include "messages/MessageElement.hpp" -#include "providers/irc/Irc2.hpp" -#include "providers/irc/IrcChannel2.hpp" -#include "providers/irc/IrcServer.hpp" -#include "providers/twitch/TwitchIrcServer.hpp" -#include "singletons/Fonts.hpp" -#include "singletons/Paths.hpp" -#include "singletons/Settings.hpp" -#include "singletons/Theme.hpp" -#include "util/Clamp.hpp" -#include "util/CombinePath.hpp" -#include "widgets/AccountSwitchPopup.hpp" -#include "widgets/FramelessEmbedWindow.hpp" -#include "widgets/Notebook.hpp" -#include "widgets/Window.hpp" -#include "widgets/dialogs/SettingsDialog.hpp" -#include "widgets/helper/NotebookTab.hpp" -#include "widgets/splits/Split.hpp" -#include "widgets/splits/SplitContainer.hpp" +#include +#include namespace chatterino { namespace { - boost::optional &shouldMoveOutOfBoundsWindow() + std::optional &shouldMoveOutOfBoundsWindow() { - static boost::optional x; + static std::optional x; return x; } @@ -51,12 +46,11 @@ const QString WindowManager::WINDOW_LAYOUT_FILENAME( QStringLiteral("window-layout.json")); using SplitNode = SplitContainer::Node; -using SplitDirection = SplitContainer::Direction; void WindowManager::showSettingsDialog(QWidget *parent, SettingsDialogPreference preference) { - if (getArgs().dontSaveSettings) + if (getApp()->getArgs().dontSaveSettings) { QMessageBox::critical(parent, "Chatterino - Editing Settings Forbidden", "Settings cannot be edited when running with\n" @@ -88,49 +82,82 @@ void WindowManager::showAccountSelectPopup(QPoint point) w->refresh(); - QPoint buttonPos = point; - w->move(buttonPos.x() - 30, buttonPos.y()); + w->moveTo(point - QPoint(30, 0), widgets::BoundsChecking::CursorPosition); w->show(); w->setFocus(); } -WindowManager::WindowManager() - : windowLayoutFilePath(combinePath(getPaths()->settingsDirectory, +WindowManager::WindowManager(const Paths &paths, Settings &settings, + Theme &themes_, Fonts &fonts) + : themes(themes_) + , windowLayoutFilePath(combinePath(paths.settingsDirectory, WindowManager::WINDOW_LAYOUT_FILENAME)) + , updateWordTypeMaskListener([this] { + this->updateWordTypeMask(); + }) + , forceLayoutChannelViewsListener([this] { + this->forceLayoutChannelViews(); + }) + , layoutChannelViewsListener([this] { + this->layoutChannelViews(); + }) + , invalidateChannelViewBuffersListener([this] { + this->invalidateChannelViewBuffers(); + }) + , repaintVisibleChatWidgetsListener([this] { + this->repaintVisibleChatWidgets(); + }) { qCDebug(chatterinoWindowmanager) << "init WindowManager"; - auto settings = getSettings(); + this->updateWordTypeMaskListener.add(settings.showTimestamps); + this->updateWordTypeMaskListener.add(settings.showBadgesGlobalAuthority); + this->updateWordTypeMaskListener.add(settings.showBadgesPredictions); + this->updateWordTypeMaskListener.add(settings.showBadgesChannelAuthority); + this->updateWordTypeMaskListener.add(settings.showBadgesSubscription); + this->updateWordTypeMaskListener.add(settings.showBadgesVanity); + this->updateWordTypeMaskListener.add(settings.showBadgesChatterino); + this->updateWordTypeMaskListener.add(settings.showBadgesFfz); + this->updateWordTypeMaskListener.add(settings.showBadgesSevenTV); + this->updateWordTypeMaskListener.add(settings.enableEmoteImages); + this->updateWordTypeMaskListener.add(settings.lowercaseDomains); + this->updateWordTypeMaskListener.add(settings.showReplyButton); - this->wordFlagsListener_.addSetting(settings->showTimestamps); - this->wordFlagsListener_.addSetting(settings->showBadgesGlobalAuthority); - this->wordFlagsListener_.addSetting(settings->showBadgesPredictions); - this->wordFlagsListener_.addSetting(settings->showBadgesChannelAuthority); - this->wordFlagsListener_.addSetting(settings->showBadgesSubscription); - this->wordFlagsListener_.addSetting(settings->showBadgesVanity); - this->wordFlagsListener_.addSetting(settings->showBadgesChatterino); - this->wordFlagsListener_.addSetting(settings->showBadgesFfz); - this->wordFlagsListener_.addSetting(settings->enableEmoteImages); - this->wordFlagsListener_.addSetting(settings->boldUsernames); - this->wordFlagsListener_.addSetting(settings->lowercaseDomains); - this->wordFlagsListener_.addSetting(settings->showReplyButton); - this->wordFlagsListener_.setCB([this] { - this->updateWordTypeMask(); - }); + this->forceLayoutChannelViewsListener.add( + settings.moderationActions.delayedItemsChanged); + this->forceLayoutChannelViewsListener.add( + settings.highlightedMessages.delayedItemsChanged); + this->forceLayoutChannelViewsListener.add( + settings.highlightedUsers.delayedItemsChanged); + this->forceLayoutChannelViewsListener.add( + settings.highlightedBadges.delayedItemsChanged); + this->forceLayoutChannelViewsListener.add( + settings.removeSpacesBetweenEmotes); + this->forceLayoutChannelViewsListener.add(settings.emoteScale); + this->forceLayoutChannelViewsListener.add(settings.timestampFormat); + this->forceLayoutChannelViewsListener.add(settings.collpseMessagesMinLines); + this->forceLayoutChannelViewsListener.add(settings.enableRedeemedHighlight); + this->forceLayoutChannelViewsListener.add(settings.colorUsernames); + this->forceLayoutChannelViewsListener.add(settings.boldUsernames); + + this->layoutChannelViewsListener.add(settings.timestampFormat); + this->layoutChannelViewsListener.add(fonts.fontChanged); + + this->invalidateChannelViewBuffersListener.add(settings.alternateMessages); + this->invalidateChannelViewBuffersListener.add(settings.separateMessages); + + this->repaintVisibleChatWidgetsListener.add( + this->themes.repaintVisibleChatWidgets_); this->saveTimer = new QTimer; this->saveTimer->setSingleShot(true); QObject::connect(this->saveTimer, &QTimer::timeout, [] { - getApp()->windows->save(); + getApp()->getWindows()->save(); }); - this->miscUpdateTimer_.start(100); - - QObject::connect(&this->miscUpdateTimer_, &QTimer::timeout, [this] { - this->miscUpdate.invoke(); - }); + this->updateWordTypeMask(); } WindowManager::~WindowManager() = default; @@ -143,7 +170,7 @@ MessageElementFlags WindowManager::getWordFlags() void WindowManager::updateWordTypeMask() { using MEF = MessageElementFlag; - auto settings = getSettings(); + auto *settings = getSettings(); // text auto flags = MessageElementFlags(MEF::Text); @@ -179,6 +206,7 @@ void WindowManager::updateWordTypeMask() flags.set(settings->showBadgesChatterino ? MEF::BadgeChatterino : MEF::None); flags.set(settings->showBadgesFfz ? MEF::BadgeFfz : MEF::None); + flags.set(settings->showBadgesSevenTV ? MEF::BadgeSevenTV : MEF::None); // username flags.set(MEF::Username); @@ -190,10 +218,7 @@ void WindowManager::updateWordTypeMask() // misc flags.set(MEF::AlwaysShow); flags.set(MEF::Collapsed); - flags.set(settings->boldUsernames ? MEF::BoldUsername - : MEF::NonBoldUsername); - flags.set(settings->lowercaseDomains ? MEF::LowercaseLink - : MEF::OriginalLink); + flags.set(MEF::LowercaseLinks, settings->lowercaseDomains); flags.set(MEF::ChannelPointReward); // update flags @@ -218,6 +243,11 @@ void WindowManager::forceLayoutChannelViews() this->layoutChannelViews(nullptr); } +void WindowManager::invalidateChannelViewBuffers(Channel *channel) +{ + this->invalidateBuffersRequested.invoke(channel); +} + void WindowManager::repaintVisibleChatWidgets(Channel *channel) { this->layoutRequested.invoke(channel); @@ -242,11 +272,15 @@ Window &WindowManager::getMainWindow() return *this->mainWindow_; } -Window &WindowManager::getSelectedWindow() +Window *WindowManager::getLastSelectedWindow() const { assertInGuiThread(); + if (this->selectedWindow_ == nullptr) + { + return this->mainWindow_; + } - return *this->selectedWindow_; + return this->selectedWindow_; } Window &WindowManager::createWindow(WindowType type, bool show, QWidget *parent) @@ -260,12 +294,18 @@ Window &WindowManager::createWindow(WindowType type, bool show, QWidget *parent) return parent; } + // FIXME: On Windows, parenting popup windows causes unwanted behavior (see + // https://github.com/Chatterino/chatterino2/issues/4179 for discussion). Ideally, we + // would use a different solution rather than relying on OS-specific code but this is + // the low-effort fix for now. +#ifndef Q_OS_WIN if (type == WindowType::Popup) { // On some window managers, popup windows require a parent to behave correctly. See // https://github.com/Chatterino/chatterino2/pull/1843 for additional context. return &(this->getMainWindow()); } +#endif // If no parent is set and something other than a popup window is being created, we fall // back to the default behavior of no parent. @@ -320,44 +360,53 @@ void WindowManager::select(SplitContainer *container) this->selectSplitContainer.invoke(container); } -QPoint WindowManager::emotePopupPos() +void WindowManager::scrollToMessage(const MessagePtr &message) { - return this->emotePopupPos_; + this->scrollToMessageSignal.invoke(message); } -void WindowManager::setEmotePopupPos(QPoint pos) +QRect WindowManager::emotePopupBounds() const { - this->emotePopupPos_ = pos; + return this->emotePopupBounds_; } -void WindowManager::initialize(Settings &settings, Paths &paths) +void WindowManager::setEmotePopupBounds(QRect bounds) +{ + if (this->emotePopupBounds_ != bounds) + { + this->emotePopupBounds_ = bounds; + this->queueSave(); + } +} + +void WindowManager::initialize() { assertInGuiThread(); - getApp()->themes->repaintVisibleChatWidgets_.connect([this] { - this->repaintVisibleChatWidgets(); - }); - - assert(!this->initialized_); - { WindowLayout windowLayout; - if (getArgs().customChannelLayout) + if (getApp()->getArgs().customChannelLayout) { - windowLayout = getArgs().customChannelLayout.value(); + windowLayout = getApp()->getArgs().customChannelLayout.value(); } else { windowLayout = this->loadWindowLayoutFromFile(); } - this->emotePopupPos_ = windowLayout.emotePopupPos_; + auto desired = getApp()->getArgs().activateChannel; + if (desired) + { + windowLayout.activateOrAddChannel(desired->provider, desired->name); + } + + this->emotePopupBounds_ = windowLayout.emotePopupBounds_; this->applyWindowLayout(windowLayout); } - if (getArgs().isFramelessEmbed) + if (getApp()->getArgs().isFramelessEmbed) { this->framelessEmbedWindow_.reset(new FramelessEmbedWindow); this->framelessEmbedWindow_->show(); @@ -370,46 +419,27 @@ void WindowManager::initialize(Settings &settings, Paths &paths) this->mainWindow_->getNotebook().addPage(true); // TODO: don't create main window if it's a frameless embed - if (getArgs().isFramelessEmbed) + if (getApp()->getArgs().isFramelessEmbed) { this->mainWindow_->hide(); } } - - settings.timestampFormat.connect([this](auto, auto) { - this->layoutChannelViews(); - }); - - settings.emoteScale.connect([this](auto, auto) { - this->forceLayoutChannelViews(); - }); - - settings.timestampFormat.connect([this](auto, auto) { - this->forceLayoutChannelViews(); - }); - settings.alternateMessages.connect([this](auto, auto) { - this->forceLayoutChannelViews(); - }); - settings.separateMessages.connect([this](auto, auto) { - this->forceLayoutChannelViews(); - }); - settings.collpseMessagesMinLines.connect([this](auto, auto) { - this->forceLayoutChannelViews(); - }); - settings.enableRedeemedHighlight.connect([this](auto, auto) { - this->forceLayoutChannelViews(); - }); - - this->initialized_ = true; } void WindowManager::save() { - if (getArgs().dontSaveSettings) + if (getApp()->getArgs().dontSaveSettings) { return; } - qCDebug(chatterinoWindowmanager) << "[WindowManager] Saving"; + + if (this->shuttingDown_) + { + qCDebug(chatterinoWindowmanager) << "Skipping save (shutting down)"; + return; + } + + qCDebug(chatterinoWindowmanager) << "Saving"; assertInGuiThread(); QJsonDocument document; @@ -450,10 +480,12 @@ void WindowManager::save() windowObj.insert("width", rect.width()); windowObj.insert("height", rect.height()); - QJsonObject emotePopupObj; - emotePopupObj.insert("x", this->emotePopupPos_.x()); - emotePopupObj.insert("y", this->emotePopupPos_.y()); - windowObj.insert("emotePopup", emotePopupObj); + windowObj["emotePopup"] = QJsonObject{ + {"x", this->emotePopupBounds_.x()}, + {"y", this->emotePopupBounds_.y()}, + {"width", this->emotePopupBounds_.width()}, + {"height", this->emotePopupBounds_.height()}, + }; // window tabs QJsonArray tabsArr; @@ -542,7 +574,7 @@ void WindowManager::encodeNodeRecursively(SplitNode *node, QJsonObject &obj) { switch (node->getType()) { - case SplitNode::_Split: { + case SplitNode::Type::Split: { obj.insert("type", "split"); obj.insert("moderationMode", node->getSplit()->getModerationMode()); @@ -556,11 +588,12 @@ void WindowManager::encodeNodeRecursively(SplitNode *node, QJsonObject &obj) obj.insert("filters", filters); } break; - case SplitNode::HorizontalContainer: - case SplitNode::VerticalContainer: { - obj.insert("type", node->getType() == SplitNode::HorizontalContainer - ? "horizontal" - : "vertical"); + case SplitNode::Type::HorizontalContainer: + case SplitNode::Type::VerticalContainer: { + obj.insert("type", + node->getType() == SplitNode::Type::HorizontalContainer + ? "horizontal" + : "vertical"); QJsonArray itemsArr; for (const std::unique_ptr &n : node->getChildren()) @@ -589,6 +622,10 @@ void WindowManager::encodeChannel(IndirectChannel channel, QJsonObject &obj) obj.insert("name", channel.get()->getName()); } break; + case Channel::Type::TwitchAutomod: { + obj.insert("type", "automod"); + } + break; case Channel::Type::TwitchMentions: { obj.insert("type", "mentions"); } @@ -605,19 +642,10 @@ void WindowManager::encodeChannel(IndirectChannel channel, QJsonObject &obj) obj.insert("type", "live"); } break; - case Channel::Type::Irc: { - if (auto ircChannel = - dynamic_cast(channel.get().get())) - { - obj.insert("type", "irc"); - if (ircChannel->server()) - { - obj.insert("server", ircChannel->server()->id()); - } - obj.insert("channel", ircChannel->getName()); - } + case Channel::Type::Misc: { + obj.insert("type", "misc"); + obj.insert("name", channel.get()->getName()); } - break; } } @@ -636,32 +664,34 @@ IndirectChannel WindowManager::decodeChannel(const SplitDescriptor &descriptor) { assertInGuiThread(); - auto app = getApp(); - if (descriptor.type_ == "twitch") { - return app->twitch->getOrAddChannel(descriptor.channelName_); + return getApp()->getTwitch()->getOrAddChannel(descriptor.channelName_); } else if (descriptor.type_ == "mentions") { - return app->twitch->mentionsChannel; + return getApp()->getTwitch()->getMentionsChannel(); } else if (descriptor.type_ == "watching") { - return app->twitch->watchingChannel; + return getApp()->getTwitch()->getWatchingChannel(); } else if (descriptor.type_ == "whispers") { - return app->twitch->whispersChannel; + return getApp()->getTwitch()->getWhispersChannel(); } else if (descriptor.type_ == "live") { - return app->twitch->liveChannel; + return getApp()->getTwitch()->getLiveChannel(); } - else if (descriptor.type_ == "irc") + else if (descriptor.type_ == "automod") { - return Irc::instance().getOrAddChannel(descriptor.server_, - descriptor.channelName_); + return getApp()->getTwitch()->getAutomodChannel(); + } + else if (descriptor.type_ == "misc") + { + return getApp()->getTwitch()->getChannelOrEmpty( + descriptor.channelName_); } return Channel::getEmpty(); @@ -671,6 +701,9 @@ void WindowManager::closeAll() { assertInGuiThread(); + qCDebug(chatterinoWindowmanager) << "Shutting down (closing windows)"; + this->shuttingDown_ = true; + for (Window *window : windows_) { window->close(); @@ -694,13 +727,13 @@ WindowLayout WindowManager::loadWindowLayoutFromFile() const void WindowManager::applyWindowLayout(const WindowLayout &layout) { - if (getArgs().dontLoadMainWindow) + if (getApp()->getArgs().dontLoadMainWindow) { return; } // Set emote popup position - this->emotePopupPos_ = layout.emotePopupPos_; + this->emotePopupBounds_ = layout.emotePopupBounds_; for (const auto &windowData : layout.windows_) { @@ -718,9 +751,9 @@ void WindowManager::applyWindowLayout(const WindowLayout &layout) // get geometry { // out of bounds windows - auto screens = qApp->screens(); + auto screens = QApplication::screens(); bool outOfBounds = - !getenv("I3SOCK") && + !qEnvironmentVariableIsSet("I3SOCK") && std::none_of(screens.begin(), screens.end(), [&](QScreen *screen) { return screen->availableGeometry().intersects( @@ -749,10 +782,14 @@ void WindowManager::applyWindowLayout(const WindowLayout &layout) // Have to offset x by one because qt moves the window 1px too // far to the left:w - window.setInitialBounds({windowData.geometry_.x(), - windowData.geometry_.y(), - windowData.geometry_.width(), - windowData.geometry_.height()}); + window.setInitialBounds( + { + windowData.geometry_.x(), + windowData.geometry_.y(), + windowData.geometry_.width(), + windowData.geometry_.height(), + }, + widgets::BoundsChecking::Off); } } diff --git a/src/singletons/WindowManager.hpp b/src/singletons/WindowManager.hpp index 5ad9cb890..7f0dcdee6 100644 --- a/src/singletons/WindowManager.hpp +++ b/src/singletons/WindowManager.hpp @@ -1,20 +1,31 @@ #pragma once -#include -#include "common/Channel.hpp" #include "common/FlagsEnum.hpp" -#include "common/Singleton.hpp" -#include "common/WindowDescriptors.hpp" - -#include "pajlada/settings/settinglistener.hpp" +#include "util/SignalListener.hpp" #include "widgets/splits/SplitContainer.hpp" +#include +#include +#include + +#include + namespace chatterino { class Settings; class Paths; class Window; -class SplitContainer; +class ChannelView; +class IndirectChannel; +class Split; +struct SplitDescriptor; +class Channel; +using ChannelPtr = std::shared_ptr; +struct Message; +using MessagePtr = std::shared_ptr; +class WindowLayout; +class Theme; +class Fonts; enum class MessageElementFlag : int64_t; using MessageElementFlags = FlagsEnum; @@ -23,13 +34,21 @@ enum class WindowType; enum class SettingsDialogPreference; class FramelessEmbedWindow; -class WindowManager final : public Singleton +class WindowManager final { + Theme &themes; + public: static const QString WINDOW_LAYOUT_FILENAME; - WindowManager(); - ~WindowManager() override; + explicit WindowManager(const Paths &paths, Settings &settings, + Theme &themes_, Fonts &fonts); + ~WindowManager(); + + WindowManager(const WindowManager &) = delete; + WindowManager(WindowManager &&) = delete; + WindowManager &operator=(const WindowManager &) = delete; + WindowManager &operator=(WindowManager &&) = delete; static void encodeTab(SplitContainer *tab, bool isSelected, QJsonObject &obj); @@ -52,11 +71,22 @@ public: // This is called, for example, when the emote scale or timestamp format has // changed void forceLayoutChannelViews(); + + // Tell a channel (or all channels if channel is nullptr) to invalidate all paint buffers + void invalidateChannelViewBuffers(Channel *channel = nullptr); + void repaintVisibleChatWidgets(Channel *channel = nullptr); void repaintGifEmotes(); Window &getMainWindow(); - Window &getSelectedWindow(); + + // Returns a pointer to the last selected window. + // Edge cases: + // - If the application was not focused since the start, this will return a pointer to the main window. + // - If the window was closed this points to the main window. + // - If the window was unfocused since being selected, this function will still return it. + Window *getLastSelectedWindow() const; + Window &createWindow(WindowType type, bool show = true, QWidget *parent = nullptr); @@ -66,12 +96,20 @@ public: void select(Split *split); void select(SplitContainer *container); + /** + * Scrolls to the message in a split that's not + * a mentions view and focuses the split. + * + * @param message Message to scroll to. + */ + void scrollToMessage(const MessagePtr &message); - QPoint emotePopupPos(); - void setEmotePopupPos(QPoint pos); + QRect emotePopupBounds() const; + void setEmotePopupBounds(QRect bounds); - virtual void initialize(Settings &settings, Paths &paths) override; - virtual void save() override; + // Set up some final signals & actually show the windows + void initialize(); + void save(); void closeAll(); int getGeneration() const; @@ -96,15 +134,15 @@ public: // This signal fires whenever views rendering a channel, or all views if the // channel is a nullptr, need to redo their layout pajlada::Signals::Signal layoutRequested; + // This signal fires whenever views rendering a channel, or all views if the + // channel is a nullptr, need to invalidate their paint buffers + pajlada::Signals::Signal invalidateBuffersRequested; pajlada::Signals::NoArgSignal wordFlagsChanged; - // This signal fires every 100ms and can be used to trigger random things that require a recheck. - // It is currently being used by the "Tooltip Preview Image" system to recheck if an image is ready to be rendered. - pajlada::Signals::NoArgSignal miscUpdate; - pajlada::Signals::Signal selectSplit; pajlada::Signals::Signal selectSplitContainer; + pajlada::Signals::Signal scrollToMessageSignal; private: static void encodeNodeRecursively(SplitContainer::Node *node, @@ -119,9 +157,9 @@ private: // Contains the full path to the window layout file, e.g. /home/pajlada/.local/share/Chatterino/Settings/window-layout.json const QString windowLayoutFilePath; - bool initialized_ = false; + bool shuttingDown_ = false; - QPoint emotePopupPos_; + QRect emotePopupBounds_; std::atomic generation_{0}; @@ -132,10 +170,18 @@ private: Window *selectedWindow_{}; MessageElementFlags wordFlags_{}; - pajlada::SettingListener wordFlagsListener_; QTimer *saveTimer; - QTimer miscUpdateTimer_; + + pajlada::Signals::SignalHolder signalHolder; + + SignalListener updateWordTypeMaskListener; + SignalListener forceLayoutChannelViewsListener; + SignalListener layoutChannelViewsListener; + SignalListener invalidateChannelViewBuffersListener; + SignalListener repaintVisibleChatWidgetsListener; + + friend class Window; // this is for selectedWindow_ }; } // namespace chatterino diff --git a/src/singletons/helper/GifTimer.cpp b/src/singletons/helper/GifTimer.cpp index 2edf7bacd..07e255381 100644 --- a/src/singletons/helper/GifTimer.cpp +++ b/src/singletons/helper/GifTimer.cpp @@ -4,27 +4,36 @@ #include "singletons/Settings.hpp" #include "singletons/WindowManager.hpp" +#include + namespace chatterino { void GIFTimer::initialize() { - this->timer.setInterval(30); + this->timer.setInterval(GIF_FRAME_LENGTH); + this->timer.setTimerType(Qt::PreciseTimer); getSettings()->animateEmotes.connect([this](bool enabled, auto) { if (enabled) + { this->timer.start(); + } else + { this->timer.stop(); + } }); QObject::connect(&this->timer, &QTimer::timeout, [this] { if (getSettings()->animationsWhenFocused && - qApp->activeWindow() == nullptr) + QApplication::activeWindow() == nullptr) + { return; + } - this->position_ += gifFrameLength; + this->position_ += GIF_FRAME_LENGTH; this->signal.invoke(); - getApp()->windows->repaintGifEmotes(); + getApp()->getWindows()->repaintGifEmotes(); }); } diff --git a/src/singletons/helper/GifTimer.hpp b/src/singletons/helper/GifTimer.hpp index da47149b8..ecb741be7 100644 --- a/src/singletons/helper/GifTimer.hpp +++ b/src/singletons/helper/GifTimer.hpp @@ -1,11 +1,11 @@ #pragma once -#include #include +#include namespace chatterino { -constexpr long unsigned gifFrameLength = 33; +constexpr long unsigned GIF_FRAME_LENGTH = 20; class GIFTimer { diff --git a/src/singletons/helper/LoggingChannel.cpp b/src/singletons/helper/LoggingChannel.cpp index 54dac7f78..dcce92f23 100644 --- a/src/singletons/helper/LoggingChannel.cpp +++ b/src/singletons/helper/LoggingChannel.cpp @@ -1,22 +1,64 @@ -#include "LoggingChannel.hpp" +#include "singletons/helper/LoggingChannel.hpp" #include "Application.hpp" #include "common/QLogging.hpp" +#include "messages/Message.hpp" +#include "messages/MessageThread.hpp" #include "singletons/Paths.hpp" #include "singletons/Settings.hpp" +#include #include -#include +namespace { + +const QByteArray ENDLINE("\n"); + +void appendLine(QFile &fileHandle, const QString &line) +{ + assert(fileHandle.isOpen()); + assert(fileHandle.isWritable()); + + fileHandle.write(line.toUtf8()); + fileHandle.flush(); +} + +QString generateOpeningString( + const QDateTime &now = QDateTime::currentDateTime()) +{ + QString ret("# Start logging at "); + + ret.append(now.toString("yyyy-MM-dd HH:mm:ss ")); + ret.append(now.timeZoneAbbreviation()); + ret.append(ENDLINE); + + return ret; +} + +QString generateClosingString( + const QDateTime &now = QDateTime::currentDateTime()) +{ + QString ret("# Stop logging at "); + + ret.append(now.toString("yyyy-MM-dd HH:mm:ss")); + ret.append(now.timeZoneAbbreviation()); + ret.append(ENDLINE); + + return ret; +} + +QString generateDateString(const QDateTime &now) +{ + return now.toString("yyyy-MM-dd"); +} + +} // namespace namespace chatterino { -QByteArray endline("\n"); - -LoggingChannel::LoggingChannel(const QString &_channelName, - const QString &_platform) - : channelName(_channelName) - , platform(_platform) +LoggingChannel::LoggingChannel(QString _channelName, QString _platform) + : channelName(std::move(_channelName)) + , platform(std::move(_platform)) { if (this->channelName.startsWith("/whispers")) { @@ -30,6 +72,10 @@ LoggingChannel::LoggingChannel(const QString &_channelName, { this->subDirectory = "Live"; } + else if (channelName.startsWith("/automod")) + { + this->subDirectory = "AutoMod"; + } else { this->subDirectory = @@ -41,22 +87,24 @@ LoggingChannel::LoggingChannel(const QString &_channelName, QDir::separator() + this->subDirectory; getSettings()->logPath.connect([this](const QString &logPath, auto) { - this->baseDirectory = - logPath.isEmpty() ? getPaths()->messageLogDirectory : logPath; + this->baseDirectory = logPath.isEmpty() + ? getApp()->getPaths().messageLogDirectory + : logPath; this->openLogFile(); }); } LoggingChannel::~LoggingChannel() { - this->appendLine(this->generateClosingString()); + appendLine(this->fileHandle, generateClosingString()); this->fileHandle.close(); + this->currentStreamFileHandle.close(); } void LoggingChannel::openLogFile() { QDateTime now = QDateTime::currentDateTime(); - this->dateString = this->generateDateString(now); + this->dateString = generateDateString(now); if (this->fileHandle.isOpen()) { @@ -82,14 +130,45 @@ void LoggingChannel::openLogFile() this->fileHandle.open(QIODevice::Append); - this->appendLine(this->generateOpeningString(now)); + appendLine(this->fileHandle, generateOpeningString(now)); } -void LoggingChannel::addMessage(MessagePtr message) +void LoggingChannel::openStreamLogFile(const QString &streamID) +{ + QDateTime now = QDateTime::currentDateTime(); + this->currentStreamID = streamID; + + if (this->currentStreamFileHandle.isOpen()) + { + this->currentStreamFileHandle.flush(); + this->currentStreamFileHandle.close(); + } + + QString baseFileName = this->channelName + "-" + streamID + ".log"; + + QString directory = + this->baseDirectory + QDir::separator() + this->subDirectory; + + if (!QDir().mkpath(directory)) + { + qCDebug(chatterinoHelper) << "Unable to create logging path"; + return; + } + + QString fileName = directory + QDir::separator() + baseFileName; + qCDebug(chatterinoHelper) << "Logging stream to" << fileName; + this->currentStreamFileHandle.setFileName(fileName); + + this->currentStreamFileHandle.open(QIODevice::Append); + appendLine(this->currentStreamFileHandle, generateOpeningString(now)); +} + +void LoggingChannel::addMessage(const MessagePtr &message, + const QString &streamID) { QDateTime now = QDateTime::currentDateTime(); - QString messageDateString = this->generateDateString(now); + QString messageDateString = generateDateString(now); if (messageDateString != this->dateString) { this->dateString = messageDateString; @@ -97,47 +176,71 @@ void LoggingChannel::addMessage(MessagePtr message) } QString str; + if (channelName.startsWith("/mentions") || + channelName.startsWith("/automod")) + { + str.append("#" + message->channelName + " "); + } + str.append('['); str.append(now.toString("HH:mm:ss")); str.append("] "); - str.append(message->searchText); - str.append(endline); + QString messageText; + if (message->loginName.isEmpty()) + { + // This accounts for any messages not explicitly sent by a user, like + // system messages, parts of announcements, subs etc. + messageText = message->messageText; + } + else + { + if (message->localizedName.isEmpty()) + { + messageText = message->loginName + ": " + message->messageText; + } + else + { + messageText = message->localizedName + " " + message->loginName + + ": " + message->messageText; + } + } - this->appendLine(str); -} + if ((message->flags.has(MessageFlag::ReplyMessage) && + getSettings()->stripReplyMention) && + !getSettings()->hideReplyContext) + { + qsizetype colonIndex = messageText.indexOf(':'); + if (colonIndex != -1) + { + QString rootMessageChatter; + if (message->replyParent) + { + rootMessageChatter = message->replyParent->loginName; + } + else + { + // we actually want to use 'reply-parent-user-login' tag here, + // but it's not worth storing just for this edge case + rootMessageChatter = message->replyThread->root()->loginName; + } + messageText.insert(colonIndex + 1, " @" + rootMessageChatter); + } + } + str.append(messageText); + str.append(ENDLINE); -QString LoggingChannel::generateOpeningString(const QDateTime &now) const -{ - QString ret = QLatin1Literal("# Start logging at "); + appendLine(this->fileHandle, str); - ret.append(now.toString("yyyy-MM-dd HH:mm:ss ")); - ret.append(now.timeZoneAbbreviation()); - ret.append(endline); + if (!streamID.isEmpty() && getSettings()->separatelyStoreStreamLogs) + { + if (this->currentStreamID != streamID) + { + this->openStreamLogFile(streamID); + } - return ret; -} - -QString LoggingChannel::generateClosingString(const QDateTime &now) const -{ - QString ret = QLatin1Literal("# Stop logging at "); - - ret.append(now.toString("yyyy-MM-dd HH:mm:ss")); - ret.append(now.timeZoneAbbreviation()); - ret.append(endline); - - return ret; -} - -void LoggingChannel::appendLine(const QString &line) -{ - this->fileHandle.write(line.toUtf8()); - this->fileHandle.flush(); -} - -QString LoggingChannel::generateDateString(const QDateTime &now) -{ - return now.toString("yyyy-MM-dd"); + appendLine(this->currentStreamFileHandle, str); + } } } // namespace chatterino diff --git a/src/singletons/helper/LoggingChannel.hpp b/src/singletons/helper/LoggingChannel.hpp index e189995de..3e57dd9bd 100644 --- a/src/singletons/helper/LoggingChannel.hpp +++ b/src/singletons/helper/LoggingChannel.hpp @@ -1,38 +1,34 @@ #pragma once -#include "messages/Message.hpp" - -#include #include #include -#include #include namespace chatterino { class Logging; +struct Message; +using MessagePtr = std::shared_ptr; -class LoggingChannel : boost::noncopyable +class LoggingChannel { - explicit LoggingChannel(const QString &_channelName, - const QString &platform); + explicit LoggingChannel(QString _channelName, QString _platform); public: ~LoggingChannel(); - void addMessage(MessagePtr message); + + LoggingChannel(const LoggingChannel &) = delete; + LoggingChannel &operator=(const LoggingChannel &) = delete; + + LoggingChannel(LoggingChannel &&) = delete; + LoggingChannel &operator=(LoggingChannel &&) = delete; + + void addMessage(const MessagePtr &message, const QString &streamID); private: void openLogFile(); - - QString generateOpeningString( - const QDateTime &now = QDateTime::currentDateTime()) const; - QString generateClosingString( - const QDateTime &now = QDateTime::currentDateTime()) const; - - void appendLine(const QString &line); - - QString generateDateString(const QDateTime &now); + void openStreamLogFile(const QString &streamID); const QString channelName; const QString platform; @@ -40,6 +36,8 @@ private: QString subDirectory; QFile fileHandle; + QFile currentStreamFileHandle; + QString currentStreamID; QString dateString; diff --git a/src/util/AbandonObject.hpp b/src/util/AbandonObject.hpp new file mode 100644 index 000000000..b0588449a --- /dev/null +++ b/src/util/AbandonObject.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include + +namespace chatterino { + +/// Guard to call `deleteLater` on a QObject when destroyed. +class AbandonObject +{ +public: + AbandonObject(QObject *obj) + : obj_(obj) + { + } + + ~AbandonObject() + { + if (this->obj_) + { + this->obj_->deleteLater(); + } + } + + AbandonObject(const AbandonObject &) = delete; + AbandonObject(AbandonObject &&) = delete; + AbandonObject &operator=(const AbandonObject &) = delete; + AbandonObject &operator=(AbandonObject &&) = delete; + +private: + QObject *obj_; +}; + +} // namespace chatterino diff --git a/src/util/AttachToConsole.cpp b/src/util/AttachToConsole.cpp index b1c993ad4..5f887260e 100644 --- a/src/util/AttachToConsole.cpp +++ b/src/util/AttachToConsole.cpp @@ -1,7 +1,10 @@ -#include "AttachToConsole.hpp" +#include "util/AttachToConsole.hpp" #ifdef USEWINSDK # include + +# include +# include #endif namespace chatterino { @@ -11,8 +14,8 @@ void attachToConsole() #ifdef USEWINSDK if (AttachConsole(ATTACH_PARENT_PROCESS)) { - freopen("CONOUT$", "w", stdout); - freopen("CONOUT$", "w", stderr); + std::ignore = freopen_s(nullptr, "CONOUT$", "w", stdout); + std::ignore = freopen_s(nullptr, "CONOUT$", "w", stderr); } #endif } diff --git a/src/util/CancellationToken.hpp b/src/util/CancellationToken.hpp new file mode 100644 index 000000000..12c26f6b1 --- /dev/null +++ b/src/util/CancellationToken.hpp @@ -0,0 +1,82 @@ +#pragma once + +#include +#include + +namespace chatterino { + +/// The CancellationToken is a thread-safe way for worker(s) +/// to know if the task they want to continue doing should be cancelled. +class CancellationToken +{ +public: + CancellationToken() = default; + explicit CancellationToken(bool isCancelled) + : isCancelled_(new std::atomic(isCancelled)) + { + } + + CancellationToken(const CancellationToken &) = default; + CancellationToken(CancellationToken &&other) + : isCancelled_(std::move(other.isCancelled_)){}; + + CancellationToken &operator=(CancellationToken &&other) + { + this->isCancelled_ = std::move(other.isCancelled_); + return *this; + } + CancellationToken &operator=(const CancellationToken &) = default; + + void cancel() + { + if (this->isCancelled_ != nullptr) + { + this->isCancelled_->store(true, std::memory_order_release); + } + } + + bool isCancelled() const + { + return this->isCancelled_ == nullptr || + this->isCancelled_->load(std::memory_order_acquire); + } + +private: + std::shared_ptr> isCancelled_; +}; + +/// The ScopedCancellationToken is a way to automatically cancel a CancellationToken when it goes out of scope +class ScopedCancellationToken +{ +public: + ScopedCancellationToken() = default; + ScopedCancellationToken(CancellationToken &&backingToken) + : backingToken_(std::move(backingToken)) + { + } + ScopedCancellationToken(CancellationToken backingToken) + : backingToken_(std::move(backingToken)) + { + } + + ~ScopedCancellationToken() + { + this->backingToken_.cancel(); + } + + ScopedCancellationToken(const ScopedCancellationToken &) = delete; + ScopedCancellationToken(ScopedCancellationToken &&other) + : backingToken_(std::move(other.backingToken_)){}; + ScopedCancellationToken &operator=(ScopedCancellationToken &&other) + { + this->backingToken_ = std::move(other.backingToken_); + return *this; + } + ScopedCancellationToken &operator=(const ScopedCancellationToken &) = + delete; + +private: + CancellationToken backingToken_; +}; + +} // namespace chatterino diff --git a/src/util/ChannelHelpers.hpp b/src/util/ChannelHelpers.hpp new file mode 100644 index 000000000..f53f3aa23 --- /dev/null +++ b/src/util/ChannelHelpers.hpp @@ -0,0 +1,121 @@ +#pragma once + +#include "common/Channel.hpp" +#include "messages/Message.hpp" +#include "messages/MessageBuilder.hpp" +#include "singletons/Settings.hpp" + +namespace chatterino { + +/// Adds a timeout or replaces a previous one sent in the last 20 messages and in the last 5s. +/// This function accepts any buffer to store the messsages in. +/// @param replaceMessage A function of type `void (int index, MessagePtr toReplace, MessagePtr replacement)` +/// - replace `buffer[i]` (=toReplace) with `replacement` +/// @param addMessage A function of type `void (MessagePtr message)` +/// - adds the `message`. +/// @param disableUserMessages If set, disables all message by the timed out user. +template +void addOrReplaceChannelTimeout(const Buf &buffer, MessagePtr message, + QTime now, Replace replaceMessage, + Add addMessage, bool disableUserMessages) +{ + // NOTE: This function uses the messages PARSE time to figure out whether they should be replaced + // This works as expected for incoming messages, but not for historic messages. + // This has never worked before, but would be nice in the future. + // For this to work, we need to make sure *all* messages have a "server received time". + + auto snapshotLength = static_cast(buffer.size()); + + auto end = std::max(0, snapshotLength - 20); + + bool shouldAddMessage = true; + + QTime minimumTime = now.addSecs(-5); + + auto timeoutStackStyle = static_cast( + getSettings()->timeoutStackStyle.getValue()); + + for (auto i = snapshotLength - 1; i >= end; --i) + { + const MessagePtr &s = buffer[i]; + + if (s->parseTime < minimumTime) + { + break; + } + + if (s->flags.has(MessageFlag::Untimeout) && + s->timeoutUser == message->timeoutUser) + { + break; + } + + if (timeoutStackStyle == TimeoutStackStyle::DontStackBeyondUserMessage) + { + if (s->loginName == message->timeoutUser && + s->flags.hasNone({MessageFlag::Disabled, MessageFlag::Timeout, + MessageFlag::Untimeout})) + { + break; + } + } + + if (s->flags.has(MessageFlag::Timeout) && + s->timeoutUser == message->timeoutUser) + { + if (message->flags.has(MessageFlag::PubSub) && + !s->flags.has(MessageFlag::PubSub)) + { + replaceMessage(i, s, message); + shouldAddMessage = false; + break; + } + if (!message->flags.has(MessageFlag::PubSub) && + s->flags.has(MessageFlag::PubSub)) + { + shouldAddMessage = + timeoutStackStyle == TimeoutStackStyle::DontStack; + break; + } + + uint32_t count = s->count + 1; + + MessageBuilder replacement(timeoutMessage, message->timeoutUser, + message->loginName, message->searchText, + count); + + replacement->timeoutUser = message->timeoutUser; + replacement->count = count; + replacement->flags = message->flags; + + replaceMessage(i, s, replacement.release()); + + shouldAddMessage = false; + break; + } + } + + // disable the messages from the user + if (disableUserMessages) + { + for (qsizetype i = 0; i < snapshotLength; i++) + { + auto &s = buffer[i]; + if (s->loginName == message->timeoutUser && + s->flags.hasNone({MessageFlag::Timeout, MessageFlag::Untimeout, + MessageFlag::Whisper})) + { + // FOURTF: disabled for now + // PAJLADA: Shitty solution described in Message.hpp + s->flags.set(MessageFlag::Disabled); + } + } + } + + if (shouldAddMessage) + { + addMessage(message); + } +} + +} // namespace chatterino diff --git a/src/util/Clamp.hpp b/src/util/Clamp.hpp deleted file mode 100644 index 33030f70a..000000000 --- a/src/util/Clamp.hpp +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once - -namespace chatterino { - -// http://en.cppreference.com/w/cpp/algorithm/clamp - -template -constexpr const T &clamp(const T &v, const T &lo, const T &hi) -{ - return assert(!(hi < lo)), (v < lo) ? lo : (hi < v) ? hi : v; -} - -} // namespace chatterino diff --git a/src/util/Clipboard.cpp b/src/util/Clipboard.cpp index b68754223..9f28d12b9 100644 --- a/src/util/Clipboard.cpp +++ b/src/util/Clipboard.cpp @@ -1,13 +1,13 @@ #include "util/Clipboard.hpp" -#include +#include #include namespace chatterino { void crossPlatformCopy(const QString &text) { - auto clipboard = QApplication::clipboard(); + auto *clipboard = QApplication::clipboard(); clipboard->setText(text); diff --git a/src/util/ConcurrentMap.hpp b/src/util/ConcurrentMap.hpp deleted file mode 100644 index 0346bcae6..000000000 --- a/src/util/ConcurrentMap.hpp +++ /dev/null @@ -1,102 +0,0 @@ -#pragma once - -#include -#include -#include - -#include -#include -#include - -namespace chatterino { - -template -class ConcurrentMap -{ -public: - ConcurrentMap() = default; - - bool tryGet(const TKey &name, TValue &value) const - { - QMutexLocker lock(&this->mutex_); - - auto a = this->data_.find(name); - if (a == this->data_.end()) - { - return false; - } - - value = a.value(); - - return true; - } - - TValue getOrAdd(const TKey &name, std::function addLambda) - { - QMutexLocker lock(&this->mutex_); - - auto a = this->data_.find(name); - if (a == this->data_.end()) - { - TValue value = addLambda(); - this->data_.insert(name, value); - return value; - } - - return a.value(); - } - - TValue &operator[](const TKey &name) - { - QMutexLocker lock(&this->mutex_); - - return this->data_[name]; - } - - void clear() - { - QMutexLocker lock(&this->mutex_); - - this->data_.clear(); - } - - void insert(const TKey &name, const TValue &value) - { - QMutexLocker lock(&this->mutex_); - - this->data_.insert(name, value); - } - - void each( - std::function func) const - { - QMutexLocker lock(&this->mutex_); - - QMapIterator it(this->data_); - - while (it.hasNext()) - { - it.next(); - func(it.key(), it.value()); - } - } - - void each(std::function func) - { - QMutexLocker lock(&this->mutex_); - - QMutableMapIterator it(this->data_); - - while (it.hasNext()) - { - it.next(); - func(it.key(), it.value()); - } - } - -private: - mutable QMutex mutex_; - QMap data_; -}; - -} // namespace chatterino diff --git a/src/util/DebugCount.cpp b/src/util/DebugCount.cpp index 331aa605a..1c3906782 100644 --- a/src/util/DebugCount.cpp +++ b/src/util/DebugCount.cpp @@ -1,7 +1,110 @@ -#include "DebugCount.hpp" +#include "util/DebugCount.hpp" + +#include "common/UniqueAccess.hpp" + +#include +#include + +#include + +namespace { + +using namespace chatterino; + +struct Count { + int64_t value = 0; + DebugCount::Flags flags = DebugCount::Flag::None; +}; + +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +UniqueAccess> COUNTS; + +} // namespace namespace chatterino { -UniqueAccess> DebugCount::counts_; +void DebugCount::configure(const QString &name, Flags flags) +{ + auto counts = COUNTS.access(); + + auto it = counts->find(name); + if (it == counts->end()) + { + counts->emplace(name, Count{.flags = flags}); + } + else + { + it->second.flags = flags; + } +} + +void DebugCount::set(const QString &name, const int64_t &amount) +{ + auto counts = COUNTS.access(); + + auto it = counts->find(name); + if (it == counts->end()) + { + counts->emplace(name, Count{amount}); + } + else + { + it->second.value = amount; + } +} + +void DebugCount::increase(const QString &name, const int64_t &amount) +{ + auto counts = COUNTS.access(); + + auto it = counts->find(name); + if (it == counts->end()) + { + counts->emplace(name, Count{amount}); + } + else + { + it->second.value += amount; + } +} + +void DebugCount::decrease(const QString &name, const int64_t &amount) +{ + auto counts = COUNTS.access(); + + auto it = counts->find(name); + if (it == counts->end()) + { + counts->emplace(name, Count{-amount}); + } + else + { + it->second.value -= amount; + } +} + +QString DebugCount::getDebugText() +{ + static const QLocale locale(QLocale::English); + + auto counts = COUNTS.access(); + + QString text; + for (const auto &[key, count] : *counts) + { + QString formatted; + if (count.flags.has(Flag::DataSize)) + { + formatted = locale.formattedDataSize(count.value); + } + else + { + formatted = locale.toString(static_cast(count.value)); + } + + text += key % ": " % formatted % '\n'; + } + return text; +} } // namespace chatterino diff --git a/src/util/DebugCount.hpp b/src/util/DebugCount.hpp index 99f70217e..ef2363984 100644 --- a/src/util/DebugCount.hpp +++ b/src/util/DebugCount.hpp @@ -1,11 +1,7 @@ #pragma once -#include "common/UniqueAccess.hpp" +#include "common/FlagsEnum.hpp" -#include -#include - -#include #include namespace chatterino { @@ -13,83 +9,30 @@ namespace chatterino { class DebugCount { public: + enum class Flag : uint16_t { + None = 0, + /// The value is a data size in bytes + DataSize = 1 << 0, + }; + using Flags = FlagsEnum; + + static void configure(const QString &name, Flags flags); + + static void set(const QString &name, const int64_t &amount); + + static void increase(const QString &name, const int64_t &amount); static void increase(const QString &name) { - auto counts = counts_.access(); - - auto it = counts->find(name); - if (it == counts->end()) - { - counts->insert(name, 1); - } - else - { - reinterpret_cast(it.value())++; - } - } - static void increase(const QString &name, const int64_t &amount) - { - auto counts = counts_.access(); - - auto it = counts->find(name); - if (it == counts->end()) - { - counts->insert(name, amount); - } - else - { - reinterpret_cast(it.value()) += amount; - } + DebugCount::increase(name, 1); } + static void decrease(const QString &name, const int64_t &amount); static void decrease(const QString &name) { - auto counts = counts_.access(); - - auto it = counts->find(name); - if (it == counts->end()) - { - counts->insert(name, -1); - } - else - { - reinterpret_cast(it.value())--; - } - } - static void decrease(const QString &name, const int64_t &amount) - { - auto counts = counts_.access(); - - auto it = counts->find(name); - if (it == counts->end()) - { - counts->insert(name, -amount); - } - else - { - reinterpret_cast(it.value()) -= amount; - } + DebugCount::decrease(name, 1); } - static QString getDebugText() - { - auto counts = counts_.access(); - - QString text; - for (auto it = counts->begin(); it != counts->end(); it++) - { - text += it.key() + ": " + QString::number(it.value()) + "\n"; - } - return text; - } - - QString toString() - { - return ""; - } - -private: - static UniqueAccess> counts_; + static QString getDebugText(); }; } // namespace chatterino diff --git a/src/util/DisplayBadge.cpp b/src/util/DisplayBadge.cpp index cf56634fa..733633339 100644 --- a/src/util/DisplayBadge.cpp +++ b/src/util/DisplayBadge.cpp @@ -1,4 +1,4 @@ -#include "DisplayBadge.hpp" +#include "util/DisplayBadge.hpp" namespace chatterino { DisplayBadge::DisplayBadge(QString displayName, QString badgeName) diff --git a/src/util/DisplayBadge.hpp b/src/util/DisplayBadge.hpp index e07e63bbb..69c23bdb7 100644 --- a/src/util/DisplayBadge.hpp +++ b/src/util/DisplayBadge.hpp @@ -3,6 +3,7 @@ #include namespace chatterino { + class DisplayBadge { public: diff --git a/src/util/DistanceBetweenPoints.hpp b/src/util/DistanceBetweenPoints.hpp index 7a2a834cc..332aee87e 100644 --- a/src/util/DistanceBetweenPoints.hpp +++ b/src/util/DistanceBetweenPoints.hpp @@ -6,15 +6,13 @@ namespace chatterino { -inline float distanceBetweenPoints(const QPointF &p1, const QPointF &p2) +inline qreal distanceBetweenPoints(const QPointF &p1, const QPointF &p2) { QPointF tmp = p1 - p2; - float distance = 0.f; - distance += tmp.x() * tmp.x(); - distance += tmp.y() * tmp.y(); + qreal distance = tmp.x() * tmp.x() + tmp.y() * tmp.y(); - return sqrt(distance); + return std::sqrt(distance); } } // namespace chatterino diff --git a/src/util/ExponentialBackoff.hpp b/src/util/ExponentialBackoff.hpp index 44eca67e1..bc7e1ec91 100644 --- a/src/util/ExponentialBackoff.hpp +++ b/src/util/ExponentialBackoff.hpp @@ -1,6 +1,5 @@ #pragma once -#include #include namespace chatterino { @@ -20,7 +19,6 @@ public: **/ ExponentialBackoff(const std::chrono::milliseconds &start) : start_(start) - , step_{1} { static_assert(maxSteps > 1, "maxSteps must be higher than 1"); } @@ -54,7 +52,7 @@ public: private: const std::chrono::milliseconds start_; - unsigned step_; + unsigned step_ = 1; }; } // namespace chatterino diff --git a/src/util/FormatTime.cpp b/src/util/FormatTime.cpp index 6d6f2e525..13b64349c 100644 --- a/src/util/FormatTime.cpp +++ b/src/util/FormatTime.cpp @@ -1,4 +1,7 @@ -#include "FormatTime.hpp" +#include "util/FormatTime.hpp" + +#include +#include namespace chatterino { @@ -45,7 +48,7 @@ QString formatTime(int totalSeconds) return res; } -QString formatTime(QString totalSecondsString) +QString formatTime(const QString &totalSecondsString) { bool ok = true; int totalSeconds(totalSecondsString.toInt(&ok)); @@ -57,4 +60,15 @@ QString formatTime(QString totalSecondsString) return "n/a"; } +QString formatTime(std::chrono::seconds totalSeconds) +{ + auto count = totalSeconds.count(); + + return formatTime(static_cast(std::clamp( + count, + static_cast(std::numeric_limits::min()), + static_cast( + std::numeric_limits::max())))); +} + } // namespace chatterino diff --git a/src/util/FormatTime.hpp b/src/util/FormatTime.hpp index 0e4eb2725..cb26cc9ca 100644 --- a/src/util/FormatTime.hpp +++ b/src/util/FormatTime.hpp @@ -2,10 +2,13 @@ #include +#include + namespace chatterino { // format: 1h 23m 42s QString formatTime(int totalSeconds); -QString formatTime(QString totalSecondsString); +QString formatTime(const QString &totalSecondsString); +QString formatTime(std::chrono::seconds totalSeconds); } // namespace chatterino diff --git a/src/util/FunctionEventFilter.cpp b/src/util/FunctionEventFilter.cpp index 5071e6e9b..33a41a2c6 100644 --- a/src/util/FunctionEventFilter.cpp +++ b/src/util/FunctionEventFilter.cpp @@ -1,4 +1,4 @@ -#include "FunctionEventFilter.hpp" +#include "util/FunctionEventFilter.hpp" namespace chatterino { diff --git a/src/util/FunctionEventFilter.hpp b/src/util/FunctionEventFilter.hpp index bde2b51f3..d76ef8520 100644 --- a/src/util/FunctionEventFilter.hpp +++ b/src/util/FunctionEventFilter.hpp @@ -2,6 +2,7 @@ #include #include + #include namespace chatterino { diff --git a/src/util/FuzzyConvert.cpp b/src/util/FuzzyConvert.cpp index 2d62e5648..0870b711a 100644 --- a/src/util/FuzzyConvert.cpp +++ b/src/util/FuzzyConvert.cpp @@ -1,4 +1,4 @@ -#include "FuzzyConvert.hpp" +#include "util/FuzzyConvert.hpp" #include diff --git a/src/util/Helpers.cpp b/src/util/Helpers.cpp index 467b9ecb1..88ba17fe1 100644 --- a/src/util/Helpers.cpp +++ b/src/util/Helpers.cpp @@ -1,13 +1,118 @@ -#include "Helpers.hpp" +#include "util/Helpers.hpp" #include "providers/twitch/TwitchCommon.hpp" #include #include +#include #include namespace chatterino { +namespace helpers::detail { + + SizeType skipSpace(QStringView view, SizeType startPos) + { + while (startPos < view.length() && view.at(startPos).isSpace()) + { + startPos++; + } + return startPos - 1; + } + + bool matchesIgnorePlural(QStringView word, const QString &expected) + { + if (!word.startsWith(expected)) + { + return false; + } + if (word.length() == expected.length()) + { + return true; + } + return word.length() == expected.length() + 1 && + word.at(word.length() - 1).toLatin1() == 's'; + } + + std::pair findUnitMultiplierToSec(QStringView view, + SizeType &pos) + { + // Step 1. find end of unit + auto startIdx = pos; + auto endIdx = view.length(); + for (; pos < view.length(); pos++) + { + auto c = view.at(pos); + if (c.isSpace() || c.isDigit()) + { + endIdx = pos; + break; + } + } + pos--; + + // TODO(QT6): use sliced (more readable) + auto unit = view.mid(startIdx, endIdx - startIdx); + if (unit.isEmpty()) + { + return std::make_pair(0, false); + } + + auto first = unit.at(0).toLatin1(); + switch (first) + { + case 's': { + if (unit.length() == 1 || + matchesIgnorePlural(unit, QStringLiteral("second"))) + { + return std::make_pair(1, true); + } + } + break; + case 'm': { + if (unit.length() == 1 || + matchesIgnorePlural(unit, QStringLiteral("minute"))) + { + return std::make_pair(60, true); + } + if ((unit.length() == 2 && unit.at(1).toLatin1() == 'o') || + matchesIgnorePlural(unit, QStringLiteral("month"))) + { + return std::make_pair(60 * 60 * 24 * 30, true); + } + } + break; + case 'h': { + if (unit.length() == 1 || + matchesIgnorePlural(unit, QStringLiteral("hour"))) + { + return std::make_pair(60 * 60, true); + } + } + break; + case 'd': { + if (unit.length() == 1 || + matchesIgnorePlural(unit, QStringLiteral("day"))) + { + return std::make_pair(60 * 60 * 24, true); + } + } + break; + case 'w': { + if (unit.length() == 1 || + matchesIgnorePlural(unit, QStringLiteral("week"))) + { + return std::make_pair(60 * 60 * 24 * 7, true); + } + } + break; + } + return std::make_pair(0, false); + } + +} // namespace helpers::detail +using namespace helpers::detail; + bool startsWithOrContains(const QString &str1, const QString &str2, Qt::CaseSensitivity caseSensitivity, bool startsWith) { @@ -19,6 +124,13 @@ bool startsWithOrContains(const QString &str1, const QString &str2, return str1.contains(str2, caseSensitivity); } +bool isNeutral(const QString &s) +{ + static const QRegularExpression re("\\p{L}"); + const QRegularExpressionMatch match = re.match(s); + return !match.hasMatch(); +} + QString generateUuid() { auto uuid = QUuid::createUuid(); @@ -50,12 +162,6 @@ QString shortenString(const QString &str, unsigned maxWidth) return shortened; } -QString localizeNumbers(const int &number) -{ - QLocale locale; - return locale.toString(number); -} - QString kFormatNumbers(const int &number) { return QString("%1K").arg(number / 1000); @@ -93,4 +199,88 @@ QString formatUserMention(const QString &userName, bool isFirstWord, return result; } +int64_t parseDurationToSeconds(const QString &inputString, + uint64_t noUnitMultiplier) +{ + if (inputString.length() == 0) + { + return -1; + } + + QStringView input(inputString); + input = input.trimmed(); + + uint64_t currentValue = 0; + + bool visitingNumber = true; // input must start with a number + SizeType numberStartIdx = 0; + + for (SizeType pos = 0; pos < input.length(); pos++) + { + QChar c = input.at(pos); + + if (visitingNumber && !c.isDigit()) + { + uint64_t parsed = + (uint64_t)input.mid(numberStartIdx, pos - numberStartIdx) + .toUInt(); + + if (c.isSpace()) + { + pos = skipSpace(input, pos) + 1; + if (pos >= input.length()) + { + // input like "40 ", this shouldn't happen + // since we trimmed the view + return -1; + } + c = input.at(pos); + } + + auto result = findUnitMultiplierToSec(input, pos); + if (!result.second) + { + return -1; // invalid unit or leading spaces (shouldn't happen) + } + + currentValue += parsed * result.first; + visitingNumber = false; + } + else if (!visitingNumber && !c.isSpace()) + { + if (!c.isDigit()) + { + return -1; // expected a digit + } + visitingNumber = true; + numberStartIdx = pos; + } + // else: visitingNumber && isDigit || !visitingNumber && isSpace + } + + if (visitingNumber) + { + if (numberStartIdx != 0) + { + return -1; // input like "1w 3s 70", 70 what? apples? + } + currentValue += input.toUInt() * noUnitMultiplier; + } + + return (int64_t)currentValue; +} + +bool compareEmoteStrings(const QString &a, const QString &b) +{ + // try comparing insensitively, if they are the same then sensitively + // (fixes order of LuL and LUL) + int k = QString::compare(a, b, Qt::CaseInsensitive); + if (k == 0) + { + return a > b; + } + + return k < 0; +} + } // namespace chatterino diff --git a/src/util/Helpers.hpp b/src/util/Helpers.hpp index 8e5bd6a9d..d499e5fc0 100644 --- a/src/util/Helpers.hpp +++ b/src/util/Helpers.hpp @@ -1,12 +1,60 @@ #pragma once #include +#include #include #include +#include +#include +#include namespace chatterino { +// only qualified for tests +namespace helpers::detail { + + using SizeType = QStringView::size_type; + + /** + * Skips all spaces. + * The caller must guarantee view.at(startPos).isSpace(). + * + * @param view The string to skip spaces in. + * @param startPos The starting position (there must be a space in the view). + * @return The position of the last space. + */ + SizeType skipSpace(QStringView view, SizeType startPos); + + /** + * Checks if `word` equals `expected` (singular) or `expected` + 's' (plural). + * + * @param word Word to test. Must not be empty. + * @param expected Singular of the expected word. + * @return true if `word` is singular or plural of `expected`. + */ + bool matchesIgnorePlural(QStringView word, const QString &expected); + + /** + * Tries to find the unit starting at `pos` and returns its multiplier so + * `valueInUnit * multiplier = valueInSeconds` (e.g. 60 for minutes). + * + * Supported units are + * 'w[eek(s)]', 'd[ay(s)]', + * 'h[our(s)]', 'm[inute(s)]', 's[econd(s)]'. + * The unit must be in lowercase. + * + * @param view A view into a string + * @param pos The starting position. + * This is set to the last position of the unit + * if it's a valid unit, undefined otherwise. + * @return (multiplier, ok) + */ + std::pair findUnitMultiplierToSec(QStringView view, + SizeType &pos); + +} // namespace helpers::detail + /** * @brief startsWithOrContains is a wrapper for checking * whether str1 starts with or contains str2 within itself @@ -14,6 +62,11 @@ namespace chatterino { bool startsWithOrContains(const QString &str1, const QString &str2, Qt::CaseSensitivity caseSensitivity, bool startsWith); +/** + * @brief isNeutral checks if the string doesn't contain any character in the unicode "letter" category + * i.e. if the string contains only neutral characters. + **/ +bool isNeutral(const QString &s); QString generateUuid(); QString formatRichLink(const QString &url, bool file = false); @@ -23,12 +76,48 @@ QString formatRichNamedLink(const QString &url, const QString &name, QString shortenString(const QString &str, unsigned maxWidth = 50); -QString localizeNumbers(const int &number); +template +QString localizeNumbers(T number) +{ + QLocale locale; + return locale.toString(number); +} QString kFormatNumbers(const int &number); QColor getRandomColor(const QString &userId); +/** + * Parses a duration. + * Spaces are allowed before and after a unit but not mandatory. + * Supported units are + * 'w[eek(s)]', 'd[ay(s)]', + * 'h[our(s)]', 'm[inute(s)]', 's[econd(s)]'. + * Units must be lowercase. + * + * If the entire input string is a number (e.g. "12345"), + * then it's multiplied by noUnitMultiplier. + * + * Examples: + * + * - "1w 2h" + * - "1w 1w 0s 4d" (2weeks, 4days) + * - "5s3h4w" (4weeks, 3hours, 5seconds) + * - "30m" + * - "1 week" + * - "5 days 12 hours" + * - "10" (10 * noUnitMultiplier seconds) + * + * @param inputString A non-empty string to parse + * @param noUnitMultiplier A multiplier if the input string only contains one number. + * For example, if a number without a unit should be interpreted + * as a minute, set this to 60. If it should be interpreted + * as a second, set it to 1 (default). + * @return The parsed duration in seconds, -1 if the input is invalid. + */ +int64_t parseDurationToSeconds(const QString &inputString, + uint64_t noUnitMultiplier = 1); + /** * @brief Takes a user's name and some formatting parameter and spits out the standardized way to format it * @@ -67,4 +156,30 @@ std::vector splitListIntoBatches(const T &list, int batchSize = 100) return batches; } +bool compareEmoteStrings(const QString &a, const QString &b); + +template +constexpr std::optional makeConditionedOptional(bool condition, + const T &value) +{ + if (condition) + { + return value; + } + + return std::nullopt; +} + +template +constexpr std::optional> makeConditionedOptional(bool condition, + T &&value) +{ + if (condition) + { + return std::optional>(std::forward(value)); + } + + return std::nullopt; +} + } // namespace chatterino diff --git a/src/util/IncognitoBrowser.cpp b/src/util/IncognitoBrowser.cpp index 24e79a7e7..3d147b6f7 100644 --- a/src/util/IncognitoBrowser.cpp +++ b/src/util/IncognitoBrowser.cpp @@ -1,99 +1,121 @@ -#include "IncognitoBrowser.hpp" +#include "util/IncognitoBrowser.hpp" +#ifdef USEWINSDK +# include "util/WindowsHelper.hpp" +#elif defined(Q_OS_UNIX) and !defined(Q_OS_DARWIN) +# include "util/XDGHelper.hpp" +#endif #include -#include -#include #include -namespace chatterino { namespace { -#ifdef Q_OS_WIN - QString injectPrivateSwitch(QString command) + +using namespace chatterino; + +QString getPrivateSwitch(const QString &browserExecutable) +{ + // list of command line switches to turn on private browsing in browsers + static auto switches = std::vector>{ + {"firefox", "-private-window"}, + {"librewolf", "-private-window"}, + {"waterfox", "-private-window"}, + {"icecat", "-private-window"}, + {"chrome", "-incognito"}, + {"google-chrome-stable", "-incognito"}, + {"vivaldi", "-incognito"}, + {"opera", "-newprivatetab"}, + {"opera\\launcher", "--private"}, + {"iexplore", "-private"}, + {"msedge", "-inprivate"}, + {"firefox-esr", "-private-window"}, + {"chromium", "-incognito"}, + {"brave", "-incognito"}, + {"firefox-devedition", "-private-window"}, + {"firefox-developer-edition", "-private-window"}, + {"firefox-beta", "-private-window"}, + {"firefox-nightly", "-private-window"}, + }; + + // compare case-insensitively + auto lowercasedBrowserExecutable = browserExecutable.toLower(); + +#ifdef Q_OS_WINDOWS + if (lowercasedBrowserExecutable.endsWith(".exe")) { - // list of command line switches to turn on private browsing in browsers - static auto switches = std::vector>{ - {"firefox", "-private-window"}, {"librewolf", "-private-window"}, - {"waterfox", "-private-window"}, {"icecat", "-private-window"}, - {"chrome", "-incognito"}, {"vivaldi", "-incognito"}, - {"opera", "-newprivatetab"}, {"opera\\\\launcher", "--private"}, - {"iexplore", "-private"}, {"msedge", "-inprivate"}, - }; - - // transform into regex and replacement string - std::vector> replacers; - for (const auto &switch_ : switches) - { - replacers.emplace_back( - QRegularExpression("(" + switch_.first + "\\.exe\"?).*", - QRegularExpression::CaseInsensitiveOption), - "\\1 " + switch_.second); - } - - // try to find matching regex and apply it - for (const auto &replacement : replacers) - { - if (replacement.first.match(command).hasMatch()) - { - command.replace(replacement.first, replacement.second); - return command; - } - } - - // couldn't match any browser -> unknown browser - return QString(); - } - - QString getCommand(const QString &link) - { - // get default browser prog id - auto browserId = QSettings("HKEY_CURRENT_" - "USER\\Software\\Microsoft\\Windows\\Shell\\" - "Associations\\UrlAssociatio" - "ns\\http\\UserChoice", - QSettings::NativeFormat) - .value("Progid") - .toString(); - - // get default browser start command - auto command = QSettings("HKEY_CLASSES_ROOT\\" + browserId + - "\\shell\\open\\command", - QSettings::NativeFormat) - .value("Default") - .toString(); - if (command.isNull()) - return QString(); - - // inject switch to enable private browsing - command = injectPrivateSwitch(command); - if (command.isNull()) - return QString(); - - // link - command += " " + link; - - return command; + lowercasedBrowserExecutable.chop(4); } #endif + + for (const auto &switch_ : switches) + { + if (lowercasedBrowserExecutable.endsWith(switch_.first)) + { + return switch_.second; + } + } + + // couldn't match any browser -> unknown browser + return {}; +} + +QString getDefaultBrowserExecutable() +{ +#ifdef USEWINSDK + // get default browser start command, by protocol if possible, falling back to extension if not + QString command = + getAssociatedExecutable(AssociationQueryType::Protocol, L"http"); + + if (command.isNull()) + { + // failed to fetch default browser by protocol, try by file extension instead + command = getAssociatedExecutable(AssociationQueryType::FileExtension, + L".html"); + } + + if (command.isNull()) + { + // also try the equivalent .htm extension + command = getAssociatedExecutable(AssociationQueryType::FileExtension, + L".htm"); + } + + return command; +#elif defined(Q_OS_UNIX) && !defined(Q_OS_DARWIN) + static QString defaultBrowser = []() -> QString { + auto desktopFile = getDefaultBrowserDesktopFile(); + if (desktopFile.has_value()) + { + auto entry = desktopFile->getEntries("Desktop Entry"); + auto exec = entry.find("Exec"); + if (exec != entry.end()) + { + return parseDesktopExecProgram(exec->second.trimmed()); + } + } + return {}; + }(); + + return defaultBrowser; +#else + return {}; +#endif +} + } // namespace +namespace chatterino { + bool supportsIncognitoLinks() { -#ifdef Q_OS_WIN - return !getCommand("").isNull(); -#else - return false; -#endif + auto browserExe = getDefaultBrowserExecutable(); + return !browserExe.isNull() && !getPrivateSwitch(browserExe).isNull(); } bool openLinkIncognito(const QString &link) { -#ifdef Q_OS_WIN - auto command = getCommand(link); - - return QProcess::startDetached(command); -#else - return false; -#endif + auto browserExe = getDefaultBrowserExecutable(); + return QProcess::startDetached(browserExe, + {getPrivateSwitch(browserExe), link}); } } // namespace chatterino diff --git a/src/util/IncognitoBrowser.hpp b/src/util/IncognitoBrowser.hpp index aa5dbec83..9db1e800f 100644 --- a/src/util/IncognitoBrowser.hpp +++ b/src/util/IncognitoBrowser.hpp @@ -1,6 +1,6 @@ #pragma once -#include +#include namespace chatterino { diff --git a/src/util/InitUpdateButton.cpp b/src/util/InitUpdateButton.cpp index a8ff6bc64..8636a85f1 100644 --- a/src/util/InitUpdateButton.cpp +++ b/src/util/InitUpdateButton.cpp @@ -1,5 +1,6 @@ -#include "InitUpdateButton.hpp" +#include "util/InitUpdateButton.hpp" +#include "Application.hpp" #include "widgets/dialogs/UpdateDialog.hpp" #include "widgets/helper/Button.hpp" @@ -12,7 +13,7 @@ void initUpdateButton(Button &button, // show update prompt when clicking the button QObject::connect(&button, &Button::leftClicked, [&button] { - auto dialog = new UpdateDialog(); + auto *dialog = new UpdateDialog(); dialog->setActionOnFocusLoss(BaseWindow::Delete); auto globalPoint = button.mapToGlobal( @@ -24,11 +25,15 @@ void initUpdateButton(Button &button, globalPoint.setX(0); } - dialog->move(globalPoint); + dialog->moveTo(globalPoint, widgets::BoundsChecking::DesiredPosition); dialog->show(); dialog->raise(); - dialog->buttonClicked.connect([&button](auto buttonType) { + // We can safely ignore the signal connection because the dialog will always + // be destroyed before the button is destroyed, since it is destroyed on focus loss + // + // The button is either attached to a Notebook, or a Window frame + std::ignore = dialog->buttonClicked.connect([&button](auto buttonType) { switch (buttonType) { case UpdateDialog::Dismiss: { @@ -36,7 +41,7 @@ void initUpdateButton(Button &button, } break; case UpdateDialog::Install: { - Updates::instance().installUpdates(); + getApp()->getUpdates().installUpdates(); } break; } @@ -48,17 +53,17 @@ void initUpdateButton(Button &button, // update image when state changes auto updateChange = [&button](auto) { - button.setVisible(Updates::instance().shouldShowUpdateButton()); + button.setVisible(getApp()->getUpdates().shouldShowUpdateButton()); - auto imageUrl = Updates::instance().isError() - ? ":/buttons/updateError.png" - : ":/buttons/update.png"; + const auto *imageUrl = getApp()->getUpdates().isError() + ? ":/buttons/updateError.png" + : ":/buttons/update.png"; button.setPixmap(QPixmap(imageUrl)); }; - updateChange(Updates::instance().getStatus()); + updateChange(getApp()->getUpdates().getStatus()); - signalHolder.managedConnect(Updates::instance().statusUpdated, + signalHolder.managedConnect(getApp()->getUpdates().statusUpdated, [updateChange](auto status) { updateChange(status); }); diff --git a/src/util/IpcQueue.cpp b/src/util/IpcQueue.cpp new file mode 100644 index 000000000..397386974 --- /dev/null +++ b/src/util/IpcQueue.cpp @@ -0,0 +1,135 @@ +#include "util/IpcQueue.hpp" + +#include "common/QLogging.hpp" +#include "singletons/Paths.hpp" + +#define BOOST_INTERPROCESS_SHARED_DIR_FUNC +#include +#include +#include +#include + +namespace boost_ipc = boost::interprocess; + +namespace { + +static const chatterino::Paths *PATHS = nullptr; + +} // namespace + +namespace boost::interprocess::ipcdetail { + +void get_shared_dir(std::string &shared_dir) +{ + if (!PATHS) + { + assert(false && "PATHS not set"); + qCCritical(chatterinoNativeMessage) + << "PATHS not set for shared directory"; + return; + } + shared_dir = PATHS->ipcDirectory.toStdString(); +} + +#ifdef BOOST_INTERPROCESS_WINDOWS +void get_shared_dir(std::wstring &shared_dir) +{ + if (!PATHS) + { + assert(false && "PATHS not set"); + qCCritical(chatterinoNativeMessage) + << "PATHS not set for shared directory"; + return; + } + shared_dir = PATHS->ipcDirectory.toStdWString(); +} +#endif + +} // namespace boost::interprocess::ipcdetail + +namespace chatterino::ipc { + +void initPaths(const Paths *paths) +{ + PATHS = paths; +} + +void sendMessage(const char *name, const QByteArray &data) +{ + try + { + boost_ipc::message_queue messageQueue(boost_ipc::open_only, name); + + messageQueue.try_send(data.data(), size_t(data.size()), 1); + } + catch (boost_ipc::interprocess_exception &ex) + { + qCDebug(chatterinoNativeMessage) + << "Failed to send message:" << ex.what(); + } +} + +class IpcQueuePrivate +{ +public: + IpcQueuePrivate(const char *name, size_t maxMessages, size_t maxMessageSize) + : queue(boost_ipc::open_or_create, name, maxMessages, maxMessageSize) + { + } + + boost_ipc::message_queue queue; +}; + +IpcQueue::IpcQueue(IpcQueuePrivate *priv) + : private_(priv){}; +IpcQueue::~IpcQueue() = default; + +std::pair, QString> IpcQueue::tryReplaceOrCreate( + const char *name, size_t maxMessages, size_t maxMessageSize) +{ + try + { + boost_ipc::message_queue::remove(name); + return std::make_pair( + std::unique_ptr(new IpcQueue( + new IpcQueuePrivate(name, maxMessages, maxMessageSize))), + QString()); + } + catch (boost_ipc::interprocess_exception &ex) + { + return {nullptr, QString::fromLatin1(ex.what())}; + } +} + +bool IpcQueue::remove(const char *name) +{ + return boost_ipc::message_queue::remove(name); +} + +QByteArray IpcQueue::receive() +{ + try + { + auto *d = this->private_.get(); + + QByteArray buf; + // The new storage is uninitialized + buf.resize(static_cast(d->queue.get_max_msg_size())); + + size_t messageSize = 0; + unsigned int priority = 0; + d->queue.receive(buf.data(), buf.size(), messageSize, priority); + + // truncate to the initialized storage + buf.truncate(static_cast(messageSize)); + return buf; + } + catch (boost_ipc::interprocess_exception &ex) + { + qCDebug(chatterinoNativeMessage) + << "Failed to receive message:" << ex.what(); + } + return {}; +} + +} // namespace chatterino::ipc diff --git a/src/util/IpcQueue.hpp b/src/util/IpcQueue.hpp new file mode 100644 index 000000000..059d15c6e --- /dev/null +++ b/src/util/IpcQueue.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include +#include + +class QByteArray; +class QString; + +namespace chatterino { + +class Paths; + +} // namespace chatterino + +namespace chatterino::ipc { + +void initPaths(const Paths *paths); + +void sendMessage(const char *name, const QByteArray &data); + +class IpcQueuePrivate; +class IpcQueue +{ +public: + ~IpcQueue(); + + static std::pair, QString> tryReplaceOrCreate( + const char *name, size_t maxMessages, size_t maxMessageSize); + + static bool remove(const char *name); + + // TODO: use std::expected + /// Try to receive a message. + /// In the case of an error, the buffer is empty. + QByteArray receive(); + +private: + IpcQueue(IpcQueuePrivate *priv); + + std::unique_ptr private_; + + friend class IpcQueuePrivate; +}; + +} // namespace chatterino::ipc diff --git a/src/util/IrcHelpers.hpp b/src/util/IrcHelpers.hpp index faa3fd71a..52691b350 100644 --- a/src/util/IrcHelpers.hpp +++ b/src/util/IrcHelpers.hpp @@ -2,6 +2,7 @@ #include #include +#include namespace chatterino { @@ -88,7 +89,7 @@ inline QDateTime calculateMessageTime(const Communi::IrcMessage *message) QString timedate = message->tags().value("time").toString(); auto date = QDateTime::fromString(timedate, Qt::ISODate); - date.setTimeSpec(Qt::TimeSpec::UTC); + date.setTimeZone(QTimeZone::utc()); return date.toLocalTime(); } @@ -96,4 +97,17 @@ inline QDateTime calculateMessageTime(const Communi::IrcMessage *message) return QDateTime::currentDateTime(); } +// "foo/bar/baz,tri/hard" can be a valid badge-info tag +// In that case, valid map content should be 'split by slash' only once: +// {"foo": "bar/baz", "tri": "hard"} +inline std::pair slashKeyValue(const QString &kvStr) +{ + return { + // part before first slash (index 0 of section) + kvStr.section('/', 0, 0), + // part after first slash (index 1 of section) + kvStr.section('/', 1, -1), + }; +} + } // namespace chatterino diff --git a/src/util/LayoutCreator.hpp b/src/util/LayoutCreator.hpp index 82f729d26..41f605d47 100644 --- a/src/util/LayoutCreator.hpp +++ b/src/util/LayoutCreator.hpp @@ -6,11 +6,12 @@ #include #include -#include +#include namespace chatterino { template + requires std::derived_from || std::derived_from class LayoutCreator { public: @@ -34,49 +35,41 @@ public: return this->item_; } - template - LayoutCreator append(T2 *_item) + template + LayoutCreator append(U *item) { - this->addItem(this->getOrCreateLayout(), _item); + addItem(this->getOrCreateLayout(), item); - return LayoutCreator(_item); + return LayoutCreator(item); } - template - // clang-format off - // clang-format can be enabled once clang-format v11+ has been installed in CI - LayoutCreator emplace(Args &&...args) - // clang-format on + template + LayoutCreator emplace(auto &&...args) { - T2 *t = new T2(std::forward(args)...); + auto *t = new U(std::forward(args)...); - this->addItem(this->getOrCreateLayout(), t); + addItem(this->getOrCreateLayout(), t); - return LayoutCreator(t); + return LayoutCreator(t); } - template ::value, - int>::type = 0> LayoutCreator emplaceScrollAreaWidget() + requires std::derived_from { - QWidget *widget = new QWidget; + auto *widget = new QWidget; this->item_->setWidget(widget); - return LayoutCreator(widget); + return {widget}; } - template ::value, - int>::type = 0, - typename std::enable_if::value, - int>::type = 0> - LayoutCreator setLayoutType() + template U> + LayoutCreator setLayoutType() + requires std::derived_from { - T2 *layout = new T2; + U *layout = new U; this->item_->setLayout(layout); - return LayoutCreator(layout); + return LayoutCreator(layout); } LayoutCreator assign(T **ptr) @@ -86,9 +79,6 @@ public: return *this; } - template ::value, - int>::type = 0> LayoutCreator withoutMargin() { this->item_->setContentsMargins(0, 0, 0, 0); @@ -97,36 +87,31 @@ public: } LayoutCreator withoutSpacing() + requires std::derived_from { this->item_->setSpacing(0); return *this; } - template ::value, - int>::type = 0> LayoutCreator hidden() + requires std::derived_from { this->item_->setVisible(false); return *this; } - template ::value, - int>::type = 0> - LayoutCreator appendTab(T2 *item, const QString &title) + template U> + LayoutCreator appendTab(U *item, const QString &title) + requires std::derived_from { - static_assert(std::is_base_of::value, - "needs to be QLayout"); - - QWidget *widget = new QWidget; + auto *widget = new QWidget; widget->setLayout(item); this->item_->addTab(widget, title); - return LayoutCreator(item); + return LayoutCreator(item); } template @@ -146,36 +131,26 @@ public: private: T *item_; - template ::value, - int>::type = 0> - void addItem(QLayout *layout, T2 *item) + static void addItem(QLayout *layout, QWidget *item) { layout->addWidget(item); } - template ::value, - int>::type = 0> - void addItem(QLayout *layout, T2 *item) + static void addItem(QLayout *layout, QLayout *item) { - QWidget *widget = new QWidget(); + auto *widget = new QWidget(); widget->setLayout(item); layout->addWidget(widget); } - template ::value, - int>::type = 0> QLayout *getOrCreateLayout() + requires std::derived_from { return this->item_; } - template ::value, - int>::type = 0> QLayout *getOrCreateLayout() + requires std::derived_from { if (!this->item_->layout()) { @@ -186,13 +161,10 @@ private: } }; -template -// clang-format off -// clang-format can be enabled once clang-format v11+ has been installed in CI -LayoutCreator makeDialog(Args &&...args) -// clang-format on +template +LayoutCreator makeDialog(auto &&...args) { - T *t = new T(std::forward(args)...); + T *t = new T(std::forward(args)...); t->setAttribute(Qt::WA_DeleteOnClose); return LayoutCreator(t); } diff --git a/src/util/LayoutHelper.cpp b/src/util/LayoutHelper.cpp index 43987d7ef..c3a71690e 100644 --- a/src/util/LayoutHelper.cpp +++ b/src/util/LayoutHelper.cpp @@ -7,14 +7,14 @@ namespace chatterino { QWidget *wrapLayout(QLayout *layout) { - auto widget = new QWidget; + auto *widget = new QWidget; widget->setLayout(layout); return widget; } QScrollArea *makeScrollArea(WidgetOrLayout item) { - auto area = new QScrollArea(); + auto *area = new QScrollArea(); switch (item.which()) { diff --git a/src/util/LayoutHelper.hpp b/src/util/LayoutHelper.hpp index 1cf7f700b..d40bab0f3 100644 --- a/src/util/LayoutHelper.hpp +++ b/src/util/LayoutHelper.hpp @@ -1,7 +1,7 @@ #pragma once -#include #include +#include class QWidget; class QScrollArea; @@ -19,7 +19,7 @@ T *makeLayout(std::initializer_list items) { auto t = new T; - for (auto &item : items) + for (const auto &item : items) { switch (item.which()) { diff --git a/src/util/LoadPixmap.cpp b/src/util/LoadPixmap.cpp new file mode 100644 index 000000000..99fdf95f3 --- /dev/null +++ b/src/util/LoadPixmap.cpp @@ -0,0 +1,48 @@ +#include "util/LoadPixmap.hpp" + +#include "common/network/NetworkRequest.hpp" +#include "common/network/NetworkResult.hpp" +#include "common/QLogging.hpp" + +#include +#include +#include +#include + +namespace chatterino { + +void loadPixmapFromUrl(const Url &url, std::function &&callback) +{ + NetworkRequest(url.string) + .concurrent() + .cache() + .onSuccess( + [callback = std::move(callback), url](const NetworkResult &result) { + auto data = result.getData(); + QBuffer buffer(&data); + buffer.open(QIODevice::ReadOnly); + QImageReader reader(&buffer); + + if (!reader.canRead() || reader.size().isEmpty()) + { + qCWarning(chatterinoImage) + << "Can't read image file at" << url.string << ":" + << reader.errorString(); + return; + } + + QImage image = reader.read(); + if (image.isNull()) + { + qCWarning(chatterinoImage) + << "Failed reading image at" << url.string << ":" + << reader.errorString(); + return; + } + + callback(QPixmap::fromImage(image)); + }) + .execute(); +} + +} // namespace chatterino diff --git a/src/util/LoadPixmap.hpp b/src/util/LoadPixmap.hpp new file mode 100644 index 000000000..81fb11921 --- /dev/null +++ b/src/util/LoadPixmap.hpp @@ -0,0 +1,15 @@ +#pragma once +#include "common/Aliases.hpp" + +#include + +namespace chatterino { + +/** + * Loads an image from url into a QPixmap. Allows for file:// protocol links. Uses cacheing. + * + * @param callback The callback you will get the pixmap by. It will be invoked concurrently with no guarantees on which thread. + */ +void loadPixmapFromUrl(const Url &url, std::function &&callback); + +} // namespace chatterino diff --git a/src/util/NuulsUploader.cpp b/src/util/NuulsUploader.cpp deleted file mode 100644 index 606b38f14..000000000 --- a/src/util/NuulsUploader.cpp +++ /dev/null @@ -1,333 +0,0 @@ -#include "NuulsUploader.hpp" - -#include "common/Env.hpp" -#include "common/NetworkRequest.hpp" -#include "providers/twitch/TwitchMessageBuilder.hpp" -#include "singletons/Paths.hpp" -#include "singletons/Settings.hpp" -#include "util/CombinePath.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include "common/QLogging.hpp" - -#define UPLOAD_DELAY 2000 -// Delay between uploads in milliseconds - -namespace { - -boost::optional convertToPng(QImage image) -{ - QByteArray imageData; - QBuffer buf(&imageData); - buf.open(QIODevice::WriteOnly); - bool success = image.save(&buf, "png"); - if (success) - { - return boost::optional(imageData); - } - else - { - return boost::optional(boost::none); - } -} -} // namespace - -namespace chatterino { -// These variables are only used from the main thread. -static auto uploadMutex = QMutex(); -static std::queue uploadQueue; - -// logging information on successful uploads to a json file -void logToFile(const QString originalFilePath, QString imageLink, - QString deletionLink, ChannelPtr channel) -{ - const QString logFileName = - combinePath((getSettings()->logPath.getValue().isEmpty() - ? getPaths()->messageLogDirectory - : getSettings()->logPath), - "ImageUploader.json"); - - //reading existing logs - QFile logReadFile(logFileName); - bool isLogFileOkay = - logReadFile.open(QIODevice::ReadWrite | QIODevice::Text); - if (!isLogFileOkay) - { - channel->addMessage(makeSystemMessage( - QString("Failed to open log file with links at ") + logFileName)); - return; - } - auto logs = logReadFile.readAll(); - if (logs.isEmpty()) - { - logs = QJsonDocument(QJsonArray()).toJson(); - } - logReadFile.close(); - - //writing new data to logs - QJsonObject newLogEntry; - newLogEntry["channelName"] = channel->getName(); - newLogEntry["deletionLink"] = - deletionLink.isEmpty() ? QJsonValue(QJsonValue::Null) : deletionLink; - newLogEntry["imageLink"] = imageLink; - newLogEntry["localPath"] = originalFilePath.isEmpty() - ? QJsonValue(QJsonValue::Null) - : originalFilePath; - newLogEntry["timestamp"] = QDateTime::currentSecsSinceEpoch(); - // channel name - // deletion link (can be empty) - // image link - // local path to an image (can be empty) - // timestamp - QSaveFile logSaveFile(logFileName); - logSaveFile.open(QIODevice::WriteOnly | QIODevice::Text); - QJsonArray entries = QJsonDocument::fromJson(logs).array(); - entries.push_back(newLogEntry); - logSaveFile.write(QJsonDocument(entries).toJson()); - logSaveFile.commit(); -} - -// extracting link to either image or its deletion from response body -QString getJSONValue(QJsonValue responseJson, QString jsonPattern) -{ - for (const QString &key : jsonPattern.split(".")) - { - responseJson = responseJson[key]; - } - return responseJson.toString(); -} - -QString getLinkFromResponse(NetworkResult response, QString pattern) -{ - QRegularExpression regExp("{(.+)}", - QRegularExpression::InvertedGreedinessOption); - auto match = regExp.match(pattern); - - while (match.hasMatch()) - { - pattern.replace(match.captured(0), - getJSONValue(response.parseJson(), match.captured(1))); - match = regExp.match(pattern); - } - return pattern; -} - -void uploadImageToNuuls(RawImageData imageData, ChannelPtr channel, - ResizingTextEdit &textEdit) -{ - const static char *const boundary = "thisistheboudaryasd"; - const static QString contentType = - QString("multipart/form-data; boundary=%1").arg(boundary); - QUrl url(getSettings()->imageUploaderUrl.getValue().isEmpty() - ? getSettings()->imageUploaderUrl.getDefaultValue() - : getSettings()->imageUploaderUrl); - QString formField( - getSettings()->imageUploaderFormField.getValue().isEmpty() - ? getSettings()->imageUploaderFormField.getDefaultValue() - : getSettings()->imageUploaderFormField); - auto extraHeaders = - parseHeaderList(getSettings()->imageUploaderHeaders.getValue()); - QString originalFilePath = imageData.filePath; - - QHttpMultiPart *payload = new QHttpMultiPart(QHttpMultiPart::FormDataType); - QHttpPart part = QHttpPart(); - part.setBody(imageData.data); - part.setHeader(QNetworkRequest::ContentTypeHeader, - QString("image/%1").arg(imageData.format)); - part.setHeader(QNetworkRequest::ContentLengthHeader, - QVariant(imageData.data.length())); - part.setHeader(QNetworkRequest::ContentDispositionHeader, - QString("form-data; name=\"%1\"; filename=\"control_v.%2\"") - .arg(formField) - .arg(imageData.format)); - payload->setBoundary(boundary); - payload->append(part); - - NetworkRequest(url, NetworkRequestType::Post) - .header("Content-Type", contentType) - .headerList(extraHeaders) - .multiPart(payload) - .onSuccess([&textEdit, channel, - originalFilePath](NetworkResult result) -> Outcome { - QString link = getSettings()->imageUploaderLink.getValue().isEmpty() - ? result.getData() - : getLinkFromResponse( - result, getSettings()->imageUploaderLink); - QString deletionLink = - getSettings()->imageUploaderDeletionLink.getValue().isEmpty() - ? "" - : getLinkFromResponse( - result, getSettings()->imageUploaderDeletionLink); - qCDebug(chatterinoNuulsuploader) << link << deletionLink; - textEdit.insertPlainText(link + " "); - if (uploadQueue.empty()) - { - channel->addMessage(makeSystemMessage( - QString("Your image has been uploaded to %1 %2.") - .arg(link) - .arg(deletionLink.isEmpty() - ? "" - : QString("(Deletion link: %1 )") - .arg(deletionLink)))); - uploadMutex.unlock(); - } - else - { - channel->addMessage(makeSystemMessage( - QString("Your image has been uploaded to %1 %2. %3 left. " - "Please wait until all of them are uploaded. " - "About %4 seconds left.") - .arg(link) - .arg(deletionLink.isEmpty() - ? "" - : QString("(Deletion link: %1 )") - .arg(deletionLink)) - .arg(uploadQueue.size()) - .arg(uploadQueue.size() * (UPLOAD_DELAY / 1000 + 1)))); - // 2 seconds for the timer that's there not to spam the remote server - // and 1 second of actual uploading. - - QTimer::singleShot(UPLOAD_DELAY, [channel, &textEdit]() { - uploadImageToNuuls(uploadQueue.front(), channel, textEdit); - uploadQueue.pop(); - }); - } - - logToFile(originalFilePath, link, deletionLink, channel); - - return Success; - }) - .onError([channel](NetworkResult result) -> bool { - channel->addMessage(makeSystemMessage( - QString("An error happened while uploading your image: %1") - .arg(result.status()))); - uploadMutex.unlock(); - return true; - }) - .execute(); -} - -void upload(const QMimeData *source, ChannelPtr channel, - ResizingTextEdit &outputTextEdit) -{ - if (!uploadMutex.tryLock()) - { - channel->addMessage(makeSystemMessage( - QString("Please wait until the upload finishes."))); - return; - } - - channel->addMessage(makeSystemMessage(QString("Started upload..."))); - if (source->hasUrls()) - { - auto mimeDb = QMimeDatabase(); - // This path gets chosen when files are copied from a file manager, like explorer.exe, caja. - // Each entry in source->urls() is a QUrl pointing to a file that was copied. - for (const QUrl &path : source->urls()) - { - QString localPath = path.toLocalFile(); - QMimeType mime = mimeDb.mimeTypeForUrl(path); - if (mime.name().startsWith("image") && !mime.inherits("image/gif")) - { - channel->addMessage(makeSystemMessage( - QString("Uploading image: %1").arg(localPath))); - QImage img = QImage(localPath); - if (img.isNull()) - { - channel->addMessage( - makeSystemMessage(QString("Couldn't load image :("))); - uploadMutex.unlock(); - return; - } - - boost::optional imageData = convertToPng(img); - if (imageData) - { - RawImageData data = {imageData.get(), "png", localPath}; - uploadQueue.push(data); - } - else - { - channel->addMessage(makeSystemMessage( - QString("Cannot upload file: %1. Couldn't convert " - "image to png.") - .arg(localPath))); - uploadMutex.unlock(); - return; - } - } - else if (mime.inherits("image/gif")) - { - channel->addMessage(makeSystemMessage( - QString("Uploading GIF: %1").arg(localPath))); - QFile file(localPath); - bool isOkay = file.open(QIODevice::ReadOnly); - if (!isOkay) - { - channel->addMessage( - makeSystemMessage(QString("Failed to open file. :("))); - uploadMutex.unlock(); - return; - } - RawImageData data = {file.readAll(), "gif", localPath}; - uploadQueue.push(data); - file.close(); - // file.readAll() => might be a bit big but it /should/ work - } - else - { - channel->addMessage(makeSystemMessage( - QString("Cannot upload file: %1. Not an image.") - .arg(localPath))); - uploadMutex.unlock(); - return; - } - } - if (!uploadQueue.empty()) - { - uploadImageToNuuls(uploadQueue.front(), channel, outputTextEdit); - uploadQueue.pop(); - } - } - else if (source->hasFormat("image/png")) - { - // the path to file is not present every time, thus the filePath is empty - uploadImageToNuuls({source->data("image/png"), "png", ""}, channel, - outputTextEdit); - } - else if (source->hasFormat("image/jpeg")) - { - uploadImageToNuuls({source->data("image/jpeg"), "jpeg", ""}, channel, - outputTextEdit); - } - else if (source->hasFormat("image/gif")) - { - uploadImageToNuuls({source->data("image/gif"), "gif", ""}, channel, - outputTextEdit); - } - - else - { // not PNG, try loading it into QImage and save it to a PNG. - QImage image = qvariant_cast(source->imageData()); - boost::optional imageData = convertToPng(image); - if (imageData) - { - uploadImageToNuuls({imageData.get(), "png", ""}, channel, - outputTextEdit); - } - else - { - channel->addMessage(makeSystemMessage( - QString("Cannot upload file, failed to convert to png."))); - uploadMutex.unlock(); - } - } -} - -} // namespace chatterino diff --git a/src/util/NuulsUploader.hpp b/src/util/NuulsUploader.hpp deleted file mode 100644 index 6339d0dae..000000000 --- a/src/util/NuulsUploader.hpp +++ /dev/null @@ -1,20 +0,0 @@ -#include "common/Channel.hpp" -#include "widgets/helper/ResizingTextEdit.hpp" - -#include -#include - -namespace chatterino { -struct RawImageData { - QByteArray data; - QString format; - QString filePath; -}; - -void upload(QByteArray imageData, ChannelPtr channel, - ResizingTextEdit &textEdit, std::string format); -void upload(RawImageData imageData, ChannelPtr channel, - ResizingTextEdit &textEdit); -void upload(const QMimeData *source, ChannelPtr channel, - ResizingTextEdit &outputTextEdit); -} // namespace chatterino diff --git a/src/util/Overloaded.hpp b/src/util/Overloaded.hpp deleted file mode 100644 index e5bc805f5..000000000 --- a/src/util/Overloaded.hpp +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once - -namespace chatterino { - -template -struct Overloaded : Ts... { - using Ts::operator()...; -}; - -template -Overloaded(Ts...) -> Overloaded; - -} // namespace chatterino diff --git a/src/util/PersistSignalVector.hpp b/src/util/PersistSignalVector.hpp deleted file mode 100644 index b1dde7786..000000000 --- a/src/util/PersistSignalVector.hpp +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include -#include "common/ChatterinoSetting.hpp" -#include "common/SignalVector.hpp" - -namespace chatterino { - -template -inline void persist(SignalVector &vec, const std::string &name) -{ - auto setting = std::make_unique>>(name); - - for (auto &&item : setting->getValue()) - vec.append(item); - - vec.delayedItemsChanged.connect([setting = setting.get(), vec = &vec] { - setting->setValue(vec->raw()); - }); - - // TODO: Delete when appropriate. - setting.release(); -} - -} // namespace chatterino diff --git a/src/util/PostToThread.hpp b/src/util/PostToThread.hpp index 5f90849d7..0401091be 100644 --- a/src/util/PostToThread.hpp +++ b/src/util/PostToThread.hpp @@ -1,39 +1,16 @@ #pragma once +#include "debug/AssertInGuiThread.hpp" + #include -#include -#include - -#include - -#define async_exec(a) \ - QThreadPool::globalInstance()->start(new LambdaRunnable(a)); - namespace chatterino { -class LambdaRunnable : public QRunnable -{ -public: - LambdaRunnable(std::function action) - { - this->action_ = std::move(action); - } - - void run() - { - this->action_(); - } - -private: - std::function action_; -}; - // Taken from // https://stackoverflow.com/questions/21646467/how-to-execute-a-functor-or-a-lambda-in-a-given-thread-in-qt-gcd-style // Qt 5/4 - preferred, has least allocations template -static void postToThread(F &&fun, QObject *obj = qApp) +static void postToThread(F &&fun, QObject *obj = QCoreApplication::instance()) { struct Event : public QEvent { using Fun = typename std::decay::type; @@ -56,4 +33,26 @@ static void postToThread(F &&fun, QObject *obj = qApp) QCoreApplication::postEvent(obj, new Event(std::forward(fun))); } +template +static void runInGuiThread(F &&fun) +{ + if (isGuiThread()) + { + fun(); + } + else + { + postToThread(fun); + } +} + +template +inline void postToGuiThread(F &&fun) +{ + assert(!isGuiThread() && + "postToGuiThread must be called from a non-GUI thread"); + + postToThread(std::forward(fun)); +} + } // namespace chatterino diff --git a/src/util/QCompareCaseInsensitive.hpp b/src/util/QCompareCaseInsensitive.hpp new file mode 100644 index 000000000..70e8a1a01 --- /dev/null +++ b/src/util/QCompareCaseInsensitive.hpp @@ -0,0 +1,144 @@ +#pragma once + +#include +#include +#include +#include + +#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) +# include +#endif + +namespace chatterino { + +/// Case insensitive transparent comparator for Qt's string types +struct QCompareCaseInsensitive { + using is_transparent = void; + + // clang-format off + bool operator()(const QString & a, const QString & b) const noexcept; + bool operator()(QStringView a, QStringView b) const noexcept; + bool operator()(QLatin1String a, QLatin1String b) const noexcept; + + bool operator()(const QString & a, QStringView b) const noexcept; + bool operator()(const QString & a, QLatin1String b) const noexcept; + + bool operator()(QStringView a, const QString & b) const noexcept; + bool operator()(QLatin1String a, const QString & b) const noexcept; + + bool operator()(QStringView a, QLatin1String b) const noexcept; + bool operator()(QLatin1String a, QStringView b) const noexcept; + +#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) + bool operator()(QUtf8StringView a, QUtf8StringView b) const noexcept; + + bool operator()(const QString & a, QUtf8StringView b) const noexcept; + bool operator()(QStringView a, QUtf8StringView b) const noexcept; + bool operator()(QLatin1String a, QUtf8StringView b) const noexcept; + + bool operator()(QUtf8StringView a, const QString & b) const noexcept; + bool operator()(QUtf8StringView a, QStringView b) const noexcept; + bool operator()(QUtf8StringView a, QLatin1String b) const noexcept; +#endif + // clang-format on +}; + +inline bool QCompareCaseInsensitive::operator()(const QString &a, + const QString &b) const noexcept +{ + return a.compare(b, Qt::CaseInsensitive) < 0; +} + +inline bool QCompareCaseInsensitive::operator()(QStringView a, + QStringView b) const noexcept +{ + return a.compare(b, Qt::CaseInsensitive) < 0; +} + +inline bool QCompareCaseInsensitive::operator()(QLatin1String a, + QLatin1String b) const noexcept +{ + return a.compare(b, Qt::CaseInsensitive) < 0; +} + +inline bool QCompareCaseInsensitive::operator()(const QString &a, + QStringView b) const noexcept +{ + return a.compare(b, Qt::CaseInsensitive) < 0; +} + +inline bool QCompareCaseInsensitive::operator()(const QString &a, + QLatin1String b) const noexcept +{ + return a.compare(b, Qt::CaseInsensitive) < 0; +} + +inline bool QCompareCaseInsensitive::operator()(QStringView a, + const QString &b) const noexcept +{ + return a.compare(b, Qt::CaseInsensitive) < 0; +} + +inline bool QCompareCaseInsensitive::operator()(QLatin1String a, + const QString &b) const noexcept +{ + return a.compare(b, Qt::CaseInsensitive) < 0; +} + +inline bool QCompareCaseInsensitive::operator()(QStringView a, + QLatin1String b) const noexcept +{ + return a.compare(b, Qt::CaseInsensitive) < 0; +} + +inline bool QCompareCaseInsensitive::operator()(QLatin1String a, + QStringView b) const noexcept +{ + return a.compare(b, Qt::CaseInsensitive) < 0; +} + +#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) +inline bool QCompareCaseInsensitive::operator()( + QUtf8StringView a, QUtf8StringView b) const noexcept +{ + return a.compare(b, Qt::CaseInsensitive) < 0; +} + +inline bool QCompareCaseInsensitive::operator()( + const QString &a, QUtf8StringView b) const noexcept +{ + return QStringView{a}.compare(b, Qt::CaseInsensitive) < 0; +} + +inline bool QCompareCaseInsensitive::operator()( + QStringView a, QUtf8StringView b) const noexcept +{ + return a.compare(b, Qt::CaseInsensitive) < 0; +} + +inline bool QCompareCaseInsensitive::operator()( + QLatin1String a, QUtf8StringView b) const noexcept +{ + return a.compare(b, Qt::CaseInsensitive) < 0; +} + +inline bool QCompareCaseInsensitive::operator()(QUtf8StringView a, + const QString &b) const noexcept +{ + return a.compare(b, Qt::CaseInsensitive) < 0; +} + +inline bool QCompareCaseInsensitive::operator()(QUtf8StringView a, + QStringView b) const noexcept +{ + return a.compare(b, Qt::CaseInsensitive) < 0; +} + +inline bool QCompareCaseInsensitive::operator()(QUtf8StringView a, + QLatin1String b) const noexcept +{ + return a.compare(b, Qt::CaseInsensitive) < 0; +} +#endif + +} // namespace chatterino diff --git a/src/util/QMagicEnum.hpp b/src/util/QMagicEnum.hpp new file mode 100644 index 000000000..0325102e3 --- /dev/null +++ b/src/util/QMagicEnum.hpp @@ -0,0 +1,313 @@ +#pragma once + +#include +#include +#include + +namespace chatterino::qmagicenum::detail { + +template +struct EnableIfEnum { +}; + +template +struct EnableIfEnum { + using type = R; +}; + +template , + typename D = std::decay_t> +using enable_if_t = typename EnableIfEnum< + std::is_enum_v && + std::is_invocable_r_v, + R>::type; + +template +consteval QStringView fromArray(const std::array &arr) +{ + return QStringView{arr.data(), static_cast(N - 1)}; +} + +// Only the latin1 subset may be used right now, since it's easily convertible +template +consteval bool isLatin1(std::string_view maybe) +{ + for (std::size_t i = 0; i < N; i++) + { + if (maybe[i] < 0x20 || maybe[i] > 0x7e) + { + return false; + } + } + return true; +} + +template +inline constexpr bool eq( + QStringView a, QStringView b, + [[maybe_unused]] BinaryPredicate && + p) noexcept(magic_enum::detail::is_nothrow_invocable()) +{ + // Note: operator== isn't constexpr + if (a.size() != b.size()) + { + return false; + } + + for (QStringView::size_type i = 0; i < a.size(); i++) + { + if (!p(a[i], b[i])) + { + return false; + } + } + + return true; +} + +template +consteval auto enumNameStorage() +{ + constexpr auto utf8 = magic_enum::enum_name(); + + static_assert(isLatin1(utf8), + "Can't convert non-latin1 UTF8 to UTF16"); + + std::array storage; + for (std::size_t i = 0; i < utf8.size(); i++) + { + storage[i] = static_cast(utf8[i]); + } + storage[utf8.size()] = 0; + return storage; +} + +template +inline constexpr auto ENUM_NAME_STORAGE = enumNameStorage(); + +template +consteval auto namesStorage(std::index_sequence /*unused*/) +{ + return std::array{{detail::fromArray( + ENUM_NAME_STORAGE()[I]>)...}}; +} + +template > +inline constexpr auto NAMES_STORAGE = namesStorage( + std::make_index_sequence()>{}); + +template > +using NamesStorage = decltype((NAMES_STORAGE)); + +template > +class CaseInsensitive +{ + static constexpr QChar toLower(QChar c) noexcept + { + return (c >= u'A' && c <= u'Z') + ? QChar(c.unicode() + static_cast(u'a' - u'A')) + : c; + } + +public: + template + constexpr std::enable_if_t, QChar> && + std::is_same_v, QChar>, + bool> + operator()(L lhs, R rhs) const noexcept + { + return Op{}(toLower(lhs), toLower(rhs)); + } +}; + +} // namespace chatterino::qmagicenum::detail + +namespace chatterino::qmagicenum { + +/// @brief Get the name of an enum value +/// +/// This version is much lighter on the compile times and is not restricted to the enum_range limitation. +/// +/// @tparam V The enum value +/// @returns The name as a string view +template +[[nodiscard]] consteval detail::enable_if_t + enumName() noexcept +{ + return QStringView{ + detail::fromArray(detail::ENUM_NAME_STORAGE)}; +} + +/// @brief Get the name of an enum value +/// +/// @param value The enum value +/// @returns The name as a string view. If @a value does not have name or the +/// value is out of range an empty string is returned. +template > +[[nodiscard]] constexpr detail::enable_if_t enumName( + E value) noexcept +{ + using D = std::decay_t; + + if (const auto i = magic_enum::enum_index(value)) + { + return detail::NAMES_STORAGE[*i]; + } + return {}; +} + +/// @brief Gets a static QString from @a view. +/// +/// @pre @a view must be a static string view (i.e. it must be valid throughout +/// the entire duration of the program). +/// +/// @param view The view to turn into a static string +/// @returns Qt6: A static string (never gets freed), Qt5: regular string +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +[[nodiscard]] inline QString staticString(QStringView view) noexcept +{ + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast) + return QString(QStringPrivate(nullptr, const_cast(view.utf16()), + view.size())); +} +#else +[[nodiscard]] inline QString staticString(QStringView view) +{ + return view.toString(); +} +#endif + +/// @brief Get the name of an enum value +/// +/// This version is much lighter on the compile times and is not restricted to +/// the enum_range limitation. +/// +/// @tparam V The enum value +/// @returns The name as a string. The returned string is static. +template +[[nodiscard]] inline detail::enable_if_t + enumNameString() noexcept +{ + return staticString(enumName()); +} + +/// @brief Get the name of an enum value +/// +/// This version is much lighter on the compile times and is not restricted to +/// the enum_range limitation. +/// +/// @tparam V The enum value +/// @returns The name as a string. If @a value does not have name or the +/// value is out of range an empty string is returned. +/// The returned string is static. +template > +[[nodiscard]] inline detail::enable_if_t enumNameString( + E value) noexcept +{ + using D = std::decay_t; + + return staticString(enumName(value)); +} + +/// @brief Gets the enum value from a name +/// +/// @tparam E The enum type to parse the @a name as +/// @param name The name of the enum value to parse +/// @param p A predicate to compare characters of a string +/// (defaults to std::equal_to) +/// @returns A `std::optional` of the parsed value. If no value was parsed, +/// `std::nullopt` is returned. +template , + typename BinaryPredicate = std::equal_to<>> +[[nodiscard]] constexpr detail::enable_if_t>, + BinaryPredicate> + enumCast(QStringView name, + [[maybe_unused]] BinaryPredicate p = + {}) noexcept(magic_enum::detail:: + is_nothrow_invocable()) +{ + using D = std::decay_t; + + if constexpr (magic_enum::enum_count() == 0) + { + static_cast(name); + return std::nullopt; // Empty enum. + } + + for (std::size_t i = 0; i < magic_enum::enum_count(); i++) + { + if (detail::eq(name, detail::NAMES_STORAGE[i], p)) + { + return magic_enum::enum_value(i); + } + } + return std::nullopt; // Invalid value or out of range. +} + +/// @brief Constructs a name from the @a flags +/// +/// @param flags The combined flags to construct the name from +/// @param sep A separator between each flag (defaults to u'|') +/// @returns A string containing all names separated by @a sep. If any flag in +/// @a flags is out of rage or does not have a name, an empty string +/// is returned. +template +[[nodiscard]] inline detail::enable_if_t enumFlagsName( + E flags, char16_t sep = u'|') +{ + using D = std::decay_t; + using U = std::underlying_type_t; + constexpr auto S = magic_enum::detail::enum_subtype::flags; // NOLINT + + QString name; + auto checkValue = U{0}; + for (std::size_t i = 0; i < magic_enum::enum_count(); ++i) + { + const auto v = static_cast(magic_enum::enum_value(i)); + if ((static_cast(flags) & v) != 0) + { + const auto n = detail::NAMES_STORAGE[i]; + if (!n.empty()) + { + checkValue |= v; + if (!name.isEmpty()) + { + name.append(sep); + } + name.append(n); + } + else + { + return {}; // Value out of range. + } + } + } + + if (checkValue != 0 && checkValue == static_cast(flags)) + { + return name; + } + return {}; // Invalid value or out of range. +} + +/// @brief Get the names of all values from @a E. +/// +/// @tparam E The enum type +/// @returns A `std::array` of all names (`QStringView`s) +template > +[[nodiscard]] constexpr auto enumNames() noexcept + -> detail::enable_if_t> +{ + return detail::NAMES_STORAGE, S>; +} + +/// Allows you to write qmagicenum::enumCast("bar", qmagicenum::CASE_INSENSITIVE) +inline constexpr auto CASE_INSENSITIVE = detail::CaseInsensitive<>{}; + +} // namespace chatterino::qmagicenum diff --git a/src/util/QObjectRef.hpp b/src/util/QObjectRef.hpp deleted file mode 100644 index f434f4a3a..000000000 --- a/src/util/QObjectRef.hpp +++ /dev/null @@ -1,85 +0,0 @@ -#pragma once - -#include -#include -#include - -namespace chatterino { -/// Holds a pointer to a QObject and resets it to nullptr if the QObject -/// gets destroyed. -template -class QObjectRef -{ -public: - QObjectRef() - { - static_assert(std::is_base_of_v); - } - - explicit QObjectRef(T *t) - { - static_assert(std::is_base_of_v); - - this->set(t); - } - - QObjectRef(const QObjectRef &other) - { - this->set(other.t_); - } - - ~QObjectRef() - { - this->set(nullptr); - } - - QObjectRef &operator=(T *t) - { - this->set(t); - - return *this; - } - - operator bool() - { - return t_; - } - - T *operator->() - { - return t_; - } - - T *get() - { - return t_; - } - -private: - void set(T *other) - { - // old - if (this->conn_) - { - QObject::disconnect(this->conn_); - } - - // new - if (other) - { - // the cast here should absolutely not be necessary, but gcc still requires it - this->conn_ = - QObject::connect((QObject *)other, &QObject::destroyed, qApp, - [this](QObject *) { - this->set(nullptr); - }, - Qt::DirectConnection); - } - - this->t_ = other; - } - - std::atomic t_{}; - QMetaObject::Connection conn_; -}; -} // namespace chatterino diff --git a/src/util/QStringHash.hpp b/src/util/QStringHash.hpp index 1152e76e2..ac4bef57a 100644 --- a/src/util/QStringHash.hpp +++ b/src/util/QStringHash.hpp @@ -1,18 +1,17 @@ #pragma once +#include #include #include -namespace std { +namespace boost { -#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) template <> struct hash { - std::size_t operator()(const QString &s) const + std::size_t operator()(QString const &s) const { return qHash(s); } }; -#endif -} // namespace std +} // namespace boost diff --git a/src/util/Qt.hpp b/src/util/Qt.hpp deleted file mode 100644 index 1187953d8..000000000 --- a/src/util/Qt.hpp +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once - -#include - -#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) -namespace Qt { -const QString::SplitBehavior SkipEmptyParts = QString::SkipEmptyParts; -} -#endif diff --git a/src/util/RapidJsonSerializeQString.hpp b/src/util/RapidJsonSerializeQString.hpp index 1c685d16f..320237e5d 100644 --- a/src/util/RapidJsonSerializeQString.hpp +++ b/src/util/RapidJsonSerializeQString.hpp @@ -1,7 +1,7 @@ #pragma once -#include #include +#include namespace pajlada { diff --git a/src/util/RapidjsonHelpers.hpp b/src/util/RapidjsonHelpers.hpp index 403bbb582..a14631fbb 100644 --- a/src/util/RapidjsonHelpers.hpp +++ b/src/util/RapidjsonHelpers.hpp @@ -2,8 +2,8 @@ #include "util/RapidJsonSerializeQString.hpp" -#include #include +#include #include #include diff --git a/src/util/RatelimitBucket.cpp b/src/util/RatelimitBucket.cpp index c33f3a30a..75bf8e344 100644 --- a/src/util/RatelimitBucket.cpp +++ b/src/util/RatelimitBucket.cpp @@ -1,4 +1,4 @@ -#include "RatelimitBucket.hpp" +#include "util/RatelimitBucket.hpp" #include diff --git a/src/util/RatelimitBucket.hpp b/src/util/RatelimitBucket.hpp index 89ecdc570..650eb9640 100644 --- a/src/util/RatelimitBucket.hpp +++ b/src/util/RatelimitBucket.hpp @@ -4,6 +4,8 @@ #include #include +#include + namespace chatterino { class RatelimitBucket : public QObject diff --git a/src/util/RenameThread.cpp b/src/util/RenameThread.cpp new file mode 100644 index 000000000..b4f1baea0 --- /dev/null +++ b/src/util/RenameThread.cpp @@ -0,0 +1,29 @@ +#include "util/RenameThread.hpp" + +#include "common/QLogging.hpp" + +#ifdef Q_OS_WIN + +# include + +namespace chatterino::windows::detail { + +void renameThread(HANDLE hThread, const QString &threadName) +{ +# if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + // SetThreadDescription requires Windows 10, version 1607 + // Qt 6 requires Windows 10 1809 + + auto hr = SetThreadDescription(hThread, threadName.toStdWString().c_str()); + if (!SUCCEEDED(hr)) + { + qCWarning(chatterinoCommon).nospace() + << "Failed to set thread description, hresult=0x" + << QString::number(hr, 16); + } +# endif +} + +} // namespace chatterino::windows::detail + +#endif diff --git a/src/util/RenameThread.hpp b/src/util/RenameThread.hpp new file mode 100644 index 000000000..5212c8761 --- /dev/null +++ b/src/util/RenameThread.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include +#include + +#ifdef Q_OS_LINUX +# include +#endif + +namespace chatterino { + +#ifdef Q_OS_WIN +namespace windows::detail { + void renameThread(void *hThread, const QString &name); +} // namespace windows::detail +#endif + +template +void renameThread(T &thread, const QString &threadName) +{ +#ifdef Q_OS_LINUX + pthread_setname_np(thread.native_handle(), threadName.toLocal8Bit()); +#elif defined(Q_OS_WIN) + windows::detail::renameThread(thread.native_handle(), threadName); +#endif +} + +} // namespace chatterino diff --git a/src/util/SampleData.cpp b/src/util/SampleData.cpp index 2a9af80f7..7b12cd31c 100644 --- a/src/util/SampleData.cpp +++ b/src/util/SampleData.cpp @@ -1,4 +1,4 @@ -#include "SampleData.hpp" +#include "util/SampleData.hpp" namespace chatterino { @@ -64,12 +64,21 @@ const QStringList &getSampleCheerMessages() const QStringList &getSampleSubMessages() { static QStringList list{ - R"(@badges=staff/1,broadcaster/1,turbo/1;color=#008000;display-name=ronni;emotes=;id=db25007f-7a18-43eb-9379-80131e44d633;login=ronni;mod=0;msg-id=resub;msg-param-months=6;msg-param-sub-plan=Prime;msg-param-sub-plan-name=Prime;room-id=1337;subscriber=1;system-msg=ronni\shas\ssubscribed\sfor\s6\smonths!;tmi-sent-ts=1507246572675;turbo=1;user-id=1337;user-type=staff :tmi.twitch.tv USERNOTICE #pajlada :Great stream -- keep it up!)", + R"(@badges=staff/1,broadcaster/1,turbo/1;color=#008000;display-name=ronni;emotes=;id=db25007f-7a18-43eb-9379-80131e44d633;login=ronni;mod=0;msg-id=resub;msg-param-months=6;msg-param-sub-plan=Prime;msg-param-sub-plan-name=Prime;room-id=11148817;subscriber=1;system-msg=ronni\shas\ssubscribed\sfor\s6\smonths!;tmi-sent-ts=1507246572675;turbo=1;user-id=1337;user-type=staff :tmi.twitch.tv USERNOTICE #pajlada :Great stream -- keep it up!)", R"(@badges=staff/1,premium/1;color=#0000FF;display-name=TWW2;emotes=;id=e9176cd8-5e22-4684-ad40-ce53c2561c5e;login=tww2;mod=0;msg-id=subgift;msg-param-months=1;msg-param-recipient-display-name=Mr_Woodchuck;msg-param-recipient-id=89614178;msg-param-recipient-name=mr_woodchuck;msg-param-sub-plan-name=House\sof\sNyoro~n;msg-param-sub-plan=1000;room-id=19571752;subscriber=0;system-msg=TWW2\sgifted\sa\sTier\s1\ssub\sto\sMr_Woodchuck!;tmi-sent-ts=1521159445153;turbo=0;user-id=13405587;user-type=staff :tmi.twitch.tv USERNOTICE #pajlada)", // hyperbolicxd gifted a sub to quote_if_nam R"(@badges=subscriber/0,premium/1;color=#00FF7F;display-name=hyperbolicxd;emotes=;id=b20ef4fe-cba8-41d0-a371-6327651dc9cc;login=hyperbolicxd;mod=0;msg-id=subgift;msg-param-months=1;msg-param-recipient-display-name=quote_if_nam;msg-param-recipient-id=217259245;msg-param-recipient-user-name=quote_if_nam;msg-param-sender-count=1;msg-param-sub-plan-name=Channel\sSubscription\s(nymn_hs);msg-param-sub-plan=1000;room-id=62300805;subscriber=1;system-msg=hyperbolicxd\sgifted\sa\sTier\s1\ssub\sto\squote_if_nam!\sThis\sis\stheir\sfirst\sGift\sSub\sin\sthe\schannel!;tmi-sent-ts=1528190938558;turbo=0;user-id=111534250;user-type= :tmi.twitch.tv USERNOTICE #pajlada)", + // multi-month sub gift + R"(@badge-info=subscriber/32;badges=subscriber/3030,sub-gift-leader/2;color=#FF8EA3;display-name=iNatsuFN;emotes=;flags=;id=0d0decbd-b8f4-4e83-9e18-eca9cab69153;login=inatsufn;mod=0;msg-id=subgift;msg-param-gift-months=6;msg-param-goal-contribution-type=SUBS;msg-param-goal-current-contributions=881;msg-param-goal-target-contributions=900;msg-param-goal-user-contributions=1;msg-param-months=16;msg-param-origin-id=2524053421157386961;msg-param-recipient-display-name=kimmi_tm;msg-param-recipient-id=225806893;msg-param-recipient-user-name=kimmi_tm;msg-param-sender-count=334;msg-param-sub-plan-name=Channel\sSubscription\s(mxddy);msg-param-sub-plan=1000;room-id=210915729;subscriber=1;system-msg=iNatsuFN\sgifted\s6\smonths\sof\sTier\s1\sto\skimmi_tm.\sThey've\sgifted\s334\smonths\sin\sthe\schannel!;tmi-sent-ts=1712034497332;user-id=218205938;user-type=;vip=0 :tmi.twitch.tv USERNOTICE #pajlada)", + + // multi-month anon sub gift + R"(@msg-param-goal-user-contributions=1;system-msg=An\sanonymous\suser\sgifted\sa\sTier\s1\ssub\sto\sMohammadrezaDH!\s;msg-param-goal-current-contributions=2;vip=0;color=;user-id=274598607;mod=0;flags=;msg-param-months=2;historical=1;id=afa2155b-f563-4973-a5c2-e4075882bbfb;msg-param-gift-months=6;msg-id=subgift;badge-info=;msg-param-recipient-user-name=mohammadrezadh;login=ananonymousgifter;room-id=441388138;msg-param-goal-target-contributions=25;rm-received-ts=1712002037736;msg-param-recipient-id=204174899;emotes=;display-name=AnAnonymousGifter;badges=;msg-param-fun-string=FunStringFive;msg-param-goal-contribution-type=NEW_SUB_POINTS;msg-param-origin-id=8862142563198473546;msg-param-recipient-display-name=MohammadrezaDH;msg-param-sub-plan-name=jmarxists;user-type=;subscriber=0;tmi-sent-ts=1712002037615;msg-param-sub-plan=1000;msg-param-goal-description=day\slee\sgoal\s:-) :tmi.twitch.tv USERNOTICE #pajlada)", + + // multi-month sub gift by broadcaster + R"(@user-id=35759863;msg-param-origin-id=2862055070165643340;display-name=Lucidfoxx;id=eeb3cdb8-337c-413a-9521-3a884ff78754;msg-param-gift-months=12;msg-param-sub-plan=1000;vip=0;emotes=;badges=broadcaster/1,subscriber/3042,partner/1;msg-param-recipient-user-name=ogprodigy;msg-param-recipient-id=53888434;badge-info=subscriber/71;room-id=35759863;msg-param-recipient-display-name=OGprodigy;msg-param-sub-plan-name=Silver\sPackage;subscriber=1;system-msg=Lucidfoxx\sgifted\sa\sTier\s1\ssub\sto\sOGprodigy!;login=lucidfoxx;msg-param-sender-count=0;user-type=;mod=0;flags=;rm-received-ts=1712803947891;color=#EB078D;msg-param-months=15;tmi-sent-ts=1712803947773;msg-id=subgift :tmi.twitch.tv USERNOTICE #pajlada)", + // first time sub R"(@badges=subscriber/0,premium/1;color=#0000FF;display-name=byebyeheart;emotes=;id=fe390424-ab89-4c33-bb5a-53c6e5214b9f;login=byebyeheart;mod=0;msg-id=sub;msg-param-months=0;msg-param-sub-plan-name=Dakotaz;msg-param-sub-plan=Prime;room-id=39298218;subscriber=0;system-msg=byebyeheart\sjust\ssubscribed\swith\sTwitch\sPrime!;tmi-sent-ts=1528190963670;turbo=0;user-id=131956000;user-type= :tmi.twitch.tv USERNOTICE #pajlada)", @@ -106,6 +115,15 @@ const QStringList &getSampleMiscMessages() // mod announcement R"(@badge-info=subscriber/47;badges=broadcaster/1,subscriber/3012,twitchconAmsterdam2020/1;color=#FF0000;display-name=Supinic;emotes=;flags=;id=8c26e1ab-b50c-4d9d-bc11-3fd57a941d90;login=supinic;mod=0;msg-id=announcement;msg-param-color=PRIMARY;room-id=31400525;subscriber=1;system-msg=;tmi-sent-ts=1648762219962;user-id=31400525;user-type= :tmi.twitch.tv USERNOTICE #supinic :mm test lol)", + + // Hype Chat (Paid option for keeping a message in chat longer) + // no level + R"(@badge-info=subscriber/3;badges=subscriber/0,bits-charity/1;color=#0000FF;display-name=SnoopyTheBot;emotes=;first-msg=0;flags=;id=8779a9e5-cf1b-47b3-b9fe-67a5b1b605f6;mod=0;pinned-chat-paid-amount=500;pinned-chat-paid-canonical-amount=5;pinned-chat-paid-currency=USD;pinned-chat-paid-exponent=2;returning-chatter=0;room-id=36340781;subscriber=1;tmi-sent-ts=1664505974154;turbo=0;user-id=136881249;user-type= :snoopythebot!snoopythebot@snoopythebot.tmi.twitch.tv PRIVMSG #pajlada :-$5)", + // level 1 + R"(@pinned-chat-paid-level=ONE;mod=0;flags=;pinned-chat-paid-amount=1400;pinned-chat-paid-exponent=2;tmi-sent-ts=1687970631828;subscriber=1;user-type=;color=#9DA364;emotes=;badges=predictions/blue-1,subscriber/60,twitchconAmsterdam2020/1;pinned-chat-paid-canonical-amount=1400;turbo=0;user-id=26753388;id=e6681ba0-cdc6-4482-93a3-515b74361e8b;room-id=36340781;first-msg=0;returning-chatter=0;pinned-chat-paid-currency=NOK;pinned-chat-paid-is-system-message=0;badge-info=predictions/Day\s53/53\sforsenSmug,subscriber/67;display-name=matrHS :matrhs!matrhs@matrhs.tmi.twitch.tv PRIVMSG #pajlada :Title: Beating the record. but who is recordingLOL)", + R"(@flags=;pinned-chat-paid-amount=8761;turbo=0;user-id=35669184;pinned-chat-paid-level=ONE;user-type=;pinned-chat-paid-canonical-amount=8761;badge-info=subscriber/2;badges=subscriber/2,sub-gifter/1;emotes=;pinned-chat-paid-exponent=2;subscriber=1;mod=0;room-id=36340781;returning-chatter=0;id=289b614d-1837-4cff-ac22-ce33a9735323;first-msg=0;tmi-sent-ts=1687631719188;color=#00FF7F;pinned-chat-paid-currency=RUB;display-name=Danis;pinned-chat-paid-is-system-message=0 :danis!danis@danis.tmi.twitch.tv PRIVMSG #pajlada :-1 lulw)", + // level 2 + R"(@room-id=36340781;tmi-sent-ts=1687970634371;flags=;id=39a80a3d-c16e-420f-9bbb-faba4976a3bb;badges=subscriber/6,premium/1;emotes=;display-name=rickharrisoncoc;pinned-chat-paid-level=TWO;turbo=0;pinned-chat-paid-amount=500;pinned-chat-paid-is-system-message=0;color=#FF69B4;subscriber=1;user-type=;first-msg=0;pinned-chat-paid-currency=USD;pinned-chat-paid-canonical-amount=500;user-id=518404689;badge-info=subscriber/10;pinned-chat-paid-exponent=2;returning-chatter=0;mod=0 :rickharrisoncoc!rickharrisoncoc@rickharrisoncoc.tmi.twitch.tv PRIVMSG #pajlada :forsen please read my super chat. Please.)", }; return list; } @@ -130,7 +148,7 @@ const QStringList &getSampleEmoteTestMessages() const QString &getSampleChannelRewardMessage() { static QString str{ - R"({ "type": "MESSAGE", "data": { "topic": "community-points-channel-v1.11148817", "message": { "type": "reward-redeemed", "data": { "timestamp": "2020-07-13T20:19:31.430785354Z", "redemption": { "id": "b9628798-1b4e-4122-b2a6-031658df6755", "user": { "id": "91800084", "login": "cranken1337", "display_name": "cranken1337" }, "channel_id": "11148817", "redeemed_at": "2020-07-13T20:19:31.345237005Z", "reward": { "id": "313969fe-cc9f-4a0a-83c6-172acbd96957", "channel_id": "11148817", "title": "annoying reward pogchamp", "prompt": "", "cost": 3000, "is_user_input_required": true, "is_sub_only": false, "image": null, "default_image": { "url_1x": "https://static-cdn.jtvnw.net/custom-reward-images/default-1.png", "url_2x": "https://static-cdn.jtvnw.net/custom-reward-images/default-2.png", "url_4x": "https://static-cdn.jtvnw.net/custom-reward-images/default-4.png" }, "background_color": "#52ACEC", "is_enabled": true, "is_paused": false, "is_in_stock": true, "max_per_stream": { "is_enabled": false, "max_per_stream": 0 }, "should_redemptions_skip_request_queue": false, "template_id": null, "updated_for_indicator_at": "2020-01-20T04:33:33.624956679Z" }, "user_input": "wow, amazing reward", "status": "UNFULFILLED", "cursor": "Yjk2Mjg3OTgtMWI0ZS00MTIyLWIyYTYtMDMxNjU4ZGY2NzU1X18yMDIwLTA3LTEzVDIwOjE5OjMxLjM0NTIzNzAwNVo=" } } } } })", + R"({"type":"MESSAGE","data":{"topic":"community-points-channel-v1.11148817","message":"{\"type\":\"reward-redeemed\",\"data\":{\"timestamp\":\"2022-11-19T13:36:49.536938653Z\",\"redemption\":{\"id\":\"fd8af65d-3532-4e91-b30e-3995cefe576b\",\"user\":{\"id\":\"11148817\",\"login\":\"pajlada\",\"display_name\":\"pajlada\"},\"channel_id\":\"11148817\",\"redeemed_at\":\"2022-11-19T13:36:49.536938653Z\",\"reward\":{\"id\":\"44c3554e-58bc-4753-bf94-b615931d1072\",\"channel_id\":\"11148817\",\"title\":\"Test\",\"prompt\":\"\",\"cost\":1,\"is_user_input_required\":false,\"is_sub_only\":false,\"image\":{\"url_1x\":\"https://static-cdn.jtvnw.net/custom-reward-images/11148817/44c3554e-58bc-4753-bf94-b615931d1072/effc017c-861d-410d-abff-8bd98f1937eb/custom-1.png\",\"url_2x\":\"https://static-cdn.jtvnw.net/custom-reward-images/11148817/44c3554e-58bc-4753-bf94-b615931d1072/effc017c-861d-410d-abff-8bd98f1937eb/custom-2.png\",\"url_4x\":\"https://static-cdn.jtvnw.net/custom-reward-images/11148817/44c3554e-58bc-4753-bf94-b615931d1072/effc017c-861d-410d-abff-8bd98f1937eb/custom-4.png\"},\"default_image\":{\"url_1x\":\"https://static-cdn.jtvnw.net/custom-reward-images/default-1.png\",\"url_2x\":\"https://static-cdn.jtvnw.net/custom-reward-images/default-2.png\",\"url_4x\":\"https://static-cdn.jtvnw.net/custom-reward-images/default-4.png\"},\"background_color\":\"#9147FF\",\"is_enabled\":true,\"is_paused\":false,\"is_in_stock\":true,\"max_per_stream\":{\"is_enabled\":false,\"max_per_stream\":0},\"should_redemptions_skip_request_queue\":false,\"template_id\":null,\"updated_for_indicator_at\":\"2021-03-16T00:40:29.385327044Z\",\"max_per_user_per_stream\":{\"is_enabled\":false,\"max_per_user_per_stream\":0},\"global_cooldown\":{\"is_enabled\":false,\"global_cooldown_seconds\":0},\"redemptions_redeemed_current_stream\":null,\"cooldown_expires_at\":null},\"status\":\"UNFULFILLED\",\"cursor\":\"ZmQ4YWY2NWQtMzUzMi00ZTkxLWIzMGUtMzk5NWNlZmU1NzZiX18yMDIyLTExLTE5VDEzOjM2OjQ5LjUzNjkzODY1M1o=\"}}}"}})", }; return str; } @@ -138,7 +156,7 @@ const QString &getSampleChannelRewardMessage() const QString &getSampleChannelRewardMessage2() { static QString str{ - R"({ "type": "MESSAGE", "data": { "topic": "community-points-channel-v1.11148817", "message": { "type": "reward-redeemed", "data": { "timestamp": "2020-07-13T20:19:31.430785354Z", "redemption": { "id": "b9628798-1b4e-4122-b2a6-031658df6755", "user": { "id": "91800084", "login": "cranken1337", "display_name": "cranken1337" }, "channel_id": "11148817", "redeemed_at": "2020-07-13T20:19:31.345237005Z", "reward": { "id": "313969fe-cc9f-4a0a-83c6-172acbd96957", "channel_id": "11148817", "title": "annoying reward pogchamp", "prompt": "", "cost": 3000, "is_user_input_required": false, "is_sub_only": false, "image": null, "default_image": { "url_1x": "https://static-cdn.jtvnw.net/custom-reward-images/default-1.png", "url_2x": "https://static-cdn.jtvnw.net/custom-reward-images/default-2.png", "url_4x": "https://static-cdn.jtvnw.net/custom-reward-images/default-4.png" }, "background_color": "#52ACEC", "is_enabled": true, "is_paused": false, "is_in_stock": true, "max_per_stream": { "is_enabled": false, "max_per_stream": 0 }, "should_redemptions_skip_request_queue": false, "template_id": null, "updated_for_indicator_at": "2020-01-20T04:33:33.624956679Z" }, "status": "UNFULFILLED", "cursor": "Yjk2Mjg3OTgtMWI0ZS00MTIyLWIyYTYtMDMxNjU4ZGY2NzU1X18yMDIwLTA3LTEzVDIwOjE5OjMxLjM0NTIzNzAwNVo=" } } } } })", + R"({"type":"MESSAGE","data":{"topic":"community-points-channel-v1.11148817","message":"{\"type\":\"reward-redeemed\",\"data\":{\"timestamp\":\"2022-11-19T13:37:55.860377616Z\",\"redemption\":{\"id\":\"e8712d29-7564-40e9-a972-d70997665605\",\"user\":{\"id\":\"11148817\",\"login\":\"pajlada\",\"display_name\":\"pajlada\"},\"channel_id\":\"11148817\",\"redeemed_at\":\"2022-11-19T13:37:55.860377616Z\",\"reward\":{\"id\":\"649ecb1f-3fa8-491e-a48d-bd50720929d9\",\"channel_id\":\"11148817\",\"title\":\"asdd\",\"prompt\":\"asd\",\"cost\":5,\"is_user_input_required\":true,\"is_sub_only\":false,\"image\":null,\"default_image\":{\"url_1x\":\"https://static-cdn.jtvnw.net/custom-reward-images/default-1.png\",\"url_2x\":\"https://static-cdn.jtvnw.net/custom-reward-images/default-2.png\",\"url_4x\":\"https://static-cdn.jtvnw.net/custom-reward-images/default-4.png\"},\"background_color\":\"#0013A3\",\"is_enabled\":true,\"is_paused\":false,\"is_in_stock\":true,\"max_per_stream\":{\"is_enabled\":false,\"max_per_stream\":0},\"should_redemptions_skip_request_queue\":false,\"template_id\":null,\"updated_for_indicator_at\":\"2021-03-16T00:45:25.236177469Z\",\"max_per_user_per_stream\":{\"is_enabled\":false,\"max_per_user_per_stream\":0},\"global_cooldown\":{\"is_enabled\":false,\"global_cooldown_seconds\":0},\"redemptions_redeemed_current_stream\":null,\"cooldown_expires_at\":null},\"user_input\":\"TEST\",\"status\":\"UNFULFILLED\",\"cursor\":\"ZTg3MTJkMjktNzU2NC00MGU5LWE5NzItZDcwOTk3NjY1NjA1X18yMDIyLTExLTE5VDEzOjM3OjU1Ljg2MDM3NzYxNlo=\"}}}"}})", }; return str; } diff --git a/src/util/SignalListener.hpp b/src/util/SignalListener.hpp new file mode 100644 index 000000000..bcace9a60 --- /dev/null +++ b/src/util/SignalListener.hpp @@ -0,0 +1,73 @@ +#pragma once + +#include "common/ChatterinoSetting.hpp" + +#include +#include + +#include +#include +#include +#include +#include + +namespace chatterino { + +class SignalListener +{ + std::mutex cbMutex; + std::function cb; + +public: + using Callback = std::function; + + explicit SignalListener(Callback callback) + : cb(std::move(callback)) + { + } + + ~SignalListener() = default; + + SignalListener(SignalListener &&other) = delete; + SignalListener &operator=(SignalListener &&other) = delete; + SignalListener(const SignalListener &) = delete; + SignalListener &operator=(const SignalListener &) = delete; + + template + requires IsChatterinoSetting + void add(T &setting) + { + setting.connectSimple( + [this](auto) { + this->invoke(); + }, + this->managedConnections, false); + } + + template + requires std::derived_from + void add(T &signal) + { + this->managedConnections.emplace_back( + std::make_unique( + signal.connect([this] { + this->invoke(); + }))); + } + + void invoke() + { + std::unique_lock lock(this->cbMutex); + + if (this->cb) + { + this->cb(); + } + } + +private: + std::vector> + managedConnections; +}; + +} // namespace chatterino diff --git a/src/util/SplitCommand.cpp b/src/util/SplitCommand.cpp deleted file mode 100644 index 09c35afc1..000000000 --- a/src/util/SplitCommand.cpp +++ /dev/null @@ -1,93 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Copyright (C) 2016 Intel Corporation. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the QtCore module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -#include "SplitCommand.hpp" - -#include -#include -#include - -QStringList chatterino::splitCommand(QStringView command) -{ - QStringList args; - QString tmp; - int quoteCount = 0; - bool inQuote = false; - - // handle quoting. tokens can be surrounded by double quotes - // "hello world". three consecutive double quotes represent - // the quote character itself. - for (int i = 0; i < command.size(); ++i) - { - if (command.at(i) == QLatin1Char('"')) - { - ++quoteCount; - if (quoteCount == 3) - { - // third consecutive quote - quoteCount = 0; - tmp += command.at(i); - } - continue; - } - if (quoteCount) - { - if (quoteCount == 1) - inQuote = !inQuote; - quoteCount = 0; - } - if (!inQuote && command.at(i).isSpace()) - { - if (!tmp.isEmpty()) - { - args += tmp; - tmp.clear(); - } - } - else - { - tmp += command.at(i); - } - } - if (!tmp.isEmpty()) - args += tmp; - - return args; -} diff --git a/src/util/SplitCommand.hpp b/src/util/SplitCommand.hpp deleted file mode 100644 index 9db79c727..000000000 --- a/src/util/SplitCommand.hpp +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once - -#include -#include - -namespace chatterino { - -// Splits the string command into a list of tokens, and returns the list. -// Tokens with spaces can be surrounded by double quotes; -// three consecutive double quotes represent the quote character itself. -// -// Backported from QProcess 5.15 -QStringList splitCommand(QStringView command); - -} // namespace chatterino diff --git a/src/util/StandardItemHelper.hpp b/src/util/StandardItemHelper.hpp index 4e62c91e7..4f8d0826b 100644 --- a/src/util/StandardItemHelper.hpp +++ b/src/util/StandardItemHelper.hpp @@ -5,7 +5,7 @@ namespace chatterino { -static auto defaultItemFlags(bool selectable) +inline auto defaultItemFlags(bool selectable) { return Qt::ItemIsEnabled | (selectable ? Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | @@ -13,7 +13,7 @@ static auto defaultItemFlags(bool selectable) : Qt::ItemFlag()); } -static void setBoolItem(QStandardItem *item, bool value, +inline void setBoolItem(QStandardItem *item, bool value, bool userCheckable = true, bool selectable = true) { item->setFlags( @@ -22,7 +22,7 @@ static void setBoolItem(QStandardItem *item, bool value, item->setCheckState(value ? Qt::Checked : Qt::Unchecked); } -static void setStringItem(QStandardItem *item, const QString &value, +inline void setStringItem(QStandardItem *item, const QString &value, bool editable = true, bool selectable = true) { item->setData(value, Qt::EditRole); @@ -30,7 +30,7 @@ static void setStringItem(QStandardItem *item, const QString &value, (editable ? (Qt::ItemIsEditable) : 0))); } -static void setFilePathItem(QStandardItem *item, const QUrl &value, +inline void setFilePathItem(QStandardItem *item, const QUrl &value, bool selectable = true) { item->setData(value, Qt::UserRole); @@ -40,7 +40,7 @@ static void setFilePathItem(QStandardItem *item, const QUrl &value, (selectable ? Qt::ItemIsSelectable : Qt::NoItemFlags))); } -static void setColorItem(QStandardItem *item, const QColor &value, +inline void setColorItem(QStandardItem *item, const QColor &value, bool selectable = true) { item->setData(value, Qt::DecorationRole); @@ -49,7 +49,7 @@ static void setColorItem(QStandardItem *item, const QColor &value, (selectable ? Qt::ItemIsSelectable : Qt::NoItemFlags))); } -static QStandardItem *emptyItem() +inline QStandardItem *emptyItem() { auto *item = new QStandardItem(); item->setFlags(Qt::ItemFlags()); diff --git a/src/util/StreamLink.cpp b/src/util/StreamLink.cpp index ecd6242d4..6bb6b6fc3 100644 --- a/src/util/StreamLink.cpp +++ b/src/util/StreamLink.cpp @@ -1,133 +1,104 @@ #include "util/StreamLink.hpp" #include "Application.hpp" -#include "providers/irc/IrcMessageBuilder.hpp" +#include "common/QLogging.hpp" +#include "common/Version.hpp" #include "singletons/Settings.hpp" #include "singletons/WindowManager.hpp" #include "util/Helpers.hpp" -#include "util/SplitCommand.hpp" -#include "widgets/Window.hpp" #include "widgets/dialogs/QualityPopup.hpp" #include "widgets/splits/Split.hpp" +#include "widgets/Window.hpp" #include #include #include -#include "common/QLogging.hpp" -#include "common/Version.hpp" +#include #include -namespace chatterino { - namespace { - const char *getBinaryName() +using namespace chatterino; + +QString getStreamlinkPath() +{ + if (getSettings()->streamlinkUseCustomPath) { -#ifdef _WIN32 - return "streamlink.exe"; -#else - return "streamlink"; -#endif + const QString path = getSettings()->streamlinkPath; + return path.trimmed() % "/" % STREAMLINK_BINARY_NAME; } - const char *getDefaultBinaryPath() + return STREAMLINK_BINARY_NAME.toString(); +} + +void showStreamlinkNotFoundError() +{ + static auto *msg = new QErrorMessage; + msg->setWindowTitle("Chatterino - streamlink not found"); + + if (getSettings()->streamlinkUseCustomPath) { -#ifdef _WIN32 - return "C:\\Program Files (x86)\\Streamlink\\bin\\streamlink.exe"; -#else - return "/usr/bin/streamlink"; -#endif + msg->showMessage("Unable to find Streamlink executable\nMake sure " + "your custom path is pointing to the DIRECTORY " + "where the streamlink executable is located"); + } + else + { + msg->showMessage( + "Unable to find Streamlink executable.\nIf you have Streamlink " + "installed, you might need to enable the custom path option"); + } +} + +QProcess *createStreamlinkProcess() +{ + auto *p = new QProcess; + + const auto path = getStreamlinkPath(); + + if (Version::instance().isFlatpak()) + { + p->setProgram("flatpak-spawn"); + p->setArguments({"--host", path}); + } + else + { + p->setProgram(path); } - bool checkStreamlinkPath(const QString &path) - { - QFileInfo fileinfo(path); - - if (!fileinfo.exists()) + QObject::connect(p, &QProcess::errorOccurred, [=](auto err) { + if (err == QProcess::FailedToStart) { - return false; - // throw Exception(fS("Streamlink path ({}) is invalid, file does - // not exist", path)); - } - - return fileinfo.isExecutable(); - } - - void showStreamlinkNotFoundError() - { - static QErrorMessage *msg = new QErrorMessage; - msg->setWindowTitle("Chatterino - streamlink not found"); - - if (getSettings()->streamlinkUseCustomPath) - { - msg->showMessage("Unable to find Streamlink executable\nMake sure " - "your custom path is pointing to the DIRECTORY " - "where the streamlink executable is located"); + showStreamlinkNotFoundError(); } else { - msg->showMessage( - "Unable to find Streamlink executable.\nIf you have Streamlink " - "installed, you might need to enable the custom path option"); - } - } - - QProcess *createStreamlinkProcess() - { - auto p = new QProcess; - - const QString path = [] { - if (getSettings()->streamlinkUseCustomPath) - { - return getSettings()->streamlinkPath + "/" + getBinaryName(); - } - else - { - return QString{getBinaryName()}; - } - }(); - - if (Version::instance().isFlatpak()) - { - p->setProgram("flatpak-spawn"); - p->setArguments({"--host", path}); - } - else - { - p->setProgram(path); + qCWarning(chatterinoStreamlink) << "Error occurred" << err; } - QObject::connect(p, &QProcess::errorOccurred, [=](auto err) { - if (err == QProcess::FailedToStart) - { - showStreamlinkNotFoundError(); - } - else - { - qCWarning(chatterinoStreamlink) << "Error occurred" << err; - } + p->deleteLater(); + }); + QObject::connect( + p, + static_cast( + &QProcess::finished), + [=](int /*exitCode*/, QProcess::ExitStatus /*exitStatus*/) { p->deleteLater(); }); - QObject::connect( - p, - static_cast( - &QProcess::finished), - [=](int /*exitCode*/, QProcess::ExitStatus /*exitStatus*/) { - p->deleteLater(); - }); - - return p; - } + return p; +} } // namespace +namespace chatterino { + void getStreamQualities(const QString &channelURL, std::function cb) { - auto p = createStreamlinkProcess(); + auto *p = createStreamlinkProcess(); QObject::connect( p, @@ -147,7 +118,7 @@ void getStreamQualities(const QString &channelURL, QStringList split = lastLine.right(lastLine.length() - 19).split(", "); - for (int i = split.length() - 1; i >= 0; i--) + for (auto i = split.length() - 1; i >= 0; i--) { QString option = split.at(i); if (option == "best)") @@ -187,18 +158,18 @@ void getStreamQualities(const QString &channelURL, void openStreamlink(const QString &channelURL, const QString &quality, QStringList extraArguments) { - auto proc = createStreamlinkProcess(); + auto *proc = createStreamlinkProcess(); auto arguments = proc->arguments() - << extraArguments << channelURL << quality; + << std::move(extraArguments) << channelURL << quality; // Remove empty arguments before appending additional streamlink options // as the options might purposely contain empty arguments arguments.removeAll(QString()); QString additionalOptions = getSettings()->streamlinkOpts.getValue(); - arguments << splitCommand(additionalOptions); + arguments << QProcess::splitCommand(additionalOptions); - proc->setArguments(std::move(arguments)); + proc->setArguments(arguments); bool res = proc->startDetached(); if (!res) @@ -211,15 +182,18 @@ void openStreamlinkForChannel(const QString &channel) { static const QString INFO_TEMPLATE("Opening %1 in Streamlink ..."); - auto *currentPage = dynamic_cast( - getApp()->windows->getMainWindow().getNotebook().getSelectedPage()); + auto *currentPage = dynamic_cast(getApp() + ->getWindows() + ->getMainWindow() + .getNotebook() + .getSelectedPage()); if (currentPage != nullptr) { - if (auto currentSplit = currentPage->getSelectedSplit(); - currentSplit != nullptr) + auto *currentSplit = currentPage->getSelectedSplit(); + if (currentSplit != nullptr) { - currentSplit->getChannel()->addMessage( - makeSystemMessage(INFO_TEMPLATE.arg(channel))); + currentSplit->getChannel()->addSystemMessage( + INFO_TEMPLATE.arg(channel)); } } diff --git a/src/util/StreamLink.hpp b/src/util/StreamLink.hpp index 99dbb95b9..8ebcc617b 100644 --- a/src/util/StreamLink.hpp +++ b/src/util/StreamLink.hpp @@ -14,6 +14,12 @@ public: using std::runtime_error::runtime_error; }; +#ifdef Q_OS_WIN +constexpr inline QStringView STREAMLINK_BINARY_NAME = u"streamlink.exe"; +#else +constexpr inline QStringView STREAMLINK_BINARY_NAME = u"streamlink"; +#endif + // Open streamlink for given channel, quality and extra arguments // the "Additional arguments" are fetched and added at the beginning of the // streamlink call diff --git a/src/util/StreamerMode.cpp b/src/util/StreamerMode.cpp deleted file mode 100644 index 2724ed73f..000000000 --- a/src/util/StreamerMode.cpp +++ /dev/null @@ -1,140 +0,0 @@ -#include "StreamerMode.hpp" - -#include "Application.hpp" -#include "common/QLogging.hpp" -#include "messages/MessageBuilder.hpp" -#include "providers/twitch/TwitchIrcServer.hpp" -#include "singletons/Settings.hpp" -#include "singletons/WindowManager.hpp" -#include "widgets/Notebook.hpp" -#include "widgets/Window.hpp" -#include "widgets/helper/NotebookTab.hpp" -#include "widgets/splits/Split.hpp" - -#ifdef USEWINSDK -# include - -# include -# include -# pragma comment(lib, "Wtsapi32.lib") -#endif - -#include - -namespace chatterino { - -constexpr int cooldownInS = 10; - -bool shouldShowWarning = true; - -const QStringList &broadcastingBinaries() -{ -#ifdef USEWINSDK - static QStringList bins = { - "obs.exe", "obs64.exe", "PRISMLiveStudio.exe", - "XSplit.Core.exe", "TwitchStudio.exe", "vMix64.exe"}; -#else - static QStringList bins = {"obs", "Twitch Studio", "Streamlabs Desktop"}; -#endif - return bins; -} - -bool isInStreamerMode() -{ - switch (getSettings()->enableStreamerMode.getEnum()) - { - case StreamerModeSetting::Enabled: - return true; - case StreamerModeSetting::Disabled: - return false; - case StreamerModeSetting::DetectStreamingSoftware: - -#if defined(Q_OS_LINUX) || defined(Q_OS_MACOS) - - static bool cache = false; - static QDateTime time = QDateTime(); - - if (time.isValid() && - time.addSecs(cooldownInS) > QDateTime::currentDateTime()) - { - return cache; - } - time = QDateTime::currentDateTime(); - - QProcess p; - p.start("pgrep", {"-x", broadcastingBinaries().join("|")}, - QIODevice::NotOpen); - - if (p.waitForFinished(1000) && - p.exitStatus() == QProcess::NormalExit) - { - cache = (p.exitCode() == 0); - return (p.exitCode() == 0); - } - - // Fallback to false and showing a warning - - if (shouldShowWarning) - { - shouldShowWarning = false; - - getApp()->twitch->addGlobalSystemMessage( - "Streamer Mode is set to Automatic, but pgrep is missing. " - "Install it to fix the issue or set Streamer Mode to " - "Enabled or Disabled in the Settings."); - } - - qCWarning(chatterinoStreamerMode) << "pgrep execution timed out!"; - - cache = false; - return false; -#endif - -#ifdef USEWINSDK - if (!IsWindowsVistaOrGreater()) - { - return false; - } - static bool cache = false; - static QDateTime time = QDateTime(); - - if (time.isValid() && - time.addSecs(cooldownInS) > QDateTime::currentDateTime()) - { - return cache; - } - time = QDateTime::currentDateTime(); - - WTS_PROCESS_INFO *pWPIs = nullptr; - DWORD dwProcCount = 0; - - if (WTSEnumerateProcesses(WTS_CURRENT_SERVER_HANDLE, NULL, 1, - &pWPIs, &dwProcCount)) - { - //Go through all processes retrieved - for (DWORD i = 0; i < dwProcCount; i++) - { - QString processName = QString::fromUtf16( - reinterpret_cast(pWPIs[i].pProcessName)); - - if (broadcastingBinaries().contains(processName)) - { - cache = true; - return true; - } - } - } - - if (pWPIs) - { - WTSFreeMemory(pWPIs); - } - - cache = false; -#endif - return false; - } - return false; -} - -} // namespace chatterino diff --git a/src/util/StreamerMode.hpp b/src/util/StreamerMode.hpp deleted file mode 100644 index d161b9067..000000000 --- a/src/util/StreamerMode.hpp +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include - -namespace chatterino { - -enum StreamerModeSetting { - Disabled = 0, - Enabled = 1, - DetectStreamingSoftware = 2, -}; - -const QStringList &broadcastingBinaries(); -bool isInStreamerMode(); - -} // namespace chatterino diff --git a/src/util/ThreadGuard.hpp b/src/util/ThreadGuard.hpp new file mode 100644 index 000000000..00748033a --- /dev/null +++ b/src/util/ThreadGuard.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include +#include + +namespace chatterino { + +// Debug-class which asserts if guard of the same object has been called from different threads +struct ThreadGuard { +#ifndef NDEBUG + mutable std::mutex mutex; + mutable std::optional threadID; +#endif + + inline void guard() const + { +#ifndef NDEBUG + std::unique_lock lock(this->mutex); + + auto currentThreadID = std::this_thread::get_id(); + + if (!this->threadID.has_value()) + { + this->threadID = currentThreadID; + return; + } + + assert(this->threadID == currentThreadID); +#endif + } +}; + +} // namespace chatterino diff --git a/src/util/Twitch.cpp b/src/util/Twitch.cpp index 3bf821929..fa21c8583 100644 --- a/src/util/Twitch.cpp +++ b/src/util/Twitch.cpp @@ -1,16 +1,37 @@ #include "util/Twitch.hpp" +#include "util/QStringHash.hpp" + #include #include +#include + namespace chatterino { namespace { const auto TWITCH_USER_LOGIN_PATTERN = R"(^[a-z0-9]\w{0,24}$)"; + // Remember to keep VALID_HELIX_COLORS up-to-date if a new color is implemented to keep naming for users consistent + const std::unordered_map HELIX_COLOR_REPLACEMENTS{ + {"blueviolet", "blue_violet"}, {"cadetblue", "cadet_blue"}, + {"dodgerblue", "dodger_blue"}, {"goldenrod", "golden_rod"}, + {"hotpink", "hot_pink"}, {"orangered", "orange_red"}, + {"seagreen", "sea_green"}, {"springgreen", "spring_green"}, + {"yellowgreen", "yellow_green"}, + }; + } // namespace +// Colors retreived from https://dev.twitch.tv/docs/api/reference#update-user-chat-color 2022-09-11 +// Remember to keep HELIX_COLOR_REPLACEMENTS up-to-date if a new color is implemented to keep naming for users consistent +extern const QStringList VALID_HELIX_COLORS{ + "blue", "blue_violet", "cadet_blue", "chocolate", "coral", + "dodger_blue", "firebrick", "golden_rod", "green", "hot_pink", + "orange_red", "red", "sea_green", "spring_green", "yellow_green", +}; + void openTwitchUsercard(QString channel, QString username) { QDesktopServices::openUrl("https://www.twitch.tv/popout/" + channel + @@ -41,6 +62,33 @@ void stripChannelName(QString &channelName) } } +std::pair parseUserNameOrID(const QString &input) +{ + if (input.startsWith("id:")) + { + return { + {}, + input.mid(3), + }; + } + + QString userName = input; + + if (userName.startsWith('@') || userName.startsWith('#')) + { + userName.remove(0, 1); + } + if (userName.endsWith(',')) + { + userName.chop(1); + } + + return { + userName, + {}, + }; +} + QRegularExpression twitchUserNameRegexp() { static QRegularExpression re( @@ -57,4 +105,17 @@ QRegularExpression twitchUserLoginRegexp() return re; } +void cleanHelixColorName(QString &color) +{ + color = color.toLower(); + auto it = HELIX_COLOR_REPLACEMENTS.find(color); + + if (it == HELIX_COLOR_REPLACEMENTS.end()) + { + return; + } + + color = it->second; +} + } // namespace chatterino diff --git a/src/util/Twitch.hpp b/src/util/Twitch.hpp index 08cb8eb57..367b6cc98 100644 --- a/src/util/Twitch.hpp +++ b/src/util/Twitch.hpp @@ -2,9 +2,12 @@ #include #include +#include namespace chatterino { +extern const QStringList VALID_HELIX_COLORS; + void openTwitchUsercard(const QString channel, const QString username); // stripUserName removes any @ prefix or , suffix to make it more suitable for command use @@ -13,6 +16,16 @@ void stripUserName(QString &userName); // stripChannelName removes any @ prefix or , suffix to make it more suitable for command use void stripChannelName(QString &channelName); +using ParsedUserName = QString; +using ParsedUserID = QString; + +/** + * Parse the given input into either a user name or a user ID + * + * User IDs take priority and are parsed if the input starts with `id:` + */ +std::pair parseUserNameOrID(const QString &input); + // Matches a strict Twitch user login. // May contain lowercase a-z, 0-9, and underscores // Must contain between 1 and 25 characters @@ -25,4 +38,9 @@ QRegularExpression twitchUserLoginRegexp(); // Must not start with an underscore QRegularExpression twitchUserNameRegexp(); +// Cleans up a color name input for use in the Helix API +// Will help massage color names like BlueViolet to the helix-acceptible blue_violet +// Will also lowercase the color +void cleanHelixColorName(QString &color); + } // namespace chatterino diff --git a/src/util/Variant.hpp b/src/util/Variant.hpp new file mode 100644 index 000000000..8e7a2b093 --- /dev/null +++ b/src/util/Variant.hpp @@ -0,0 +1,28 @@ +#pragma once + +namespace chatterino::variant { + +/// Compile-time safe visitor for std and boost variants. +/// +/// From https://en.cppreference.com/w/cpp/utility/variant/visit +/// +/// Usage: +/// +/// ``` +/// std::variant v; +/// std::visit(variant::Overloaded{ +/// [](double) { qDebug() << "double"; }, +/// [](int) { qDebug() << "int"; } +/// }, v); +/// ``` +template +struct Overloaded : Ts... { + using Ts::operator()...; +}; + +// Technically, we shouldn't need this, as we're on C++ 20, +// but not all of our compilers support CTAD for aggregates yet. +template +Overloaded(Ts...) -> Overloaded; + +} // namespace chatterino::variant diff --git a/src/util/WidgetHelpers.cpp b/src/util/WidgetHelpers.cpp new file mode 100644 index 000000000..cd76dea80 --- /dev/null +++ b/src/util/WidgetHelpers.cpp @@ -0,0 +1,133 @@ +#include "util/WidgetHelpers.hpp" + +#include +#include +#include +#include +#include + +namespace { + +QPoint applyBounds(QScreen *screen, QPoint point, QSize frameSize, int height) +{ + if (screen == nullptr) + { + screen = QGuiApplication::primaryScreen(); + } + + const QRect bounds = screen->availableGeometry(); + + bool stickRight = false; + bool stickBottom = false; + + if (point.x() < bounds.left()) + { + point.setX(bounds.left()); + } + if (point.y() < bounds.top()) + { + point.setY(bounds.top()); + } + if (point.x() + frameSize.width() > bounds.right()) + { + stickRight = true; + point.setX(bounds.right() - frameSize.width()); + } + if (point.y() + frameSize.height() > bounds.bottom()) + { + stickBottom = true; + point.setY(bounds.bottom() - frameSize.height()); + } + + if (stickRight && stickBottom) + { + const QPoint globalCursorPos = QCursor::pos(); + point.setY(globalCursorPos.y() - height - 16); + } + + return point; +} + +/// Move the `window` into the `screen` geometry if it's not already in there. +void moveWithinScreen(QWidget *window, QScreen *screen, QPoint point) +{ + auto checked = + applyBounds(screen, point, window->frameSize(), window->height()); + window->move(checked); +} + +} // namespace + +namespace chatterino::widgets { + +QRect checkInitialBounds(QRect initialBounds, BoundsChecking mode) +{ + switch (mode) + { + case BoundsChecking::Off: { + return initialBounds; + } + break; + + case BoundsChecking::CursorPosition: { + return QRect{ + applyBounds(QGuiApplication::screenAt(QCursor::pos()), + initialBounds.topLeft(), initialBounds.size(), + initialBounds.height()), + initialBounds.size(), + }; + } + break; + + case BoundsChecking::DesiredPosition: { + return QRect{ + applyBounds(QGuiApplication::screenAt(initialBounds.topLeft()), + initialBounds.topLeft(), initialBounds.size(), + initialBounds.height()), + initialBounds.size(), + }; + } + break; + default: + assert(false && "Invalid bounds checking mode"); + return initialBounds; + } +} + +void moveWindowTo(QWidget *window, QPoint position, BoundsChecking mode) +{ + switch (mode) + { + case BoundsChecking::Off: { + window->move(position); + } + break; + + case BoundsChecking::CursorPosition: { + moveWithinScreen(window, QGuiApplication::screenAt(QCursor::pos()), + position); + } + break; + + case BoundsChecking::DesiredPosition: { + moveWithinScreen(window, QGuiApplication::screenAt(position), + position); + } + break; + } +} + +void showAndMoveWindowTo(QWidget *window, QPoint position, BoundsChecking mode) +{ +#ifdef Q_OS_WINDOWS + window->show(); + + moveWindowTo(window, position, mode); +#else + moveWindowTo(window, position, mode); + + window->show(); +#endif +} + +} // namespace chatterino::widgets diff --git a/src/util/WidgetHelpers.hpp b/src/util/WidgetHelpers.hpp new file mode 100644 index 000000000..5de90340f --- /dev/null +++ b/src/util/WidgetHelpers.hpp @@ -0,0 +1,48 @@ +#pragma once + +class QWidget; +class QPoint; +class QScreen; +class QRect; + +namespace chatterino::widgets { + +enum class BoundsChecking { + /// Don't do any bounds checking (equivalent to `QWidget::move`). + Off, + + /// Attempt to keep the window within bounds of the screen the cursor is on. + CursorPosition, + + /// Attempt to keep the window within bounds of the screen the desired position is on. + DesiredPosition, +}; + +/// Applies bounds checking to @a initialBounds. +/// +/// @param initialBounds The bounds to check. +/// @param mode The desired bounds checking. +/// @returns The potentially modified bounds. +QRect checkInitialBounds(QRect initialBounds, + BoundsChecking mode = BoundsChecking::DesiredPosition); + +/// Moves the `window` to the (global) `position` +/// while doing bounds-checking according to `mode` to ensure the window stays on one screen. +/// +/// @param window The window to move. +/// @param position The global position to move the window to. +/// @param mode The desired bounds checking. +void moveWindowTo(QWidget *window, QPoint position, + BoundsChecking mode = BoundsChecking::DesiredPosition); + +/// Moves the `window` to the (global) `position` +/// while doing bounds-checking according to `mode` to ensure the window stays on one screen. +/// Will also call show on the `window`, order is dependant on platform. +/// +/// @param window The window to move. +/// @param position The global position to move the window to. +/// @param mode The desired bounds checking. +void showAndMoveWindowTo(QWidget *window, QPoint position, + BoundsChecking mode = BoundsChecking::DesiredPosition); + +} // namespace chatterino::widgets diff --git a/src/util/WindowsHelper.cpp b/src/util/WindowsHelper.cpp index d2b9a4efe..6584604db 100644 --- a/src/util/WindowsHelper.cpp +++ b/src/util/WindowsHelper.cpp @@ -1,22 +1,28 @@ -#include "WindowsHelper.hpp" +#include "util/WindowsHelper.hpp" +#include "common/Literals.hpp" + +#include +#include +#include #include #ifdef USEWINSDK +# include +# include +# include +# include + namespace chatterino { -typedef enum MONITOR_DPI_TYPE { - MDT_EFFECTIVE_DPI = 0, - MDT_ANGULAR_DPI = 1, - MDT_RAW_DPI = 2, - MDT_DEFAULT = MDT_EFFECTIVE_DPI -} MONITOR_DPI_TYPE; +using namespace literals; -typedef HRESULT(CALLBACK *GetDpiForMonitor_)(HMONITOR, MONITOR_DPI_TYPE, UINT *, - UINT *); +using GetDpiForMonitor_ = HRESULT(CALLBACK *)(HMONITOR, MONITOR_DPI_TYPE, + UINT *, UINT *); -boost::optional getWindowDpi(HWND hwnd) +// TODO: This should be changed to `GetDpiForWindow`. +std::optional getWindowDpi(HWND hwnd) { static HINSTANCE shcore = LoadLibrary(L"Shcore.dll"); if (shcore != nullptr) @@ -27,45 +33,38 @@ boost::optional getWindowDpi(HWND hwnd) HMONITOR monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); - UINT xScale, yScale; - + UINT xScale = 96; + UINT yScale = 96; getDpiForMonitor(monitor, MDT_DEFAULT, &xScale, &yScale); return xScale; } } - return boost::none; + return std::nullopt; } -typedef HRESULT(CALLBACK *OleFlushClipboard_)(); - void flushClipboard() { - static HINSTANCE ole32 = LoadLibrary(L"Ole32.dll"); - if (ole32 != nullptr) + if (QApplication::clipboard()->ownsClipboard()) { - if (auto oleFlushClipboard = - OleFlushClipboard_(GetProcAddress(ole32, "OleFlushClipboard"))) - { - oleFlushClipboard(); - } + OleFlushClipboard(); } } -constexpr const char *runKey = - "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run"; +const QString RUN_KEY = + uR"(HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run)"_s; bool isRegisteredForStartup() { - QSettings settings(runKey, QSettings::NativeFormat); + QSettings settings(RUN_KEY, QSettings::NativeFormat); return !settings.value("Chatterino").toString().isEmpty(); } void setRegisteredForStartup(bool isRegistered) { - QSettings settings(runKey, QSettings::NativeFormat); + QSettings settings(RUN_KEY, QSettings::NativeFormat); if (isRegistered) { @@ -81,6 +80,54 @@ void setRegisteredForStartup(bool isRegistered) } } +QString getAssociatedExecutable(AssociationQueryType queryType, LPCWSTR query) +{ + // always error out instead of returning a truncated string when the + // buffer is too small - avoids race condition when the user changes their + // default browser between calls to AssocQueryString + ASSOCF flags = ASSOCF_NOTRUNCATE; + + if (queryType == AssociationQueryType::Protocol) + { + // ASSOCF_IS_PROTOCOL was introduced in Windows 8 + if (IsWindows8OrGreater()) + { + flags |= ASSOCF_IS_PROTOCOL; + } + else + { + return {}; + } + } + + DWORD resultSize = 0; + if (FAILED(AssocQueryStringW(flags, ASSOCSTR_EXECUTABLE, query, nullptr, + nullptr, &resultSize))) + { + return {}; + } + + if (resultSize <= 1) + { + // resultSize includes the null terminator. if resultSize is 1, the + // returned value would be the empty string. + return {}; + } + + QString result; + auto *buf = new wchar_t[resultSize]; + if (SUCCEEDED(AssocQueryStringW(flags, ASSOCSTR_EXECUTABLE, query, nullptr, + buf, &resultSize))) + { + // QString::fromWCharArray expects the length in characters *not + // including* the null terminator, but AssocQueryStringW calculates + // length including the null terminator + result = QString::fromWCharArray(buf, resultSize - 1); + } + delete[] buf; + return result; +} + } // namespace chatterino #endif diff --git a/src/util/WindowsHelper.hpp b/src/util/WindowsHelper.hpp index b40a8270f..af719996b 100644 --- a/src/util/WindowsHelper.hpp +++ b/src/util/WindowsHelper.hpp @@ -2,17 +2,23 @@ #ifdef USEWINSDK +# include # include -# include + +# include namespace chatterino { -boost::optional getWindowDpi(HWND hwnd); +enum class AssociationQueryType { Protocol, FileExtension }; + +std::optional getWindowDpi(HWND hwnd); void flushClipboard(); bool isRegisteredForStartup(); void setRegisteredForStartup(bool isRegistered); +QString getAssociatedExecutable(AssociationQueryType queryType, LPCWSTR query); + } // namespace chatterino #endif diff --git a/src/util/XDGDesktopFile.cpp b/src/util/XDGDesktopFile.cpp new file mode 100644 index 000000000..886a921dc --- /dev/null +++ b/src/util/XDGDesktopFile.cpp @@ -0,0 +1,118 @@ +#include "util/XDGDesktopFile.hpp" + +#include "util/XDGDirectory.hpp" + +#include +#include + +#include + +#if defined(Q_OS_UNIX) and !defined(Q_OS_DARWIN) + +namespace chatterino { + +XDGDesktopFile::XDGDesktopFile(const QString &filename) +{ + QFile file(filename); + if (!file.open(QIODevice::ReadOnly)) + { + return; + } + this->valid = true; + + std::optional> entries; + + while (!file.atEnd()) + { + auto lineBytes = file.readLine().trimmed(); + + // Ignore comments & empty lines + if (lineBytes.startsWith('#') || lineBytes.size() == 0) + { + continue; + } + + auto line = QString::fromUtf8(lineBytes); + + if (line.startsWith('[')) + { + // group header + auto end = line.indexOf(']', 1); + if (end == -1 || end == 1) + { + // malformed header - either empty or no closing bracket + continue; + } + auto groupName = line.mid(1, end - 1); + + // it is against spec for the group name to already exist, but the + // parsing behavior for that case is not specified. operator[] will + // result in duplicate groups being merged, which makes the most + // sense for a read-only parser + entries = this->groups[groupName]; + + continue; + } + + // group entry + if (!entries.has_value()) + { + // no group header yet, entry before a group header is against spec + // and should be ignored + continue; + } + + auto delimiter = line.indexOf('='); + if (delimiter == -1) + { + // line is not a group header or a key value pair, ignore it + continue; + } + + auto key = QStringView(line).left(delimiter).trimmed().toString(); + // QStringView.mid() does not do bounds checking before qt 5.15, so + // we have to do it ourselves + auto valueStart = delimiter + 1; + QString value; + if (valueStart < line.size()) + { + value = QStringView(line).mid(valueStart).trimmed().toString(); + } + + // existing keys are against spec, so we can overwrite them with + // wild abandon + entries->get().emplace(key, value); + } +} + +XDGEntries XDGDesktopFile::getEntries(const QString &groupHeader) const +{ + auto group = this->groups.find(groupHeader); + if (group != this->groups.end()) + { + return group->second; + } + + return {}; +} + +std::optional XDGDesktopFile::findDesktopFile( + const QString &desktopFileID) +{ + for (const auto &dataDir : getXDGDirectories(XDGDirectoryType::Data)) + { + auto fileName = + QDir::cleanPath(dataDir + QDir::separator() + "applications" + + QDir::separator() + desktopFileID); + XDGDesktopFile desktopFile(fileName); + if (desktopFile.isValid()) + { + return desktopFile; + } + } + return {}; +} + +} // namespace chatterino + +#endif diff --git a/src/util/XDGDesktopFile.hpp b/src/util/XDGDesktopFile.hpp new file mode 100644 index 000000000..d61705c80 --- /dev/null +++ b/src/util/XDGDesktopFile.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include "util/QStringHash.hpp" + +#include +#include + +#if defined(Q_OS_UNIX) and !defined(Q_OS_DARWIN) + +namespace chatterino { + +// See https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#group-header +using XDGEntries = std::unordered_map; + +class XDGDesktopFile +{ +public: + // Read the file at `filename` as an XDG desktop file, parsing its groups & their entries + // + // Use the `isValid` function to check if the file was read properly + explicit XDGDesktopFile(const QString &filename); + + /// Returns a map of entries for the given group header + XDGEntries getEntries(const QString &groupHeader) const; + + /// isValid returns true if the file exists and is readable + bool isValid() const + { + return valid; + } + + /// Find the first desktop file based on the given desktop file ID + /// + /// This will look through all Data XDG directories + /// + /// Can return std::nullopt if no desktop file was found for the given desktop file ID + /// + /// References: https://specifications.freedesktop.org/desktop-entry-spec/latest/ar01s02.html#desktop-file-id + static std::optional findDesktopFile( + const QString &desktopFileID); + +private: + bool valid{}; + std::unordered_map groups; +}; + +} // namespace chatterino + +#endif diff --git a/src/util/XDGDirectory.cpp b/src/util/XDGDirectory.cpp new file mode 100644 index 000000000..ab401e50f --- /dev/null +++ b/src/util/XDGDirectory.cpp @@ -0,0 +1,78 @@ +#include "util/XDGDirectory.hpp" + +#include "util/CombinePath.hpp" + +#include + +namespace chatterino { + +#if defined(Q_OS_UNIX) and !defined(Q_OS_DARWIN) + +QStringList getXDGDirectories(XDGDirectoryType directory) +{ + // User XDG directory environment variables with defaults + // Defaults fetched from https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html#variables 2023-08-05 + static std::unordered_map> + userDirectories = { + { + XDGDirectoryType::Config, + { + "XDG_CONFIG_HOME", + combinePath(QDir::homePath(), ".config/"), + }, + }, + { + XDGDirectoryType::Data, + { + "XDG_DATA_HOME", + combinePath(QDir::homePath(), ".local/share/"), + }, + }, + }; + + // Base (or system) XDG directory environment variables with defaults + // Defaults fetched from https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html#variables 2023-08-05 + static std::unordered_map> + baseDirectories = { + { + XDGDirectoryType::Config, + { + "XDG_CONFIG_DIRS", + {"/etc/xdg"}, + }, + }, + { + XDGDirectoryType::Data, + { + "XDG_DATA_DIRS", + {"/usr/local/share/", "/usr/share/"}, + }, + }, + }; + + QStringList paths; + + const auto &[userEnvVar, userDefaultValue] = userDirectories.at(directory); + auto userEnvPath = qEnvironmentVariable(userEnvVar, userDefaultValue); + paths.push_back(userEnvPath); + + const auto &[baseEnvVar, baseDefaultValue] = baseDirectories.at(directory); + auto baseEnvPaths = + qEnvironmentVariable(baseEnvVar).split(':', Qt::SkipEmptyParts); + if (baseEnvPaths.isEmpty()) + { + paths.append(baseDefaultValue); + } + else + { + paths.append(baseEnvPaths); + } + + return paths; +} + +#endif + +} // namespace chatterino diff --git a/src/util/XDGDirectory.hpp b/src/util/XDGDirectory.hpp new file mode 100644 index 000000000..9a18ea25f --- /dev/null +++ b/src/util/XDGDirectory.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include + +namespace chatterino { + +#if defined(Q_OS_UNIX) and !defined(Q_OS_DARWIN) + +enum class XDGDirectoryType { + Config, + Data, +}; + +/// getXDGDirectories returns a list of directories given a directory type +/// +/// This will attempt to read the relevant environment variable (e.g. XDG_CONFIG_HOME and XDG_CONFIG_DIRS) and merge them, with sane defaults +QStringList getXDGDirectories(XDGDirectoryType directory); + +#endif + +} // namespace chatterino diff --git a/src/util/XDGHelper.cpp b/src/util/XDGHelper.cpp new file mode 100644 index 000000000..e6e2df63f --- /dev/null +++ b/src/util/XDGHelper.cpp @@ -0,0 +1,258 @@ +#include "util/XDGHelper.hpp" + +#include "common/Literals.hpp" +#include "common/QLogging.hpp" +#include "util/CombinePath.hpp" +#include "util/XDGDesktopFile.hpp" +#include "util/XDGDirectory.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include + +#if defined(Q_OS_UNIX) and !defined(Q_OS_DARWIN) + +using namespace chatterino::literals; + +namespace { + +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +const auto &LOG = chatterinoXDG; + +using namespace chatterino; + +const auto HTTPS_MIMETYPE = u"x-scheme-handler/https"_s; + +/// Read the given mimeapps file and try to find an association for the HTTPS_MIMETYPE +/// +/// If the mimeapps file is invalid (i.e. wasn't read), return nullopt +/// If the file is valid, look for the default Desktop File ID handler for the HTTPS_MIMETYPE +/// If no default Desktop File ID handler is found, populate `associations` +/// and `denyList` with Desktop File IDs from "Added Associations" and "Removed Associations" respectively +std::optional processMimeAppsList( + const QString &mimeappsPath, QStringList &associations, + std::unordered_set &denyList) +{ + XDGDesktopFile mimeappsFile(mimeappsPath); + if (!mimeappsFile.isValid()) + { + return {}; + } + + // get the list of Desktop File IDs for the given mimetype under the "Default + // Applications" group in the mimeapps.list file + auto defaultGroup = mimeappsFile.getEntries("Default Applications"); + auto defaultApps = defaultGroup.find(HTTPS_MIMETYPE); + if (defaultApps != defaultGroup.cend()) + { + // for each desktop ID in the list: + auto desktopIds = defaultApps->second.split(';', Qt::SkipEmptyParts); + for (const auto &entry : desktopIds) + { + auto desktopId = entry.trimmed(); + + // if a valid desktop file is found, verify that it is associated + // with the type. being in the default list gives it an implicit + // association, so just check that it's not in the denylist + if (!denyList.contains(desktopId)) + { + auto desktopFile = XDGDesktopFile::findDesktopFile(desktopId); + // if a valid association is found, we have found the default + // application + if (desktopFile.has_value()) + { + return desktopFile; + } + } + } + } + + // no definitive default application found. process added and removed + // associations, then return empty + + // load any removed associations into the denylist + auto removedGroup = mimeappsFile.getEntries("Removed Associations"); + auto removedApps = removedGroup.find(HTTPS_MIMETYPE); + if (removedApps != removedGroup.end()) + { + auto desktopIds = removedApps->second.split(';', Qt::SkipEmptyParts); + for (const auto &entry : desktopIds) + { + denyList.insert(entry.trimmed()); + } + } + + // append any created associations to the associations list + auto addedGroup = mimeappsFile.getEntries("Added Associations"); + auto addedApps = addedGroup.find(HTTPS_MIMETYPE); + if (addedApps != addedGroup.end()) + { + auto desktopIds = addedApps->second.split(';', Qt::SkipEmptyParts); + for (const auto &entry : desktopIds) + { + associations.push_back(entry.trimmed()); + } + } + + return {}; +} + +std::optional searchMimeAppsListsInDirectory( + const QString &directory, QStringList &associations, + std::unordered_set &denyList) +{ + static auto desktopNames = qEnvironmentVariable("XDG_CURRENT_DESKTOP") + .split(':', Qt::SkipEmptyParts); + static const QString desktopFilename = QStringLiteral("%1-mimeapps.list"); + static const QString nonDesktopFilename = QStringLiteral("mimeapps.list"); + + // try desktop specific mimeapps.list files first + for (const auto &desktopName : desktopNames) + { + auto fileName = + combinePath(directory, desktopFilename.arg(desktopName)); + auto defaultApp = processMimeAppsList(fileName, associations, denyList); + if (defaultApp.has_value()) + { + return defaultApp; + } + } + + // try the generic mimeapps.list + auto fileName = combinePath(directory, nonDesktopFilename); + auto defaultApp = processMimeAppsList(fileName, associations, denyList); + if (defaultApp.has_value()) + { + return defaultApp; + } + + // no definitive default application found + return {}; +} + +} // namespace + +namespace chatterino { + +/// Try to figure out the most reasonably default web browser to use +/// +/// If the `xdg-settings` program is available, use that +/// If not, read through all possible mimapps files in the order specified here: https://specifications.freedesktop.org/mime-apps-spec/mime-apps-spec-1.0.1.html#file +/// If no mimeapps file has a default, try to use the Added Associations in those files +std::optional getDefaultBrowserDesktopFile() +{ + // no xdg-utils, find it manually by searching mimeapps.list files + QStringList associations; + std::unordered_set denyList; + + // config dirs first + for (const auto &configDir : getXDGDirectories(XDGDirectoryType::Config)) + { + auto defaultApp = + searchMimeAppsListsInDirectory(configDir, associations, denyList); + if (defaultApp.has_value()) + { + return defaultApp; + } + } + + // data dirs for backwards compatibility + for (const auto &dataDir : getXDGDirectories(XDGDirectoryType::Data)) + { + auto appsDir = combinePath(dataDir, "applications"); + auto defaultApp = + searchMimeAppsListsInDirectory(appsDir, associations, denyList); + if (defaultApp.has_value()) + { + return defaultApp; + } + } + + // no mimeapps.list has an explicit default, use the most preferred added + // association that exists. We could search here for one we support... + if (!associations.empty()) + { + for (const auto &desktopId : associations) + { + auto desktopFile = XDGDesktopFile::findDesktopFile(desktopId); + if (desktopFile.has_value()) + { + return desktopFile; + } + } + } + + // use xdg-settings if installed + QProcess xdgSettings; + xdgSettings.start("xdg-settings", {"get", "default-web-browser"}, + QIODevice::ReadOnly); + xdgSettings.waitForFinished(1000); + if (xdgSettings.exitStatus() == QProcess::ExitStatus::NormalExit && + xdgSettings.error() == QProcess::UnknownError && + xdgSettings.exitCode() == 0) + { + return XDGDesktopFile::findDesktopFile( + xdgSettings.readAllStandardOutput().trimmed()); + } + + return {}; +} + +QString parseDesktopExecProgram(const QString &execKey) +{ + static const QRegularExpression unescapeReservedCharacters( + R"(\\(["`$\\]))"); + + QString program = execKey; + + // string values in desktop files escape all backslashes. This is an + // independent escaping scheme that must be processed first + program.replace(u"\\\\"_s, u"\\"_s); + + if (!program.startsWith('"')) + { + // not quoted, trim after the first space (if any) + auto end = program.indexOf(' '); + if (end != -1) + { + program = program.left(end); + } + } + else + { + // quoted + auto endQuote = program.indexOf('"', 1); + if (endQuote == -1) + { + // No end quote found, the returned program might be malformed + program = program.mid(1); + qCWarning(LOG).noquote().nospace() + << "Malformed desktop entry key " << program << ", originally " + << execKey << ", you might run into issues"; + } + else + { + // End quote found + program = program.mid(1, endQuote - 1); + } + } + + // program now contains the first token of the command line. + // this is either the program name with an absolute path, or just the program name + // denoting it's a relative path. Either will be handled by QProcess cleanly + // now, there is a second escaping scheme specific to the + // exec key that must be applied. + program.replace(unescapeReservedCharacters, "\\1"); + + return program; +} + +} // namespace chatterino + +#endif diff --git a/src/util/XDGHelper.hpp b/src/util/XDGHelper.hpp new file mode 100644 index 000000000..c862af936 --- /dev/null +++ b/src/util/XDGHelper.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include "util/XDGDesktopFile.hpp" + +#include + +namespace chatterino { + +#if defined(Q_OS_UNIX) and !defined(Q_OS_DARWIN) + +std::optional getDefaultBrowserDesktopFile(); + +/// Parses the given `execKey` and returns the resulting program name, ignoring all arguments +/// +/// Parsing is done in accordance to https://specifications.freedesktop.org/desktop-entry-spec/latest/ar01s07.html +/// +/// Note: We do *NOT* support field codes +QString parseDesktopExecProgram(const QString &execKey); + +#endif + +} // namespace chatterino diff --git a/src/util/serialize/Container.hpp b/src/util/serialize/Container.hpp new file mode 100644 index 000000000..66b7f240b --- /dev/null +++ b/src/util/serialize/Container.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include +#include + +#include + +namespace pajlada { + +template +struct Serialize, RJValue> { + static RJValue get(const std::unordered_map &value, + typename RJValue::AllocatorType &a) + { + RJValue ret(rapidjson::kObjectType); + + for (auto it = value.begin(); it != value.end(); ++it) + { + detail::AddMember(ret, it->first.toUtf8(), + it->second, a); + } + + return ret; + } +}; + +template +struct Deserialize, RJValue> { + static std::unordered_map get(const RJValue &value, + bool *error = nullptr) + { + std::unordered_map ret; + + if (!value.IsObject()) + { + PAJLADA_REPORT_ERROR(error) + return ret; + } + + for (typename RJValue::ConstMemberIterator it = value.MemberBegin(); + it != value.MemberEnd(); ++it) + { + ret.emplace(it->name.GetString(), + Deserialize::get(it->value, error)); + } + + return ret; + } +}; + +} // namespace pajlada diff --git a/src/widgets/AccountSwitchPopup.cpp b/src/widgets/AccountSwitchPopup.cpp index e940ef5d3..f94b94f7a 100644 --- a/src/widgets/AccountSwitchPopup.cpp +++ b/src/widgets/AccountSwitchPopup.cpp @@ -1,16 +1,22 @@ #include "widgets/AccountSwitchPopup.hpp" + +#include "common/Literals.hpp" +#include "singletons/Theme.hpp" +#include "widgets/AccountSwitchWidget.hpp" #include "widgets/dialogs/SettingsDialog.hpp" -#include #include #include #include -#include namespace chatterino { +using namespace literals; + AccountSwitchPopup::AccountSwitchPopup(QWidget *parent) - : BaseWindow({BaseWindow::TopMost, BaseWindow::Frameless}, parent) + : BaseWindow({BaseWindow::TopMost, BaseWindow::Frameless, + BaseWindow::DisableLayoutSave}, + parent) { #ifdef Q_OS_LINUX this->setWindowFlag(Qt::Popup); @@ -23,8 +29,8 @@ AccountSwitchPopup::AccountSwitchPopup(QWidget *parent) this->ui_.accountSwitchWidget->setFocusPolicy(Qt::NoFocus); vbox->addWidget(this->ui_.accountSwitchWidget); - auto hbox = new QHBoxLayout(); - auto manageAccountsButton = new QPushButton(this); + auto *hbox = new QHBoxLayout(); + auto *manageAccountsButton = new QPushButton(this); manageAccountsButton->setText("Manage Accounts"); manageAccountsButton->setFocusPolicy(Qt::NoFocus); hbox->addWidget(manageAccountsButton); @@ -37,6 +43,48 @@ AccountSwitchPopup::AccountSwitchPopup(QWidget *parent) this->getLayoutContainer()->setLayout(vbox); this->setScaleIndependantSize(200, 200); + this->themeChangedEvent(); +} + +void AccountSwitchPopup::themeChangedEvent() +{ + BaseWindow::themeChangedEvent(); + + auto *t = getTheme(); + auto color = [](const QColor &c) { + return c.name(QColor::HexArgb); + }; + this->setStyleSheet(uR"( + QListView { + color: %1; + background: %2; + } + QListView::item:hover { + background: %3; + } + QListView::item:selected { + background: %4; + } + + QPushButton { + background: %5; + color: %1; + } + QPushButton:hover { + background: %3; + } + QPushButton:pressed { + background: %6; + } + + chatterino--AccountSwitchPopup { + background: %7; + } + )"_s.arg(color(t->window.text), color(t->splits.header.background), + color(t->splits.header.focusedBackground), color(t->accent), + color(t->tabs.regular.backgrounds.regular), + color(t->tabs.selected.backgrounds.regular), + color(t->window.background))); } void AccountSwitchPopup::refresh() diff --git a/src/widgets/AccountSwitchPopup.hpp b/src/widgets/AccountSwitchPopup.hpp index 619bfbe20..68d7b69b8 100644 --- a/src/widgets/AccountSwitchPopup.hpp +++ b/src/widgets/AccountSwitchPopup.hpp @@ -1,12 +1,13 @@ #pragma once -#include "widgets/AccountSwitchWidget.hpp" #include "widgets/BaseWindow.hpp" #include namespace chatterino { +class AccountSwitchWidget; + class AccountSwitchPopup : public BaseWindow { Q_OBJECT @@ -20,6 +21,8 @@ protected: void focusOutEvent(QFocusEvent *event) final; void paintEvent(QPaintEvent *event) override; + void themeChangedEvent() override; + private: struct { AccountSwitchWidget *accountSwitchWidget = nullptr; diff --git a/src/widgets/AccountSwitchWidget.cpp b/src/widgets/AccountSwitchWidget.cpp index b7a5eefc9..5afd68392 100644 --- a/src/widgets/AccountSwitchWidget.cpp +++ b/src/widgets/AccountSwitchWidget.cpp @@ -1,57 +1,62 @@ -#include "AccountSwitchWidget.hpp" +#include "widgets/AccountSwitchWidget.hpp" #include "Application.hpp" #include "common/Common.hpp" #include "controllers/accounts/AccountController.hpp" #include "providers/twitch/TwitchAccount.hpp" #include "providers/twitch/TwitchCommon.hpp" +#include "singletons/Settings.hpp" namespace chatterino { AccountSwitchWidget::AccountSwitchWidget(QWidget *parent) : QListWidget(parent) { - auto app = getApp(); + auto *app = getApp(); this->addItem(ANONYMOUS_USERNAME_LABEL); - for (const auto &userName : app->accounts->twitch.getUsernames()) + for (const auto &userName : app->getAccounts()->twitch.getUsernames()) { this->addItem(userName); } - app->accounts->twitch.userListUpdated.connect([=]() { - this->blockSignals(true); + this->managedConnections_.managedConnect( + app->getAccounts()->twitch.userListUpdated, [=, this]() { + this->blockSignals(true); - this->clear(); + this->clear(); - this->addItem(ANONYMOUS_USERNAME_LABEL); + this->addItem(ANONYMOUS_USERNAME_LABEL); - for (const auto &userName : app->accounts->twitch.getUsernames()) - { - this->addItem(userName); - } + for (const auto &userName : + app->getAccounts()->twitch.getUsernames()) + { + this->addItem(userName); + } - this->refreshSelection(); + this->refreshSelection(); - this->blockSignals(false); - }); + this->blockSignals(false); + }); this->refreshSelection(); - QObject::connect(this, &QListWidget::clicked, [=] { + QObject::connect(this, &QListWidget::clicked, [=, this] { if (!this->selectedItems().isEmpty()) { QString newUsername = this->currentItem()->text(); if (newUsername.compare(ANONYMOUS_USERNAME_LABEL, Qt::CaseInsensitive) == 0) { - app->accounts->twitch.currentUsername = ""; + app->getAccounts()->twitch.currentUsername = ""; } else { - app->accounts->twitch.currentUsername = newUsername; + app->getAccounts()->twitch.currentUsername = newUsername; } + + getSettings()->requestSave(); } }); } @@ -68,9 +73,9 @@ void AccountSwitchWidget::refreshSelection() // Select the currently logged in user if (this->count() > 0) { - auto app = getApp(); + auto *app = getApp(); - auto currentUser = app->accounts->twitch.getCurrent(); + auto currentUser = app->getAccounts()->twitch.getCurrent(); if (currentUser->isAnon()) { diff --git a/src/widgets/AccountSwitchWidget.hpp b/src/widgets/AccountSwitchWidget.hpp index 8d236766d..97a0fdd94 100644 --- a/src/widgets/AccountSwitchWidget.hpp +++ b/src/widgets/AccountSwitchWidget.hpp @@ -1,5 +1,7 @@ #pragma once +#include "pajlada/signals/signalholder.hpp" + #include namespace chatterino { @@ -15,6 +17,8 @@ public: private: void refreshSelection(); + + pajlada::Signals::SignalHolder managedConnections_; }; } // namespace chatterino diff --git a/src/widgets/AttachedWindow.cpp b/src/widgets/AttachedWindow.cpp index 9ac753345..b83afb65d 100644 --- a/src/widgets/AttachedWindow.cpp +++ b/src/widgets/AttachedWindow.cpp @@ -1,7 +1,6 @@ -#include "AttachedWindow.hpp" +#include "widgets/AttachedWindow.hpp" #include "Application.hpp" -#include "ForwardDecl.hpp" #include "common/QLogging.hpp" #include "singletons/Settings.hpp" #include "util/DebugCount.hpp" @@ -9,14 +8,17 @@ #include #include + #include #ifdef USEWINSDK # include "util/WindowsHelper.hpp" -# include "Windows.h" +// clang-format off // don't even think about reordering these +# include "Windows.h" # include "Psapi.h" +// clang-format on # pragma comment(lib, "Dwmapi.lib") #endif @@ -48,7 +50,7 @@ AttachedWindow::AttachedWindow(void *_target, int _yOffset) , yOffset_(_yOffset) { QLayout *layout = new QVBoxLayout(this); - layout->setMargin(0); + layout->setContentsMargins(0, 0, 0, 0); this->setLayout(layout); auto *split = new Split(this); @@ -133,6 +135,13 @@ AttachedWindow *AttachedWindow::get(void *target, const GetArgs &args) return window; } +#ifdef USEWINSDK +AttachedWindow *AttachedWindow::getForeground(const GetArgs &args) +{ + return AttachedWindow::get(::GetForegroundWindow(), args); +} +#endif + void AttachedWindow::detach(const QString &winId) { for (Item &item : items) @@ -261,20 +270,22 @@ void AttachedWindow::updateWindowRect(void *_attachedPtr) } float scale = 1.f; + float ourScale = 1.F; if (auto dpi = getWindowDpi(attached)) { - scale = dpi.get() / 96.f; + scale = *dpi / 96.f; + ourScale = scale / this->devicePixelRatio(); for (auto w : this->ui_.split->findChildren()) { - w->setOverrideScale(scale); + w->setOverrideScale(ourScale); } - this->ui_.split->setOverrideScale(scale); + this->ui_.split->setOverrideScale(ourScale); } if (this->height_ != -1) { - this->ui_.split->setFixedWidth(int(this->width_ * scale)); + this->ui_.split->setFixedWidth(int(this->width_ * ourScale)); // offset int o = this->fullscreen_ ? 0 : 8; diff --git a/src/widgets/AttachedWindow.hpp b/src/widgets/AttachedWindow.hpp index 26d3333d9..3f863cc32 100644 --- a/src/widgets/AttachedWindow.hpp +++ b/src/widgets/AttachedWindow.hpp @@ -4,6 +4,7 @@ #include #include + #include namespace chatterino { @@ -27,22 +28,25 @@ public: bool fullscreen = false; }; - virtual ~AttachedWindow() override; + ~AttachedWindow() override; static AttachedWindow *get(void *target_, const GetArgs &args); +#ifdef USEWINSDK + static AttachedWindow *getForeground(const GetArgs &args); +#endif static void detach(const QString &winId); void setChannel(ChannelPtr channel); protected: - virtual void showEvent(QShowEvent *) override; + void showEvent(QShowEvent *) override; // virtual void nativeEvent(const QByteArray &eventType, void *message, // long *result) override; private: struct { Split *split; - } ui_; + } ui_{}; struct Item { void *hwnd; @@ -57,7 +61,7 @@ private: void *target_; int yOffset_; - int currentYOffset_; + int currentYOffset_{}; double x_ = -1; double pixelRatio_ = -1; int width_ = 360; diff --git a/src/widgets/BaseWidget.cpp b/src/widgets/BaseWidget.cpp index 7a1654924..f2a6160d6 100644 --- a/src/widgets/BaseWidget.cpp +++ b/src/widgets/BaseWidget.cpp @@ -1,9 +1,9 @@ #include "widgets/BaseWidget.hpp" -#include "BaseSettings.hpp" -#include "BaseTheme.hpp" +#include "Application.hpp" #include "common/QLogging.hpp" #include "controllers/hotkeys/HotkeyController.hpp" +#include "singletons/Theme.hpp" #include "widgets/BaseWindow.hpp" #include @@ -11,16 +11,15 @@ #include #include #include + #include namespace chatterino { BaseWidget::BaseWidget(QWidget *parent, Qt::WindowFlags f) : QWidget(parent, f) + , theme(getApp()->getThemes()) { - // REMOVED - this->theme = getTheme(); - this->signalHolder_.managedConnect(this->theme->updated, [this]() { this->themeChangedEvent(); @@ -29,7 +28,7 @@ BaseWidget::BaseWidget(QWidget *parent, Qt::WindowFlags f) } void BaseWidget::clearShortcuts() { - for (auto shortcut : this->shortcuts_) + for (auto *shortcut : this->shortcuts_) { shortcut->setKey(QKeySequence()); shortcut->removeEventFilter(this); @@ -42,21 +41,24 @@ float BaseWidget::scale() const { if (this->overrideScale_) { - return this->overrideScale_.get(); + return *this->overrideScale_; } - else if (auto baseWidget = dynamic_cast(this->window())) + + if (auto *baseWidget = dynamic_cast(this->window())) { return baseWidget->scale_; } - else - { - return 1.f; - } + + return 1.F; } void BaseWidget::setScale(float value) { - // update scale value + if (this->scale_ == value) + { + return; + } + this->scale_ = value; this->scaleChangedEvent(this->scale()); @@ -65,13 +67,13 @@ void BaseWidget::setScale(float value) this->setScaleIndependantSize(this->scaleIndependantSize()); } -void BaseWidget::setOverrideScale(boost::optional value) +void BaseWidget::setOverrideScale(std::optional value) { this->overrideScale_ = value; this->setScale(this->scale()); } -boost::optional BaseWidget::overrideScale() const +std::optional BaseWidget::overrideScale() const { return this->overrideScale_; } @@ -122,25 +124,12 @@ void BaseWidget::setScaleIndependantHeight(int value) QSize(this->scaleIndependantSize_.width(), value)); } -float BaseWidget::qtFontScale() const -{ - if (auto window = dynamic_cast(this->window())) - { - // ensure no div by 0 - return this->scale() / std::max(0.01f, window->nativeScale_); - } - else - { - return this->scale(); - } -} - void BaseWidget::childEvent(QChildEvent *event) { if (event->added()) { // add element if it's a basewidget - if (auto widget = dynamic_cast(event->child())) + if (auto *widget = dynamic_cast(event->child())) { this->widgets_.push_back(widget); } diff --git a/src/widgets/BaseWidget.hpp b/src/widgets/BaseWidget.hpp index 2b415a1d9..4fdc421cd 100644 --- a/src/widgets/BaseWidget.hpp +++ b/src/widgets/BaseWidget.hpp @@ -1,10 +1,11 @@ #pragma once -#include -#include -#include #include #include +#include +#include + +#include namespace chatterino { @@ -22,8 +23,8 @@ public: virtual float scale() const; pajlada::Signals::Signal scaleChanged; - boost::optional overrideScale() const; - void setOverrideScale(boost::optional); + std::optional overrideScale() const; + void setOverrideScale(std::optional); QSize scaleIndependantSize() const; int scaleIndependantWidth() const; @@ -33,11 +34,9 @@ public: void setScaleIndependantWidth(int value); void setScaleIndependantHeight(int value); - float qtFontScale() const; - protected: - virtual void childEvent(QChildEvent *) override; - virtual void showEvent(QShowEvent *) override; + void childEvent(QChildEvent *) override; + void showEvent(QShowEvent *) override; virtual void scaleChangedEvent(float newScale); virtual void themeChangedEvent(); @@ -56,7 +55,7 @@ protected: private: float scale_{1.f}; - boost::optional overrideScale_; + std::optional overrideScale_; QSize scaleIndependantSize_; std::vector widgets_; diff --git a/src/widgets/BaseWindow.cpp b/src/widgets/BaseWindow.cpp index 008c55a69..97dabb672 100644 --- a/src/widgets/BaseWindow.cpp +++ b/src/widgets/BaseWindow.cpp @@ -1,46 +1,201 @@ -#include "BaseWindow.hpp" +#include "widgets/BaseWindow.hpp" -#include "BaseSettings.hpp" -#include "BaseTheme.hpp" -#include "boost/algorithm/algorithm.hpp" +#include "Application.hpp" +#include "common/QLogging.hpp" +#include "singletons/Settings.hpp" +#include "singletons/Theme.hpp" +#include "singletons/WindowManager.hpp" #include "util/DebugCount.hpp" #include "util/PostToThread.hpp" #include "util/WindowsHelper.hpp" -#include "widgets/Label.hpp" -#include "widgets/TooltipWidget.hpp" #include "widgets/helper/EffectLabel.hpp" +#include "widgets/helper/TitlebarButtons.hpp" +#include "widgets/Label.hpp" +#include "widgets/Window.hpp" #include -#include -#include #include #include +#include +#include + #include -#ifdef CHATTERINO -# include "Application.hpp" -# include "singletons/WindowManager.hpp" -#endif - #ifdef USEWINSDK -# include +# include # include # include -# include -# include # include -//#include # pragma comment(lib, "Dwmapi.lib") # include -# include - -# define WM_DPICHANGED 0x02E0 +# include +# include +# include #endif #include "widgets/helper/TitlebarButton.hpp" +namespace { + +#ifdef USEWINSDK + +// From kHiddenTaskbarSize in Firefox +constexpr UINT HIDDEN_TASKBAR_SIZE = 2; + +bool isWindows11OrGreater() +{ + static const bool result = [] { + // This calls RtlGetVersion under the hood so we don't have to. + // The micro version corresponds to dwBuildNumber. + auto version = QOperatingSystemVersion::current(); + return (version.majorVersion() > 10) || + (version.microVersion() >= 22000); + }(); + + return result; +} + +/// Finds the taskbar HWND on a specific monitor (or any) +HWND findTaskbarWindow(LPRECT rcMon = nullptr) +{ + HWND taskbar = nullptr; + RECT taskbarRect; + // return value of IntersectRect, unused + RECT intersectionRect; + + while ((taskbar = FindWindowEx(nullptr, taskbar, L"Shell_TrayWnd", + nullptr)) != nullptr) + { + if (!rcMon) + { + // no monitor was specified, return the first encountered window + break; + } + if (GetWindowRect(taskbar, &taskbarRect) != 0 && + IntersectRect(&intersectionRect, &taskbarRect, rcMon) != 0) + { + // taskbar intersects with the monitor - this is the one + break; + } + } + + return taskbar; +} + +/// Gets the edge of the taskbar if it's automatically hidden +std::optional hiddenTaskbarEdge(LPRECT rcMon = nullptr) +{ + HWND taskbar = findTaskbarWindow(rcMon); + if (!taskbar) + { + return std::nullopt; + } + + APPBARDATA state = {sizeof(state), taskbar}; + APPBARDATA pos = {sizeof(pos), taskbar}; + + auto appBarState = + static_cast(SHAppBarMessage(ABM_GETSTATE, &state)); + if ((appBarState & ABS_AUTOHIDE) == 0) + { + return std::nullopt; + } + + if (SHAppBarMessage(ABM_GETTASKBARPOS, &pos) == 0) + { + qCDebug(chatterinoApp) << "Failed to get taskbar pos"; + return ABE_BOTTOM; + } + + return pos.uEdge; +} + +/// @brief Gets the window borders for @a hwnd +/// +/// Each side of the returned RECT has the correct sign, so they can be added +/// to a window rect. +/// Shrinking by 1px would return {left: 1, top: 1, right: -1, left: -1}. +RECT windowBordersFor(HWND hwnd, bool isMaximized) +{ + RECT margins{0, 0, 0, 0}; + + auto addBorders = isMaximized || isWindows11OrGreater(); + if (addBorders) + { + // GetDpiForWindow and GetSystemMetricsForDpi are only supported on + // Windows 10 and later. Qt 6 requires Windows 10. +# if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + auto dpi = GetDpiForWindow(hwnd); +# endif + + auto systemMetric = [&](auto index) { +# if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + if (dpi != 0) + { + return GetSystemMetricsForDpi(index, dpi); + } +# endif + return GetSystemMetrics(index); + }; + + auto paddedBorder = systemMetric(SM_CXPADDEDBORDER); + auto borderWidth = systemMetric(SM_CXSIZEFRAME) + paddedBorder; + auto borderHeight = systemMetric(SM_CYSIZEFRAME) + paddedBorder; + + margins.left += borderWidth; + margins.right -= borderWidth; + if (isMaximized) + { + margins.top += borderHeight; + } + margins.bottom -= borderHeight; + } + + if (isMaximized) + { + auto *hMonitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); + MONITORINFO mi; + mi.cbSize = sizeof(mi); + auto *monitor = [&]() -> LPRECT { + if (GetMonitorInfo(hMonitor, &mi)) + { + return &mi.rcMonitor; + } + return nullptr; + }(); + + auto edge = hiddenTaskbarEdge(monitor); + if (edge) + { + switch (*edge) + { + case ABE_LEFT: + margins.left += HIDDEN_TASKBAR_SIZE; + break; + case ABE_RIGHT: + margins.right -= HIDDEN_TASKBAR_SIZE; + break; + case ABE_TOP: + margins.top += HIDDEN_TASKBAR_SIZE; + break; + case ABE_BOTTOM: + margins.bottom -= HIDDEN_TASKBAR_SIZE; + break; + default: + break; + } + } + } + + return margins; +} + +#endif + +} // namespace + namespace chatterino { BaseWindow::BaseWindow(FlagsEnum _flags, QWidget *parent) @@ -64,7 +219,7 @@ BaseWindow::BaseWindow(FlagsEnum _flags, QWidget *parent) this->setWindowFlags(Qt::ToolTip); #else this->setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | - Qt::X11BypassWindowManagerHint | + Qt::WindowDoesNotAcceptFocus | Qt::BypassWindowManagerHint); #endif } @@ -75,10 +230,9 @@ BaseWindow::BaseWindow(FlagsEnum _flags, QWidget *parent) [this]() { postToThread([this] { this->updateScale(); - this->updateScale(); }); }, - this->connections_); + this->connections_, false); this->updateScale(); @@ -87,7 +241,7 @@ BaseWindow::BaseWindow(FlagsEnum _flags, QWidget *parent) #ifdef USEWINSDK this->useNextBounds_.setSingleShot(true); QObject::connect(&this->useNextBounds_, &QTimer::timeout, this, [this]() { - this->currentBounds_ = this->nextBounds_; + this->currentBounds_ = this->geometry(); }); #endif @@ -100,8 +254,9 @@ BaseWindow::~BaseWindow() DebugCount::decrease("BaseWindow"); } -void BaseWindow::setInitialBounds(const QRect &bounds) +void BaseWindow::setInitialBounds(QRect bounds, widgets::BoundsChecking mode) { + bounds = widgets::checkInitialBounds(bounds, mode); #ifdef USEWINSDK this->initalBounds_ = bounds; #else @@ -109,7 +264,7 @@ void BaseWindow::setInitialBounds(const QRect &bounds) #endif } -QRect BaseWindow::getBounds() +QRect BaseWindow::getBounds() const { #ifdef USEWINSDK return this->currentBounds_; @@ -123,141 +278,152 @@ float BaseWindow::scale() const return std::max(0.01f, this->overrideScale().value_or(this->scale_)); } -float BaseWindow::qtFontScale() const -{ - return this->scale() / std::max(0.01, this->nativeScale_); -} - void BaseWindow::init() { #ifdef USEWINSDK if (this->hasCustomWindowFrame()) { // CUSTOM WINDOW FRAME - QVBoxLayout *layout = new QVBoxLayout(); + auto *layout = new QVBoxLayout(this); this->ui_.windowLayout = layout; - layout->setContentsMargins(1, 1, 1, 1); + layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); - this->setLayout(layout); + + if (!this->frameless_) { - if (!this->frameless_) - { - QHBoxLayout *buttonLayout = this->ui_.titlebarBox = - new QHBoxLayout(); - buttonLayout->setMargin(0); - layout->addLayout(buttonLayout); + QHBoxLayout *buttonLayout = this->ui_.titlebarBox = + new QHBoxLayout(); + buttonLayout->setContentsMargins(0, 0, 0, 0); + layout->addLayout(buttonLayout); - // title - Label *title = new Label; - QObject::connect(this, &QWidget::windowTitleChanged, - [title](const QString &text) { - title->setText(text); - }); + // title + Label *title = new Label; + QObject::connect(this, &QWidget::windowTitleChanged, + [title](const QString &text) { + title->setText(text); + }); - QSizePolicy policy(QSizePolicy::Ignored, - QSizePolicy::Preferred); - policy.setHorizontalStretch(1); - title->setSizePolicy(policy); - buttonLayout->addWidget(title); - this->ui_.titleLabel = title; + QSizePolicy policy(QSizePolicy::Ignored, QSizePolicy::Preferred); + policy.setHorizontalStretch(1); + title->setSizePolicy(policy); + buttonLayout->addWidget(title); + this->ui_.titleLabel = title; - // buttons - TitleBarButton *_minButton = new TitleBarButton; - _minButton->setButtonStyle(TitleBarButtonStyle::Minimize); - TitleBarButton *_maxButton = new TitleBarButton; - _maxButton->setButtonStyle(TitleBarButtonStyle::Maximize); - TitleBarButton *_exitButton = new TitleBarButton; - _exitButton->setButtonStyle(TitleBarButtonStyle::Close); + // buttons + auto *minButton = new TitleBarButton; + minButton->setButtonStyle(TitleBarButtonStyle::Minimize); + auto *maxButton = new TitleBarButton; + maxButton->setButtonStyle(TitleBarButtonStyle::Maximize); + auto *exitButton = new TitleBarButton; + exitButton->setButtonStyle(TitleBarButtonStyle::Close); - QObject::connect(_minButton, &TitleBarButton::leftClicked, this, - [this] { - this->setWindowState(Qt::WindowMinimized | - this->windowState()); - }); - QObject::connect(_maxButton, &TitleBarButton::leftClicked, this, - [this, _maxButton] { - this->setWindowState( - _maxButton->getButtonStyle() != + QObject::connect(minButton, &TitleBarButton::leftClicked, this, + [this] { + this->setWindowState(Qt::WindowMinimized | + this->windowState()); + }); + QObject::connect( + maxButton, &TitleBarButton::leftClicked, this, + [this, maxButton] { + this->setWindowState(maxButton->getButtonStyle() != TitleBarButtonStyle::Maximize ? Qt::WindowActive : Qt::WindowMaximized); - }); - QObject::connect(_exitButton, &TitleBarButton::leftClicked, - this, [this] { - this->close(); - }); + }); + QObject::connect(exitButton, &TitleBarButton::leftClicked, this, + [this] { + this->close(); + }); - this->ui_.minButton = _minButton; - this->ui_.maxButton = _maxButton; - this->ui_.exitButton = _exitButton; + this->ui_.titlebarButtons = + new TitleBarButtons(this, minButton, maxButton, exitButton); - this->ui_.buttons.push_back(_minButton); - this->ui_.buttons.push_back(_maxButton); - this->ui_.buttons.push_back(_exitButton); + this->ui_.buttons.push_back(minButton); + this->ui_.buttons.push_back(maxButton); + this->ui_.buttons.push_back(exitButton); - // buttonLayout->addStretch(1); - buttonLayout->addWidget(_minButton); - buttonLayout->addWidget(_maxButton); - buttonLayout->addWidget(_exitButton); - buttonLayout->setSpacing(0); - } + buttonLayout->addWidget(minButton); + buttonLayout->addWidget(maxButton); + buttonLayout->addWidget(exitButton); + buttonLayout->setSpacing(0); } + this->ui_.layoutBase = new BaseWidget(this); this->ui_.layoutBase->setContentsMargins(1, 0, 1, 1); layout->addWidget(this->ui_.layoutBase); } - -// DPI -// auto dpi = getWindowDpi(this->winId()); - -// if (dpi) { -// this->scale = dpi.value() / 96.f; -// } #endif -#ifdef USEWINSDK - // fourtf: don't ask me why we need to delay this - if (!this->flags_.has(TopMost)) - { - QTimer::singleShot(1, this, [this] { - getSettings()->windowTopMost.connect( - [this](bool topMost, auto) { - ::SetWindowPos(HWND(this->winId()), - topMost ? HWND_TOPMOST : HWND_NOTOPMOST, 0, - 0, 0, 0, - SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); - }, - this->connections_); - }); - } -#else // TopMost flag overrides setting if (!this->flags_.has(TopMost)) { getSettings()->windowTopMost.connect( - [this](bool topMost, auto) { - auto isVisible = this->isVisible(); - this->setWindowFlag(Qt::WindowStaysOnTopHint, topMost); - if (isVisible) - { - this->show(); - } + [this](bool topMost) { + this->setTopMost(topMost); }, this->connections_); } +} + +void BaseWindow::setTopMost(bool topMost) +{ + if (this->flags_.has(TopMost)) + { + qCWarning(chatterinoWidget) + << "Called setTopMost on a window with the `TopMost` flag set."; + return; + } + + if (this->isTopMost_ == topMost) + { + return; + } + this->isTopMost_ = topMost; + +#ifdef USEWINSDK + if (!this->waitingForTopMost_) + { + this->tryApplyTopMost(); + } +#else + auto isVisible = this->isVisible(); + this->setWindowFlag(Qt::WindowStaysOnTopHint, topMost); + if (isVisible) + { + this->show(); + } #endif + + this->topMostChanged(this->isTopMost_); } -void BaseWindow::setStayInScreenRect(bool value) +#ifdef USEWINSDK +void BaseWindow::tryApplyTopMost() { - this->stayInScreenRect_ = value; + auto hwnd = this->safeHWND(); + if (!hwnd) + { + this->waitingForTopMost_ = true; + QTimer::singleShot(50, this, &BaseWindow::tryApplyTopMost); + return; + } + this->waitingForTopMost_ = false; - this->moveIntoDesktopRect(this, this->pos()); + if (this->parent()) + { + // Don't change the topmost value of child windows. This would apply + // to the top-level window too. + return; + } + + ::SetWindowPos(*hwnd, this->isTopMost_ ? HWND_TOPMOST : HWND_NOTOPMOST, 0, + 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); } +#endif -bool BaseWindow::getStayInScreenRect() const +bool BaseWindow::isTopMost() const { - return this->stayInScreenRect_; + return this->isTopMost_ || this->flags_.has(TopMost); } void BaseWindow::setActionOnFocusLoss(ActionOnFocusLoss value) @@ -282,7 +448,7 @@ QWidget *BaseWindow::getLayoutContainer() } } -bool BaseWindow::hasCustomWindowFrame() +bool BaseWindow::hasCustomWindowFrame() const { return BaseWindow::supportsCustomWindowFrame() && this->enableCustomFrame_; } @@ -338,19 +504,38 @@ bool BaseWindow::event(QEvent *event) this->onFocusLost(); } +#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) + if (this->flags_.hasAny(DontFocus, Dialog, FramelessDraggable)) + { + // This certain windows (e.g. TooltipWidget, input completion widget, and the search popup) retains their nullptr parent + // NOTE that this currently does not retain their original transient parent (which is the window it was created under) + // For now, we haven't noticed that this creates any issues, and I don't know of a good place to store the previous transient + // parent to restore it. + if (event->type() == QEvent::ParentWindowChange) + { + assert(this->windowHandle() != nullptr); + if (this->windowHandle()->parent() != nullptr) + { + this->windowHandle()->setParent(nullptr); + } + } + } +#endif + return QWidget::event(event); } void BaseWindow::wheelEvent(QWheelEvent *event) { - if (event->orientation() != Qt::Vertical) + // ignore horizontal mouse wheels + if (event->angleDelta().x() != 0) { return; } if (event->modifiers() & Qt::ControlModifier) { - if (event->delta() > 0) + if (event->angleDelta().y() > 0) { getSettings()->setClampedUiScale( getSettings()->getClampedUiScale() + 0.1); @@ -392,12 +577,12 @@ void BaseWindow::mousePressEvent(QMouseEvent *event) if (this->flags_.has(FramelessDraggable)) { this->movingRelativePos = event->localPos(); - if (auto widget = + if (auto *widget = this->childAt(event->localPos().x(), event->localPos().y())) { std::function recursiveCheckMouseTracking; recursiveCheckMouseTracking = [&](QWidget *widget) { - if (widget == nullptr) + if (widget == nullptr || widget->isHidden()) { return false; } @@ -486,28 +671,23 @@ EffectLabel *BaseWindow::addTitleBarLabel(std::function onClicked) void BaseWindow::changeEvent(QEvent *) { - if (this->isVisible()) - { - TooltipWidget::instance()->hide(); - } - #ifdef USEWINSDK - if (this->ui_.maxButton) + if (this->ui_.titlebarButtons) { - this->ui_.maxButton->setButtonStyle( - this->windowState() & Qt::WindowMaximized - ? TitleBarButtonStyle::Unmaximize - : TitleBarButtonStyle::Maximize); + this->ui_.titlebarButtons->updateMaxButton(); } if (this->isVisible() && this->hasCustomWindowFrame()) { - auto palette = this->palette(); - palette.setColor(QPalette::Window, - GetForegroundWindow() == HWND(this->winId()) - ? QColor(90, 90, 90) - : QColor(50, 50, 50)); - this->setPalette(palette); + auto hwnd = this->safeHWND(); + if (hwnd) + { + auto palette = this->palette(); + palette.setColor(QPalette::Window, GetForegroundWindow() == *hwnd + ? QColor(90, 90, 90) + : QColor(50, 50, 50)); + this->setPalette(palette); + } } #endif @@ -518,57 +698,56 @@ void BaseWindow::changeEvent(QEvent *) void BaseWindow::leaveEvent(QEvent *) { - TooltipWidget::instance()->hide(); + this->leaving.invoke(); } -void BaseWindow::moveTo(QWidget *parent, QPoint point, bool offset) +void BaseWindow::moveTo(QPoint point, widgets::BoundsChecking mode) { - if (offset) + this->lastBoundsCheckPosition_ = point; + this->lastBoundsCheckMode_ = mode; + widgets::moveWindowTo(this, point, mode); +} + +void BaseWindow::showAndMoveTo(QPoint point, widgets::BoundsChecking mode) +{ + this->lastBoundsCheckPosition_ = point; + this->lastBoundsCheckMode_ = mode; + widgets::showAndMoveWindowTo(this, point, mode); +} + +bool BaseWindow::applyLastBoundsCheck() +{ + if (this->lastBoundsCheckMode_ == widgets::BoundsChecking::Off) { - point.rx() += 16; - point.ry() += 16; + return false; } - this->moveIntoDesktopRect(parent, point); + this->moveTo(this->lastBoundsCheckPosition_, this->lastBoundsCheckMode_); + return true; } void BaseWindow::resizeEvent(QResizeEvent *) { // Queue up save because: Window resized -#ifdef CHATTERINO - getApp()->windows->queueSave(); -#endif - - //this->moveIntoDesktopRect(this); + if (!flags_.has(DisableLayoutSave)) + { + getApp()->getWindows()->queueSave(); + } #ifdef USEWINSDK - if (this->hasCustomWindowFrame() && !this->isResizeFixing_) - { - this->isResizeFixing_ = true; - QTimer::singleShot(50, this, [this] { - RECT rect; - ::GetWindowRect((HWND)this->winId(), &rect); - ::SetWindowPos((HWND)this->winId(), nullptr, 0, 0, - rect.right - rect.left + 1, rect.bottom - rect.top, - SWP_NOMOVE | SWP_NOZORDER); - ::SetWindowPos((HWND)this->winId(), nullptr, 0, 0, - rect.right - rect.left, rect.bottom - rect.top, - SWP_NOMOVE | SWP_NOZORDER); - QTimer::singleShot(10, this, [this] { - this->isResizeFixing_ = false; - }); - }); - } -#endif - this->calcButtonsSizes(); + this->updateRealSize(); +#endif } void BaseWindow::moveEvent(QMoveEvent *event) { // Queue up save because: Window position changed #ifdef CHATTERINO - getApp()->windows->queueSave(); + if (!flags_.has(DisableLayoutSave)) + { + getApp()->getWindows()->queueSave(); + } #endif BaseWidget::moveEvent(event); @@ -581,70 +760,44 @@ void BaseWindow::closeEvent(QCloseEvent *) void BaseWindow::showEvent(QShowEvent *) { - this->moveIntoDesktopRect(this, this->pos()); - if (this->frameless_) +#ifdef Q_OS_WIN + if (this->flags_.has(BoundsCheckOnShow)) { - QTimer::singleShot(30, this, [this] { - this->moveIntoDesktopRect(this, this->pos()); + this->moveTo(this->pos(), widgets::BoundsChecking::CursorPosition); + } + + if (!this->flags_.has(TopMost)) + { + QTimer::singleShot(1, this, [this] { + if (!this->waitingForTopMost_) + { + this->tryApplyTopMost(); + } }); } +#endif } -void BaseWindow::moveIntoDesktopRect(QWidget *parent, QPoint point) -{ - if (!this->stayInScreenRect_) - return; - - // move the widget into the screen geometry if it's not already in there - QDesktopWidget *desktop = QApplication::desktop(); - QPoint globalCursorPos = QCursor::pos(); - - QRect s = desktop->availableGeometry(parent); - - bool stickRight = false; - bool stickBottom = false; - - if (point.x() < s.left()) - { - point.setX(s.left()); - } - if (point.y() < s.top()) - { - point.setY(s.top()); - } - if (point.x() + this->width() > s.right()) - { - stickRight = true; - point.setX(s.right() - this->width()); - } - if (point.y() + this->height() > s.bottom()) - { - stickBottom = true; - point.setY(s.bottom() - this->height()); - } - - if (stickRight && stickBottom) - { - point.setY(globalCursorPos.y() - this->height() - 16); - } - - this->move(point); -} - +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +bool BaseWindow::nativeEvent(const QByteArray &eventType, void *message, + qintptr *result) +#else bool BaseWindow::nativeEvent(const QByteArray &eventType, void *message, long *result) +#endif { #ifdef USEWINSDK MSG *msg = reinterpret_cast(message); bool returnValue = false; + auto isHoveringTitlebarButton = [&]() { + auto ht = msg->wParam; + return ht == HTMAXBUTTON || ht == HTMINBUTTON || ht == HTCLOSE; + }; + switch (msg->message) { - case WM_DPICHANGED: - returnValue = this->handleDPICHANGED(msg); - break; - case WM_SHOWWINDOW: returnValue = this->handleSHOWWINDOW(msg); break; @@ -666,6 +819,96 @@ bool BaseWindow::nativeEvent(const QByteArray &eventType, void *message, returnValue = this->handleNCHITTEST(msg, result); break; + case WM_NCMOUSEHOVER: + case WM_NCMOUSEMOVE: { + // WM_NCMOUSEMOVE/WM_NCMOUSEHOVER gets sent when the mouse is + // moving/hovering in the non-client area + // - (mostly) the edges and the titlebar. + // We only need to handle the event for the titlebar buttons, + // as Qt doesn't create mouse events for these events. + if (!this->ui_.titlebarButtons) + { + // we don't consume the event if we don't have custom buttons + break; + } + + if (isHoveringTitlebarButton()) + { + *result = 0; + returnValue = true; + + POINT p{GET_X_LPARAM(msg->lParam), GET_Y_LPARAM(msg->lParam)}; + ScreenToClient(msg->hwnd, &p); + + QPoint globalPos(p.x, p.y); + globalPos /= this->devicePixelRatio(); + globalPos = this->mapToGlobal(globalPos); + + // TODO(nerix): use TrackMouseEvent here + this->ui_.titlebarButtons->hover(msg->wParam, globalPos); + this->lastEventWasNcMouseMove_ = true; + } + else + { + this->ui_.titlebarButtons->leave(); + } + } + break; + + case WM_MOUSEMOVE: { + if (!this->lastEventWasNcMouseMove_) + { + break; + } + this->lastEventWasNcMouseMove_ = false; + // Windows doesn't send WM_NCMOUSELEAVE in some cases, + // so the buttons show as hovered even though they're not hovered. + [[fallthrough]]; + } + case WM_NCMOUSELEAVE: { + // WM_NCMOUSELEAVE gets sent when the mouse leaves any + // non-client area. In case we have titlebar buttons, + // we want to ensure they're deselected. + if (this->ui_.titlebarButtons) + { + this->ui_.titlebarButtons->leave(); + } + } + break; + + case WM_NCLBUTTONDOWN: + case WM_NCLBUTTONUP: { + // WM_NCLBUTTON{DOWN, UP} gets called when the left mouse button + // was pressed in a non-client area. + // We simulate a mouse down/up event for the titlebar buttons + // as Qt doesn't create an event in that case. + if (!this->ui_.titlebarButtons || !isHoveringTitlebarButton()) + { + break; + } + returnValue = true; + *result = 0; + + auto ht = msg->wParam; + + POINT p{GET_X_LPARAM(msg->lParam), GET_Y_LPARAM(msg->lParam)}; + ScreenToClient(msg->hwnd, &p); + + QPoint globalPos(p.x, p.y); + globalPos /= this->devicePixelRatio(); + globalPos = this->mapToGlobal(globalPos); + + if (msg->message == WM_NCLBUTTONDOWN) + { + this->ui_.titlebarButtons->mousePress(ht, globalPos); + } + else + { + this->ui_.titlebarButtons->mouseRelease(ht, globalPos); + } + } + break; + default: return QWidget::nativeEvent(eventType, message, result); } @@ -684,7 +927,8 @@ void BaseWindow::scaleChangedEvent(float scale) this->calcButtonsSizes(); #endif - this->setFont(getFonts()->getFont(FontStyle::UiTabs, this->qtFontScale())); + this->setFont( + getApp()->getFonts()->getFont(FontStyle::UiTabs, this->scale())); } void BaseWindow::paintEvent(QPaintEvent *) @@ -702,19 +946,52 @@ void BaseWindow::paintEvent(QPaintEvent *) void BaseWindow::updateScale() { - auto scale = - this->nativeScale_ * (this->flags_.has(DisableCustomScaling) - ? 1 - : getABSettings()->getClampedUiScale()); + auto scale = this->flags_.has(DisableCustomScaling) + ? 1 + : getSettings()->getClampedUiScale(); this->setScale(scale); - for (auto child : this->findChildren()) + BaseWindow::applyScaleRecursive(this, scale); +} + +// NOLINTNEXTLINE(misc-no-recursion) +void BaseWindow::applyScaleRecursive(QObject *root, float scale) +{ + for (QObject *obj : root->children()) { - child->setScale(scale); + auto *base = dynamic_cast(obj); + if (base) + { + auto *window = dynamic_cast(obj); + if (window) + { + // stop here, the window will get the event as well (via uiScale) + continue; + } + base->setScale(scale); + } + + applyScaleRecursive(obj, scale); } } +#ifdef USEWINSDK +void BaseWindow::updateRealSize() +{ + auto hwnd = this->safeHWND(); + if (!hwnd) + { + return; + } + + RECT real; + ::GetWindowRect(*hwnd, &real); + this->realBounds_ = QRect(real.left, real.top, real.right - real.left, + real.bottom - real.top); +} +#endif + void BaseWindow::calcButtonsSizes() { if (!this->shown_) @@ -722,24 +999,21 @@ void BaseWindow::calcButtonsSizes() return; } - if ((this->width() / this->scale()) < 300) + if (this->frameless_ || !this->ui_.titlebarButtons) { - if (this->ui_.minButton) - this->ui_.minButton->setScaleIndependantSize(30, 30); - if (this->ui_.maxButton) - this->ui_.maxButton->setScaleIndependantSize(30, 30); - if (this->ui_.exitButton) - this->ui_.exitButton->setScaleIndependantSize(30, 30); + return; + } + +#ifdef USEWINSDK + if ((static_cast(this->width()) / this->scale()) < 300) + { + this->ui_.titlebarButtons->setSmallSize(); } else { - if (this->ui_.minButton) - this->ui_.minButton->setScaleIndependantSize(46, 30); - if (this->ui_.maxButton) - this->ui_.maxButton->setScaleIndependantSize(46, 30); - if (this->ui_.exitButton) - this->ui_.exitButton->setScaleIndependantSize(46, 30); + this->ui_.titlebarButtons->setRegularSize(); } +#endif } void BaseWindow::drawCustomWindowFrame(QPainter &painter) @@ -749,63 +1023,57 @@ void BaseWindow::drawCustomWindowFrame(QPainter &painter) { QColor bg = this->overrideBackgroundColor_.value_or( this->theme->window.background); - painter.fillRect(QRect(1, 2, this->width() - 2, this->height() - 3), - bg); + if (this->isMaximized_) + { + painter.fillRect(this->rect(), bg); + } + else + { + // Draw a border that's exactly 1px wide + // + // There is a bug where the border can get px wide while dragging. + // this "fixes" itself when deselecting the window. + auto dpr = this->devicePixelRatio(); + if (dpr != 1) + { + painter.setTransform(QTransform::fromScale(1 / dpr, 1 / dpr)); + } + painter.fillRect(1, 1, this->realBounds_.width() - 2, + this->realBounds_.height() - 2, bg); + } } #endif } -bool BaseWindow::handleDPICHANGED(MSG *msg) -{ -#ifdef USEWINSDK - int dpi = HIWORD(msg->wParam); - - float _scale = dpi / 96.f; - - auto *prcNewWindow = reinterpret_cast(msg->lParam); - SetWindowPos(msg->hwnd, nullptr, prcNewWindow->left, prcNewWindow->top, - prcNewWindow->right - prcNewWindow->left, - prcNewWindow->bottom - prcNewWindow->top, - SWP_NOZORDER | SWP_NOACTIVATE); - - this->nativeScale_ = _scale; - this->updateScale(); - - return true; -#else - return false; -#endif -} - bool BaseWindow::handleSHOWWINDOW(MSG *msg) { #ifdef USEWINSDK - if (auto dpi = getWindowDpi(msg->hwnd)) + // ignore window hide event + if (!msg->wParam) { - this->nativeScale_ = dpi.get() / 96.f; - this->updateScale(); + return true; } - if (!this->shown_ && this->isVisible()) + if (!this->shown_) { + this->shown_ = true; + if (this->hasCustomWindowFrame()) { - this->shown_ = true; - - const MARGINS shadow = {8, 8, 8, 8}; - DwmExtendFrameIntoClientArea(HWND(this->winId()), &shadow); + // disable OS window border + const MARGINS margins = {-1}; + DwmExtendFrameIntoClientArea(msg->hwnd, &margins); } + if (!this->initalBounds_.isNull()) { - ::SetWindowPos(msg->hwnd, nullptr, this->initalBounds_.x(), - this->initalBounds_.y(), this->initalBounds_.width(), - this->initalBounds_.height(), - SWP_NOZORDER | SWP_NOACTIVATE); + this->setGeometry(this->initalBounds_); this->currentBounds_ = this->initalBounds_; } - } - this->calcButtonsSizes(); + this->calcButtonsSizes(); + this->updateRealSize(); + } return true; #else @@ -813,28 +1081,61 @@ bool BaseWindow::handleSHOWWINDOW(MSG *msg) #endif } +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +bool BaseWindow::handleNCCALCSIZE(MSG *msg, qintptr *result) +#else bool BaseWindow::handleNCCALCSIZE(MSG *msg, long *result) +#endif { #ifdef USEWINSDK - if (this->hasCustomWindowFrame()) + if (!this->hasCustomWindowFrame()) { - // int cx = GetSystemMetrics(SM_CXSIZEFRAME); - // int cy = GetSystemMetrics(SM_CYSIZEFRAME); - - if (msg->wParam == TRUE) - { - NCCALCSIZE_PARAMS *ncp = - (reinterpret_cast(msg->lParam)); - ncp->lppos->flags |= SWP_NOREDRAW; - RECT *clientRect = &ncp->rgrc[0]; - - clientRect->top -= 1; - } + return false; + } + if (msg->wParam != TRUE) + { *result = 0; return true; } - return false; + + auto *params = reinterpret_cast(msg->lParam); + auto *r = ¶ms->rgrc[0]; + + WINDOWPLACEMENT wp; + wp.length = sizeof(WINDOWPLACEMENT); + this->isMaximized_ = GetWindowPlacement(msg->hwnd, &wp) != 0 && + (wp.showCmd == SW_SHOWMAXIMIZED); + + auto borders = windowBordersFor(msg->hwnd, this->isMaximized_); + r->left += borders.left; + r->top += borders.top; + r->right += borders.right; + r->bottom += borders.bottom; + + if (borders.left != 0 || borders.top != 0 || borders.right != 0 || + borders.bottom != 0) + { + // We added borders -> we changed the rect, so we can't return + // WVR_VALIDRECTS + *result = 0; + return true; + } + + // This is an attempt at telling Windows to not redraw (or at least to do a + // better job at redrawing) the window. There is a long list of tricks + // people tried to prevent this at + // https://stackoverflow.com/q/53000291/16300717 + // + // We set the source and destination rectangles to a 1x1 rectangle at the + // top left. Windows is instructed by WVR_VALIDRECTS to copy and preserve + // some parts of the window image. + QPoint fixed = {r->left, r->top}; + params->rgrc[1] = {fixed.x(), fixed.y(), fixed.x() + 1, fixed.y() + 1}; + params->rgrc[2] = {fixed.x(), fixed.y(), fixed.x() + 1, fixed.y() + 1}; + *result = WVR_VALIDRECTS; + + return true; #else return false; #endif @@ -851,30 +1152,30 @@ bool BaseWindow::handleSIZE(MSG *msg) } else if (this->hasCustomWindowFrame()) { - if (msg->wParam == SIZE_MAXIMIZED) - { - auto offset = int( - getWindowDpi(HWND(this->winId())).value_or(96) * 8 / 96); - - this->ui_.windowLayout->setContentsMargins(offset, offset, - offset, offset); - } - else - { - this->ui_.windowLayout->setContentsMargins(0, 1, 0, 0); - } - this->isNotMinimizedOrMaximized_ = msg->wParam == SIZE_RESTORED; if (this->isNotMinimizedOrMaximized_) { - RECT rect; - ::GetWindowRect(msg->hwnd, &rect); - this->currentBounds_ = - QRect(QPoint(rect.left, rect.top), - QPoint(rect.right - 1, rect.bottom - 1)); + // Wait for WM_SIZE to be processed by Qt and update the current + // bounds afterwards. + postToThread([this] { + this->currentBounds_ = this->geometry(); + }); } this->useNextBounds_.stop(); + + if (msg->wParam == SIZE_MINIMIZED && this->ui_.titlebarButtons) + { + // Windows doesn't send a WM_NCMOUSELEAVE event when clicking + // the minimize button, so we have to emulate it. + this->ui_.titlebarButtons->leave(); + } + + RECT real; + ::GetWindowRect(msg->hwnd, &real); + this->realBounds_ = + QRect(real.left, real.top, real.right - real.left, + real.bottom - real.top); } } return false; @@ -888,45 +1189,52 @@ bool BaseWindow::handleMOVE(MSG *msg) #ifdef USEWINSDK if (this->isNotMinimizedOrMaximized_) { - RECT rect; - ::GetWindowRect(msg->hwnd, &rect); - this->nextBounds_ = QRect(QPoint(rect.left, rect.top), - QPoint(rect.right - 1, rect.bottom - 1)); - + // Wait for WM_SIZE (in case the window was maximized, we don't want to + // save the bounds but keep the old ones) this->useNextBounds_.start(10); } #endif return false; } +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +bool BaseWindow::handleNCHITTEST(MSG *msg, qintptr *result) +#else bool BaseWindow::handleNCHITTEST(MSG *msg, long *result) +#endif { #ifdef USEWINSDK - const LONG border_width = 8; // in pixels - RECT winrect; - GetWindowRect(HWND(winId()), &winrect); + const LONG borderWidth = 8; // in device independent pixels - long x = GET_X_LPARAM(msg->lParam); - long y = GET_Y_LPARAM(msg->lParam); + auto rect = this->rect(); - QPoint point(x - winrect.left, y - winrect.top); + POINT p{GET_X_LPARAM(msg->lParam), GET_Y_LPARAM(msg->lParam)}; + ScreenToClient(msg->hwnd, &p); + + QPoint point(p.x, p.y); + point /= this->devicePixelRatio(); + + auto x = point.x(); + auto y = point.y(); if (this->hasCustomWindowFrame()) { *result = 0; - bool resizeWidth = minimumWidth() != maximumWidth(); - bool resizeHeight = minimumHeight() != maximumHeight(); + bool resizeWidth = + minimumWidth() != maximumWidth() && !this->isMaximized(); + bool resizeHeight = + minimumHeight() != maximumHeight() && !this->isMaximized(); if (resizeWidth) { // left border - if (x < winrect.left + border_width) + if (x < rect.left() + borderWidth) { *result = HTLEFT; } // right border - if (x >= winrect.right - border_width) + if (x >= rect.right() - borderWidth) { *result = HTRIGHT; } @@ -934,12 +1242,12 @@ bool BaseWindow::handleNCHITTEST(MSG *msg, long *result) if (resizeHeight) { // bottom border - if (y >= winrect.bottom - border_width) + if (y >= rect.bottom() - borderWidth) { *result = HTBOTTOM; } // top border - if (y < winrect.top + border_width) + if (y < rect.top() + borderWidth) { *result = HTTOP; } @@ -947,26 +1255,26 @@ bool BaseWindow::handleNCHITTEST(MSG *msg, long *result) if (resizeWidth && resizeHeight) { // bottom left corner - if (x >= winrect.left && x < winrect.left + border_width && - y < winrect.bottom && y >= winrect.bottom - border_width) + if (x >= rect.left() && x < rect.left() + borderWidth && + y < rect.bottom() && y >= rect.bottom() - borderWidth) { *result = HTBOTTOMLEFT; } // bottom right corner - if (x < winrect.right && x >= winrect.right - border_width && - y < winrect.bottom && y >= winrect.bottom - border_width) + if (x < rect.right() && x >= rect.right() - borderWidth && + y < rect.bottom() && y >= rect.bottom() - borderWidth) { *result = HTBOTTOMRIGHT; } // top left corner - if (x >= winrect.left && x < winrect.left + border_width && - y >= winrect.top && y < winrect.top + border_width) + if (x >= rect.left() && x < rect.left() + borderWidth && + y >= rect.top() && y < rect.top() + borderWidth) { *result = HTTOPLEFT; } // top right corner - if (x < winrect.right && x >= winrect.right - border_width && - y >= winrect.top && y < winrect.top + border_width) + if (x < rect.right() && x >= rect.right() - borderWidth && + y >= rect.top() && y < rect.top() + borderWidth) { *result = HTTOPRIGHT; } @@ -974,26 +1282,55 @@ bool BaseWindow::handleNCHITTEST(MSG *msg, long *result) if (*result == 0) { - bool client = false; - - for (QWidget *widget : this->ui_.buttons) - { - if (widget->geometry().contains(point)) - { - client = true; - } - } - + // Check the main layout first, as it's the largest area if (this->ui_.layoutBase->geometry().contains(point)) - { - client = true; - } - - if (client) { *result = HTCLIENT; } - else + + // Check the titlebar buttons + if (*result == 0 && + this->ui_.titlebarBox->geometry().contains(point)) + { + for (const auto *widget : this->ui_.buttons) + { + if (!widget->isVisible() || + !widget->geometry().contains(point)) + { + continue; + } + + if (const auto *btn = + dynamic_cast(widget)) + { + switch (btn->getButtonStyle()) + { + case TitleBarButtonStyle::Minimize: { + *result = HTMINBUTTON; + break; + } + case TitleBarButtonStyle::Unmaximize: + case TitleBarButtonStyle::Maximize: { + *result = HTMAXBUTTON; + break; + } + case TitleBarButtonStyle::Close: { + *result = HTCLOSE; + break; + } + default: { + *result = HTCLIENT; + break; + } + } + break; + } + *result = HTCLIENT; + break; + } + } + + if (*result == 0) { *result = HTCAPTION; } @@ -1001,16 +1338,17 @@ bool BaseWindow::handleNCHITTEST(MSG *msg, long *result) return true; } - else if (this->flags_.has(FramelessDraggable)) + + if (this->flags_.has(FramelessDraggable)) { *result = 0; bool client = false; - if (auto widget = this->childAt(point)) + if (auto *widget = this->childAt(point)) { std::function recursiveCheckMouseTracking; recursiveCheckMouseTracking = [&](QWidget *widget) { - if (widget == nullptr) + if (widget == nullptr || widget->isHidden()) { return false; } @@ -1020,6 +1358,11 @@ bool BaseWindow::handleNCHITTEST(MSG *msg, long *result) return true; } + if (widget == this) + { + return false; + } + return recursiveCheckMouseTracking(widget->parentWidget()); }; @@ -1040,10 +1383,23 @@ bool BaseWindow::handleNCHITTEST(MSG *msg, long *result) return true; } + + // don't handle the message return false; #else return false; #endif } +#ifdef USEWINSDK +std::optional BaseWindow::safeHWND() const +{ + if (!this->testAttribute(Qt::WA_WState_Created)) + { + return std::nullopt; + } + return reinterpret_cast(this->winId()); +} +#endif + } // namespace chatterino diff --git a/src/widgets/BaseWindow.hpp b/src/widgets/BaseWindow.hpp index 9c9d9bc27..2f2168645 100644 --- a/src/widgets/BaseWindow.hpp +++ b/src/widgets/BaseWindow.hpp @@ -1,10 +1,13 @@ #pragma once +#include "common/FlagsEnum.hpp" +#include "util/WidgetHelpers.hpp" #include "widgets/BaseWidget.hpp" -#include #include -#include "common/FlagsEnum.hpp" +#include + +#include class QHBoxLayout; struct tagMSG; @@ -15,6 +18,7 @@ namespace chatterino { class Button; class EffectLabel; class TitleBarButton; +class TitleBarButtons; enum class TitleBarButtonStyle; class BaseWindow : public BaseWidget @@ -24,13 +28,15 @@ class BaseWindow : public BaseWidget public: enum Flags { None = 0, - EnableCustomFrame = 1, - Frameless = 2, - TopMost = 4, - DisableCustomScaling = 8, - FramelessDraggable = 16, - DontFocus = 32, - Dialog = 64, + EnableCustomFrame = 1 << 0, + Frameless = 1 << 1, + TopMost = 1 << 2, + DisableCustomScaling = 1 << 3, + FramelessDraggable = 1 << 4, + DontFocus = 1 << 5, + Dialog = 1 << 6, + DisableLayoutSave = 1 << 7, + BoundsCheckOnShow = 1 << 8, }; enum ActionOnFocusLoss { Nothing, Delete, Close, Hide }; @@ -39,47 +45,74 @@ public: QWidget *parent = nullptr); ~BaseWindow() override; - void setInitialBounds(const QRect &bounds); - QRect getBounds(); + void setInitialBounds(QRect bounds, widgets::BoundsChecking mode); + QRect getBounds() const; QWidget *getLayoutContainer(); - bool hasCustomWindowFrame(); + bool hasCustomWindowFrame() const; TitleBarButton *addTitleBarButton(const TitleBarButtonStyle &style, std::function onClicked); EffectLabel *addTitleBarLabel(std::function onClicked); - void setStayInScreenRect(bool value); - bool getStayInScreenRect() const; - void setActionOnFocusLoss(ActionOnFocusLoss value); ActionOnFocusLoss getActionOnFocusLoss() const; - void moveTo(QWidget *widget, QPoint point, bool offset = true); + void moveTo(QPoint point, widgets::BoundsChecking mode); - virtual float scale() const override; - float qtFontScale() const; + /** + * Moves the window to the given point and does bounds checking according to `mode` + * Depending on the platform, either the move or the show will take place first + **/ + void showAndMoveTo(QPoint point, widgets::BoundsChecking mode); + + /// @brief Applies the last moveTo operation if that one was bounds-checked + /// + /// If there was a previous moveTo or showAndMoveTo operation with a mode + /// other than `Off`, a moveTo is repeated with the last supplied @a point + /// and @a mode. Note that in the case of showAndMoveTo, moveTo is run. + /// + /// @returns true if there was a previous bounds-checked moveTo operation + bool applyLastBoundsCheck(); + + float scale() const override; + + /// @returns true if the window is the top-most window. + /// Either #setTopMost was called or the `TopMost` flag is set which overrides this + bool isTopMost() const; + /// Updates the window's top-most status + /// If the `TopMost` flag is set, this is a no-op + void setTopMost(bool topMost); pajlada::Signals::NoArgSignal closing; + pajlada::Signals::NoArgSignal leaving; static bool supportsCustomWindowFrame(); +signals: + void topMostChanged(bool topMost); + protected: - virtual bool nativeEvent(const QByteArray &eventType, void *message, - long *result) override; - virtual void scaleChangedEvent(float) override; +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + bool nativeEvent(const QByteArray &eventType, void *message, + qintptr *result) override; +#else + bool nativeEvent(const QByteArray &eventType, void *message, + long *result) override; +#endif + void scaleChangedEvent(float) override; - virtual void paintEvent(QPaintEvent *) override; + void paintEvent(QPaintEvent *) override; - virtual void changeEvent(QEvent *) override; - virtual void leaveEvent(QEvent *) override; - virtual void resizeEvent(QResizeEvent *) override; - virtual void moveEvent(QMoveEvent *) override; - virtual void closeEvent(QCloseEvent *) override; - virtual void showEvent(QShowEvent *) override; + void changeEvent(QEvent *) override; + void leaveEvent(QEvent *) override; + void resizeEvent(QResizeEvent *) override; + void moveEvent(QMoveEvent *) override; + void closeEvent(QCloseEvent *) override; + void showEvent(QShowEvent *) override; - virtual void themeChangedEvent() override; - virtual bool event(QEvent *event) override; - virtual void wheelEvent(QWheelEvent *event) override; + void themeChangedEvent() override; + bool event(QEvent *event) override; + void wheelEvent(QWheelEvent *event) override; void mousePressEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; @@ -89,48 +122,79 @@ protected: void updateScale(); - boost::optional overrideBackgroundColor_; + std::optional overrideBackgroundColor_; private: void init(); - void moveIntoDesktopRect(QWidget *parent, QPoint point); + void calcButtonsSizes(); void drawCustomWindowFrame(QPainter &painter); void onFocusLost(); - bool handleDPICHANGED(MSG *msg); + static void applyScaleRecursive(QObject *root, float scale); + bool handleSHOWWINDOW(MSG *msg); - bool handleNCCALCSIZE(MSG *msg, long *result); bool handleSIZE(MSG *msg); bool handleMOVE(MSG *msg); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + bool handleNCCALCSIZE(MSG *msg, qintptr *result); + bool handleNCHITTEST(MSG *msg, qintptr *result); +#else + bool handleNCCALCSIZE(MSG *msg, long *result); bool handleNCHITTEST(MSG *msg, long *result); +#endif bool enableCustomFrame_; ActionOnFocusLoss actionOnFocusLoss_ = Nothing; bool frameless_; - bool stayInScreenRect_ = false; bool shown_ = false; FlagsEnum flags_; - float nativeScale_ = 1; - bool isResizeFixing_ = false; + bool isTopMost_ = false; struct { QLayout *windowLayout = nullptr; QHBoxLayout *titlebarBox = nullptr; QWidget *titleLabel = nullptr; - TitleBarButton *minButton = nullptr; - TitleBarButton *maxButton = nullptr; - TitleBarButton *exitButton = nullptr; + TitleBarButtons *titlebarButtons = nullptr; QWidget *layoutBase = nullptr; std::vector